集約(Aggregate)(DDD)

参考書籍一覧(Amazon アソシエイトリンク)

構築と永続化、破棄をするドメインモデルの単位。一貫性(不変条件=>契約による設計を参照)を維持する単位とも言える。

集約は図示すると、ルートエンティティを頂点として、参照で繋がったオブジェクトのネットワークになる。後述するが、ネットワーク構造ではなく、ツリー構造となるように構築した方が良いだろうと個人的には考えている。 ネットワーク構造の集約

このオブジェクトのネットワークにおいて、ネットワーク外から参照されるのはルートエンティティだけである。つまり、ネットワークの構成が変わらない限りは、ルートエンティティ配下のネットワークの形は変わることがない(コレクション要素の増減は別にして)。

簡単な例として、リクルートサイトで履歴書を処理する機能を考える。履歴書には学歴と職務経歴が複数紐づくので以下のようなクラスになるだろう。

# 履歴書
class Resume
  def initialize(name, birthday, educational_backgrounds, careers)
    @name = name
    @birthday = birthday
    @educational_backgrounds = educational_backgrounds # 学歴の配列
    @careers = careers # 職務経歴の配列
  end
end

# 学歴の一つ一つ
class EducationalBackground
  def initialize(from, to, name)
    # ...
  end
end

# 職務経歴の一つ一つ
class Career
  def initialize(from, to, summary)
    # ...
  end
end

この時、Resumeクラスがルートエンティティとなり、オブジェクトの参照はツリー構造になっている。EducationalBackgroundCareerは他から参照されてはならない。履歴書なので、この3つのクラスは1つの単位で扱うことになる。学歴や職務経歴のない履歴書や誰のものともわからぬ学歴だけを使っても意味のあることはできないからだ。誰のものかもわからない学歴だけを更新することはないし、学歴を残して履歴書を削除することもない。

要点としては以上で、以下は枝葉の話である。

ツリー構造の集約

前述の通り、集約の構造はネットワーク構造ではなく、ツリー構造にした方が良いと私は考えている。複数方向から参照されることで単一責任の原則に違反するリスクが高まることが理由である。バリューオブジェクトは内部に持つ値を使った操作としてイニシアチブをバリューオブジェクト側が握っていられるが、エンティティの場合は使う側がエンティティをどう使いたいかという意識でメソッドを追加しがちなため、エンティティが責務に対するイニシアチブを持っていないと考えた方が良い。

例えば、履歴書に職歴(過去の在籍歴)が追加され、職務経歴にその時勤めていた会社が追加されたとする。会社は職歴からも職務経歴からも参照されるので集約はネットワーク構造となる。職務経歴の正当性をチェックするため、職務内容がその会社で妥当なものか判断する機能も追加することにもなったとすれば、多分Company#has_job?のようなメソッドが追加されるだろう。しかし、履歴書の持つ職歴からはそのメソッドが使われることはなく、たやすく単一責任の原則に違反してしまう。

ネットワーク構造が原因で単一責任の原則違反を起こす例

そのため、そもそもクラスを分けてしまうか、職歴が会社名しか使わないというのなら職歴のフィールドとして会社名を持ってしまって職歴側からは会社クラスを参照しないといった方法を取った方が良い。

ルートエンティティから集約内部のエンティティを外部に渡すこと、集約内部から他の集約へ参照することはエリック・エヴァンスのドメイン駆動設計においては、許可されている。

ルートエンティティは内部のエンティティへの参照を他のオブジェクトに渡せるが、受け取ったオブジェクトは参照を一時的に使用することができるだけで、その参照を保持してはならない。

(中略)

集約内部のオブジェクトは、他の集約ルートへの参照を保持することができる。

エリック・エヴァンスのドメイン駆動設計 第6章 ドメインオブジェクトのライフサイクル 集約(AGGREGATES)

これらについても、単一責任の原則に違反するリスクを上げるだけなので、避けた方が良いと私は考えている。前者は外部からしか使われないメソッドを追加されやすく、後者は集約本来の責務の他に別の集約に属する集約としての責務を負うことになりやすい。

ただ、あくまで責務のイニシアチブを外部に取られやすいエンティティや集約に限った話であり、バリューオブジェクトや仕様(Specification)、あるいはその他のビジネスルールを記述したクラスのようなイニシアチブを取られないものは、さまざまな場所から参照されたとしても問題はない。また、サービスレイヤ(ServiceLayer)パターンからの参照数を制限するものではない。アプリケーションサービスもドメインサービスもエンティティや集約の責務にそった使い方をする存在であり、責務の問題はドメインレイヤの設計で解決しなければならない(解決の結果、あるサービスからの集約への参照が別の集約への参照に変更されることも当然ある)。

集約の責務についての別の話題として、永続化用のクラスを用意した方が良いというのがある。いわゆる、ドメインモデルだけ分けたCQRSである(これをCQRSと呼ぶのは違うのでは? と個人的には思っているが一部の人はCQRSという言葉を使っている)。読み込んだデータを使って何かする操作(Company#has_job?など)と永続化時に実行する操作(バリデーションや保存するデータを計算するなど)は異なる責務であり交わることがない。そのため、単一責任の原則違反であり別々の集約に分けた方が良い。必要なら新規追加と更新、削除のそれぞれで集約を用意することになるだろう。