2460


Javaでドメイン駆動設計を実践しようとしています。

質問ですが、DBレコードからドメインモデルを完全に構築するのがコスト面で現実的でない場合、どのように実装すべきでしょうか？

例えば、`Line`が多数の`Point`を持ち、かつ、`Point`から`Line`を参照可能なモデルがあるとき、指定するidの`Point`の完全な状態のドメインモデルを構築するには、`Point`、`Line`に加え、`Line`に属する全ての`Point`のDBレコードを獲得しなくてはなりません。仮に`Line.points`の数が非常に多い場合、完全な状態のドメインモデルを構築するコストが高くなります。

[code]

    public class Line {
        private Long id;
        private List<Point> points;
    }
    public class Point {
        private Long id;
        private Line line;
    }
    // setter, getter は省略
    
[/code]

この場合、`Line.points`を遅延ロードするというのが一般的な解決策でしょうか？
遅延ロードは遅延ロードを行う操作が行われるまでDBセッションを閉じられないのが少し気にかかります。

別の策として、似て非なる`Line`と`Point`クラスを用意することも考えましたが、`Line`や`Point`クラスにフィールドやロジックが増えてくるとコードの重複が多くなってしまいそうです。

皆さんはこういったケースではどのように実装されていますか？


CC BY-SA 3.0
2014-12-22T16:24:06.447
2394
2015-01-03T09:23:55.207
2014-12-23T01:46:18.833
208
5531
question
java ドメイン駆動設計
ドメインモデルの完全な状態を保つ方法


Line が、points を保持する集約ルートとなるドメインであるならば、そのライフサイクルにおいて points
を復元せずに機能するフェーズはごく短いと思われます。ただし懸念されているように points が非常に多い場合は性能上の考慮が必要です。

「間引いた情報だけで機能が提供できるなら必要な points のみ復元する」「隣り合う point 間の距離の総和を求めるような場合は DB
に集計用クエリを発行してスカラー値として取得する」など、全ポイント復元を極力避けるようにすべきかと。

ドメインモデルって、対象ドメインを周辺的な処理から隔離してなるべくピュアに表現しようとする試みなので、データアクセスはリポジトリの責務として簡潔なメソッドを提供するのがよいです。DB
セッションの問題についても、場合によっては ORM にまかせず Line ドメインの中でリポジトリを使って Point を復元するなどコードで LAZY
を実現することも考えられます。

ただ、語弊があるかもしれませんが DDD
はそもそもがコスト高な世界(工数的にも計算機資源的にも)なので、モデルがそれほど巨大でなければ基本は完全構築なんじゃないでしょうか。ORM
でのフェッチ戦略はデフォルト EAGER、パフォーマンス上のリスクがある場合に LAZY
を検討。その時も上記のようにリポジトリや集約ルートのドメインに構築の責務を持たせる。メモリがたくさん必要でも N+1
問題が発生するよりはマシかなと思います。


CC BY-SA 3.0
2014-12-23T11:51:33.800
2429
2014-12-23T11:51:33.800
2718
2394
answer


非常に面白い話題ですね！ちょっと簡単にまとめてみました。参考になれば幸いです。

話は若干逸れますが、そもそも相互参照がつらいって話があるので、その話から。DDD二部
関連の章を読んでいたら理解できると思いますが、双方向の関連は仕様を満たしているか検証する実装コストが高いと思います。

[code]

    Line line = new Line(id);
    Point point = new Point(id);
    
    point.setLineId(line.getId()); // --- (A)
    line.addPoint(point); // --- (B)
    
[/code]

この場合、(A),
(B)は両方の状態が正しく設定されていないといけません。しかし、実際は(A)もしくは(B)の参照の設定を忘れてしまうと、どちらが正しい状態を示しているか見分けがつかなくなります。

まぁ、それだけ参照が増えるというのは設計や実装が複雑になる原因になるってことですね。つらい。ということで、エリックさん曰く、便利なだけの参照はやめて、本質的な参照に絞れって話らしい。つまり、相互参照をできるだけ諦めて単一方向の参照に絞れと。

次に、集約の境界定義について。集約にはオブジェクト同士の関連の爆発を防ぐための役割もあります。

