ドメイン駆動設計の集約のわかりにくさの原因と集約を理解するためのヒント

ドメイン駆動設計』のモデル要素のひとつとして「集約」があります。 アプリケーションの対象となる事業活動の仕組みや決め事をソフトウェアで表現する技法のひとつとして集約の考え方はとても役に立ちます。

集約パターンはデータベースのデータ整合性の視点での説明されることが多いようです。しかしデータ整合性の文脈で集約を理解しても、ドメイン駆動設計の中核の関心事である「ドメインの複雑さ」を理解しドメインの知識をクラスで表現するためにはあまり役に立ちません。

この記事では、集約パターンをドメインロジックを表現するモデルの構成要素として効果的に利用するためのヒントを提供したいと思います。

集約はデータ操作の道具ではありません。集約はビジネスルールにもとづくドメインロジックのモデリングと実装の手段です。ここがわかるとドメイン駆動設計の理解が一気に進むと思います。

どうして集約がデータ整合性の話になってしまうのか

集約は「第6章 ドメインオブジェクトのライフサイクル」で基本パターンのひとつとして説明されています。 この章はオブジェクトの永続化と復元をとりあつかっています。集約パターンのほかにリポジトリパターンとファクトリーパターンが登場します。

6章のオブジェクトのライフサイクルの関するパターン、特にリポジトリの考え方はそれ自体はドメイン(問題領域)に由来しません。エヴァンスが書いているとおりです。

6章はオブジェクトの永続化と復元の複雑さを「ドメインモデルの本来の関心事」から分離するためのパターンの紹介です。この章は他の章と異なり、ドメインの知識をどうモデリングしクラスとして表現するかよりも、データベース操作と密接に関係した内容を中心に書かれています。『ドメイン駆動設計』の中で、この章だけがドメインロジックよりもデータベースに関心事の焦点が当たっています。

その結果、この章で登場する集約パターンはデータベース操作やデータ整合性の視点からの説明が多く、またそう理解している人も多いようです。

データの記録と参照はアプリケーションの重要な関心事

アプリケーションの機能として、データの記録と参照は重要な関心事です。設計や実装に多くの時間を使うのは、データの記録と参照かもしれません。その観点からすれば、集約を「データの集約」として理解し、データベースのトランザクションと関係づけて考えるのが自然だし、多くの開発者にとって理解がしやすいのかもしれません。

ドメイン駆動設計の関心の焦点はデータではなくロジック

しかし、ドメイン駆動設計の関心の焦点はデータの記録と参照ではありません。ドメイン駆動設計が立ち向かおうとしているソフトウェアの複雑さはデータの扱い方の複雑さではなく、ドメインロジックの複雑さです。

ドメインロジックとはビジネスルール(事業活動の決め事)に基づく計算や判断のロジックです。もちろん、計算や判断にデータを使います。しかし、ドメインロジックの複雑さはデータの扱い方の問題ではなく、計算式や判断条件の複雑さをどのように整理して記述するかの問題です。

集約パターンがわかりにくいのは、本来はドメインロジックの複雑さを整理しクラスの組み合わせで表現する設計パターンである集約を、永続化やデータ整合性という別の関心事とからめて説明しているためです。集約を理解するには、まず、集約を永続化の関心事から切り離して考えたほうがわかりやすくなります。

ロジックの置き場所としてクラスを考える

ドメイン駆動設計を理解し実践するための基本は、オブジェクト指向プログラミングのクラス設計のアプローチです。

オブジェクト指向プログラミングでは、クラスは「データを整理」する手段ではありません。クラスは「ロジックを整理」する手段です。金額・数量・日付・区分値など、ビジネスで扱う値の種類を分類し、それぞれの値に対する計算・判断のロジックをそれぞれのクラスに集めて整理するための手段がクラスです。

クラスは内部に計算・判断に使うデータを持ちますが、それは内部に隠蔽されます。クラスを利用する側が意識するのは内部のデータではなく、操作(メソッド)だけです。

メソッドは、内部に保持したインスタンス変数を使った計算判断ロジックの置き場所です。メソッドがそのクラスに所属している理由は、そのメソッドがそのクラスのインスタンス変数を常に使う必然性があるからです。クラスは内部に保持したデータを操作するロジックを集めるための入れ物です。

ドメイン駆動設計は、このロジックの置き場所としてのクラス、というオブジェクト指向プログラミングのクラス設計の考え方を前提にしています。クラスをロジックの置き場所として設計することがドメイン駆動設計を理解し実践するための基本です。

関心の分離と集約

6章の「ドメインオブジェクトのライフサイクル」にでてくる集約も、まずオブジェクト指向プログラミングの基本である、クラスはロジックの置き場所という視点で理解することが役に立ちます。

集約は、複雑なロジックを複数のクラスの組み合わせで表現する設計パターンです。ひとつのクラスにさまざまな計算判断ロジックを詰め込んでしまうのは「関心の分離」の原則に反するアンチパターンです。

金額に関する計算判断と日付に関する計算判断は別にクラスに分けて整理します。金額も円ベースの価格計算と、円以外の支払い通貨に換算した請求金額の計算があれば、別のクラスに分けて関心事を分離したほうがコードをすっきりと整理できます。

このような役割を明確に分離したクラスを組み合わせて、より複雑なロジックを表現します。複雑なロジックを表現するためにクラスを組み合わせる。これが本来の集約パターンの使いどころです。

