ドメインモデルの完全な状態を保つ方法


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`クラスにフィールドやロジックが増えてくるとコードの重複が多くなってしまいそうです。

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




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

話は若干逸れますが、そもそも相互参照がつらいって話があるので、その話から。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の状態を再現しなければならないので、状態管理が難しくなるでしょう)。




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

話は若干逸れますが、そもそも相互参照がつらいって話があるので、その話から。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の状態を再現しなければならないので、状態管理が難しくなるでしょう)。