ひとつ質問がありまして、PointはLineの一部(つまりPointという集約の一部)でしょうか？それともLineはPointからみて外部のオブジェクトでしょうか？

前者であれば、PointはLineの一部という考え方です。Pointは所属しているLineがひとつしかないので、Point.lineを保持するメリットはさほどないと思っています。

[code]

    // Lineはグローバルエンティティ
    public class Line {
      private Long id;
      private List<Point> points;
      // その他の属性
    }
    
    // Pointはローカルエンティティ
    public class Point {
      private Long id;
      // その他の属性
    }
    
[/code]

上記だと、Point内部でLineにアクセスできないという問題があるかもしれません。であれば、少し変形させてPointからLineの機能にアクセスできるようにする方法もありますね。

[code]

    // Lineはグローバルエンティティ
    public class Line {
      private Long id;
      private List<Point> points;
      // その他の属性
    
      // Pointはローカルエンティティ
      public static class Point {
        private Long id;
        // その他の属性
      }
    }
    
[/code]

後者、つまり両方がグローバルエンティティだとしても、集約内部に外部の集約を内包せずに識別子で間接的な参照を作るべきだと思います。識別子自体はその集約の一部なので境界定義としての一貫性は保証されます(そもそも集約というのは、内部に複数のオブジェクトを抱えていても、それ自体で1個のオブジェクトとみなす考え方なので、外部の集約を含めるのは現実的ではない)。では、実体はどこから手に入れればいいかというと、リポジトリから取得すればいいわけですね。一手間入って面倒というのはありますが、ドメインモデル間の結合度は下がります。

[code]

    // グローバルエンティティ
    public class Line {
      private Long id;
      private List<Long> pointIds;
      // その他の属性
    }
    
    // グローバルエンティティ
    public class Point {
      private Long id;
      private Long lineId;
      // その他の属性
    }
    
    List<Point> points = pointRepository.resolveAllByPointIds(line.getPointIds);
    Line line = lineRepository.resolveAllByLineId(point.getLineId);
    
[/code]

この例は、間接であってもこれは相互参照なので、仮に子が親を知っているという前提であれば、単一方向参照にできますね。LineIdでPointを取得したい場合はリポジトリに問い合わせればよいですし。

[code]

    public class Line {
      private Long id;
      // その他の属性
    }
    
    public class Point {
      private Long id;
      private Long lineId;
      // その他の属性
    }
    
    List<Point> points = pointRepository.resolveAllByLineId(line.getId());
    
[/code]

以上、僕がいつも考えている設計と実装はこんな感じです。

追記：

補足ですが、DDD二部のライフサイクル管理の章を読んでいただくとわかりますが、集約をベースにしたファクトリやリポジトリなどのライフサイクル管理はユビキタス言語由‌​来ではないので、ドメイン上の課題とは直接関係ありません。

ファクトリやリポジトリの存在意義としては、ドメインモデルから永続化などの複雑な責務を分離し、ドメイン上の課‌​題解決に専念するためのものです。なので、便利だからといって、ドメインモデル内にリポジトリ機能などを安易に実装してしまうと、このようなリスクに直面することになります‌​。あえてリスクテイクするってのもありですが。僕はいまのところそういう設計は複雑になるだけだと思っているのでやっていませんね。

Lineからみて複数のPointがあって、それを遅延ロードしたいという性能上の要求がドメイン上の課題解決より優先される場合はあります。その場合は、Line,
Pointをそれぞれグローバルエンティティ化してしまえば、いくらでも遅延ロードできます(LineからみてPointがローカルエンティティの場合は、その時点での複‌​数のPointを含むLineの状態を再現しなければならないので、状態管理が難しくなるでしょう)。


CC BY-SA 3.0
2014-12-24T02:52:16.990
2460
2014-12-24T05:49:30.163
2014-12-24T05:49:30.163
5610
5610
2394
answer


おお、DDDの質問だ。蛇足ながらちょっと、コメントを加えてみようと思います。

Lineは、直線なので、Pointで表すとすると2つのPointが必要ですね。

Pointが3つ以上の複数で、同一直線上にないとすると(PointはListなので順序がありますね)、

  * LinerApproximation(近似直線)
  * Polygonal Chain(折れ線)