集約を設計する時にはデータの整合性や永続化を考えないようにします。ドメインロジックを整理するためには、データの整合性や永続化は異なる関心事です。ドメインロジックの整理にデータの整合性を持ち込むことは、クラス設計を不必要に複雑にします。集約は、ドメインロジックを表現するクラス設計を単純に保つための考え方であり工夫です。永続化の関心事をそこに持ち込んではいけません。

またオブジェクトをできるだけ不変(イミュータブル)に設計することで、そもそもデータの整合性をクラス設計の問題として考える必要性はなくなります。クラスをイミュータブルに設計するのも、クラス設計を単純で分かりやすくするための工夫です。

ドメイン駆動設計の集約とはロジックの集約であり関連の設計である

ドメイン駆動設計』の「5章 ソフトウェアで表現されたモデル」で、最初に取り上げられているのが「関連の設計」です。

5章は、エンティティ・値オブジェクト・ドメインサービスがパターンの形式で説明されていて、ドメイン駆動設計の説明でもこの3つのパターンはよくとりあげられます。一方、「関連」はパターン名として登場していないためかあまり取り上げられることはありません。

しかし集約を理解し活用するには、クラスの組み合わせ方を表現する関連の設計の理解と実践が基本です。関連を考えることが集約の設計の核心です。

5章で登場する集約の具体例

クラス設計では関連の設計はもっとも基本的な設計要素の一つです。エヴァンスが5章を関連の説明から始めているのもそういうことです。

5章では証券取引口座と他のクラスの関連の例が説明されています。

f:id:masuda220:20210507132856p:plain
証券取引口座

このようにクラスとクラスの関係をモデリングしたものが「集約」です。

この集約の例ではどのような計算判断のロジックに関心があり、その計算判断ロジックをどのクラスのメソッドとして提供するかは、まだはっきりしていません。分析と設計が進むにつれ、ビジネス的に重要な計算判断ロジックが特定され、適切な場所にメソッドとしてロジックが配置されていくはずです。

銘柄単位の計算判断ロジックは投資クラスに配置するだろうし、口座番号ごとの合算や評価のロジックは証券取引口座クラスが持つことになるでしょう。

複数の関心事の境界がはっきりしない集約の例

集約の別の例を見てみましょう。

この図は、集約に複数の関心事(複数の計算判断ロジック)が混在している例です。

f:id:masuda220:20210507132924p:plain
ロジックの置き場所の設計例

注文の合計金額と送料計算は、異なる関心事(異なる計算ロジック)です。

図として相互につながっていますが、注文金額を計算するための集約と、送料を計算するための集約とに分けて考えたほうがよさそうです。もちろん、配送先は注文の一部なので、その情報をどうやって送料計算の集約で参照可能にするかという設計課題はあります。この図のクラス設計のイメージのままでは、うまく実装できないかもしれません。

また、送料体系クラスは、送料の計算時に計算の条件によってはデータベースの料金設定テーブルへの参照が必要になるかもしれません。ここらへんになってくると「6章 ドメインオブジェクトのライフサイクル」の議論が関係してきます。

データの操作は別の関心事としてクラス設計からは分離する

しかし、送料を計算するロジックをクラスでどう表現するかを検討する時には、いったんはデータベース参照は別の関心事としてとらえ、クラス設計の一部として考えないようにします。そして、ロジックの表現としてわかりやすい集約のクラス設計ができたら、その後でデータベース参照の設計と実装を検討します。

ドメイン駆動設計あるいはオブジェクト指向プログラミングのクラス設計では、データベース操作の関心事は別の関心事として徹底的に分離するほうが良い設計ができます。 

異なる関心事を同時に考えると頭がこんがらがってしまいます。よい設計はできません。①ロジックの置き場所としてクラスを設計する、②データの記録と参照の視点でテーブルを設計する、③オブジェクトとレコードのマッピング方法の設計と実装を検討する。この3つを別々の関心事として分離して設計することでわかりやすい良い設計ができるようになります。

集約の設計は、ロジックの置き場所の設計として考える

5章の「関連の設計」はクラスの集約の考え方の基本です。そして、エヴァンスのようにオブジェクト指向プログラミングでクラス設計をすることがあたりまえの開発者にとっては、集約の設計がクラスの設計、つまりロジックの置き場所の設計であることをあらためて強調するという発想にはなりづらかったのだと思います。

集約がロジックの整理と表現の手段であることを強調しないまま「6章 ドメインオブジェクトのライフサイクル」の永続化とデータ整合性に関する設計課題として集約を説明したために、本来のクラスの集約の考え方とは異なって説明され解釈されることが多くなってしまったのでしょう。

6章も注意深く読めばドメインロジックの整理に焦点があたっています。タイヤの例は、走行距離に応じたタイヤのローテーションルールの話しです。購入注文の例は、購入注文の承認限度額ルールの話しです。

集約はロジックを整理するためのパターン

クラスの集約の設計は、永続化とは異なる関心事です。クラスの集約はデータの扱い方の設計ではなく、ロジックの記述と整理するための設計パターンです。集約の境界は「あるロジックを表現するために必要なクラスの範囲」として検討します。そういうクラスの集約の設計パターンを理解し実践できている前提で、はじめて6章のオブジェクトのライフサイクルとデータ整合性が重要な設計課題として議論されています。

ドメイン駆動設計』を理解する基礎固めの一つとして、集約をデータ整合性ではなく、ロジックの置き場所の設計として考えてみましょう。

ドメイン駆動設計』の後半の「しなやかな設計」にでてくるシェアパイ演算や「大規模な構造」にでてくるコアドメインとしての経路選択ロジックなどの説明を「集約の設計は計算判断ロジックの整理と記述」という視点で読むことで、ドメイン駆動設計についての理解が深まるはずです。