のどちらかでしょうか？そのあたりをモデルで記述できているとよいですね(名前はとっても大事です)。

そこで、DDDで大事なのは、そこでどういう振る舞いを期待しようとするかです。

近似直線の相関値を計算したいのか？ 折れ線の状態を確かめたいのか(交差がないかとか、閉じてるかどうかとか)？

そこでの振る舞いに合わせて、適切な表現方法を探すことになります。 ひょっとすると、DDD本の航路みたいに、Legの連続で表すほうが適切かもしれませんね。

もうちょっと、それらのクラスをどう使いたいのかをご説明いただけると、よりよいコメントができるかもしれません。


CC BY-SA 3.0
2014-12-24T09:05:06.533
2487
2014-12-24T09:05:06.533
5671
2394
answer


ほぼ今出ている回答で十分かつ、重複もあるとは思いますが、私の意見も。

> DBレコードからドメインモデルを完全に構築するのがコスト面で現実的でない場合、どのように実装すべきでしょうか？
> 皆さんはこういったケースではどのように実装されていますか？

私であれば

  1. 前提としてそもそも本当に相互参照が必要なのかを検討。そして複雑さを回避する為、原則相互参照は採用しない。
  2. `Line` > `Point` の一方向で関連を制限したという前提で  
2-1. 一方向であれば性能上の問題にならない場合、`Line` の `Repository` 内ですべての `Point` を取得する  
2-2. 性能上の問題になりえる場合は、キャッシュの検討、または限定的に採用するにはあまり美しくはないが CQRS の様なアプローチで Query
として、ドメイン層を介在しないサービスを作り JSON などの DTO
を直接返す、他の回答者の方々が指摘されている様なドメイン特有の限定的、効率的な取得が可能かを検討

といったアプローチをとります。

ただし、やはりドメインモデリングの話しになる為、より明確な実装案を提示するには、例示のドメインを明確にする必要がでてきます。

DDD ベースの設計は、パフォーマンスのトレードオフとなる選択が求められる事が多い為、原理主義にならず常に現実の要求とバランスさせる事が重要です。


CC BY-SA 3.0
2014-12-24T15:58:21.957
2507
2014-12-24T15:58:21.957
5644
2394
answer


集約について、きちんと整理したほうが良いと思いました。

LineとPointを異なる集約として設計するのか、Line
集約の内部のモデル（Pointは値オブジェクトで十分と思われる）としてPointを実装するのかによって、設計方針が大きく異なります。

# 判断の基準

  * 複数のPointを同一トランザクションで一貫性を持って変更する必要がある場合は、Line集約の内部モデルとしてPointを実装するのが良いです。その場合、PointにはLineを介してしか触ることはできません。
  * 同一トランザクションでの一貫性が必要ない場合は、LineとPointを別の集約として実装することで、Line集約のインスタンスが肥大化することを防ぐことが出来ます。

この辺りの詳細は、Implementing Domain Driven Design
の第10章が詳しいです。Qiitaでも簡単にまとめてくれている人がいます。

  * [ドメイン駆動設計: Aggregate実装チェックシート](http://qiita.com/suin/items/a171550c49de078e39fd)

# LineとPointを別集約で実装する場合

Junichi Kato
さんが書かれているとおりです。PointからLineへの参照をlineIdで実装することで、集約間の関係を疎結合にすることができます。

# Line集約の内部モデルとしてPointを保持する場合

Point自身のidに大きな意味は無いので、おそらくPointは値オブジェクトとして実装したほうば良いです。

Point数が大きい場合にモデルの構築コストが大きくなりますが、トランザクショナルな一貫性が必要なら、この制約は受け入れざるを得ないでしょう。再利用するオブジェクトをキャッシュするなど、別のところで工夫は出来そうです。

haradakiroさんが言うように、そもそもこのモデルが適切なのかも再考した方が良いと思います。LineやPointを読込みおんりーで利用するなら、CQRSでリード専用のDTOを実装する方法もあります。


CC BY-SA 3.0
2015-01-03T09:23:55.207
2976
2015-01-03T09:23:55.207
5997
2394
answer