当章では、複雑な例を第二章で使用したパターンを用いてどう解決するかを記載する。
輸送貨物会社のための新しいソフトウェアを開発する。
最初は3つの基本的な機能を要求。
-
顧客貨物に対する主要な荷役を追跡する。
-
あらかじめ貨物を予約する。
-
貨物が荷役の過程で所定の場所に到達した際に、自動敵に請求書を顧客に送付する。
-
ふつうは、モデルの改良と設計および実装がイテレーティブな開発システムで、連携して進められなければいけない。
モデルを変更するきっかけになるのは、構成要素パターンを採用した実装に対して、モデルを結び付ける必要が生じた場合に限定する。 -
モデルが設計をよりよく支えるように改良される際には、ドメインへの新しい洞察も反映するように改良されなければならない。
以後は、説明を明確にするため、変更のきっかけはモデルと設計を結びつけることに限定する。
ドメインの責務が、システムの他の部分が持つ責務と混ざらないように、レイヤ化アーキテクチャを適用してドメイン層を区切る。
- 特定の貨物に対して行われた過去と現在の荷役にアクセスできる追跡問い合わせ
- 新規の貨物を登録できるようにして、それに対してシステムの準備を整える予約アプリケーション
- 貨物に対して行われた書く荷役を記録できるイベント記録アプリケーション(追跡問い合わせで検索される情報を提供する)
これらのアプリケーションは調停役(coordinator)であり、質問に対する答えを考えてはならない。それはドメイン層の仕事である。
追跡すべき同一性や、表現されている基本的な値を探す。
まず、明快なケースを見た後に、より曖昧なケースについて検討する。
-
顧客
- 顧客オブジェクトは個人や企業を表現する。
実体を持つ、実際に使われる意味でのエンティティ。
顧客オブジェクトは、ユーザにとって重要な同一性が明らかに存在するから、モデルにおいてもエンティティとして扱える。
今回の例では、ドメインエキスパートとの協議のうえ、最初の営業で顧客それぞれにIDを付与していたので、そのIDを流用する。
- 顧客オブジェクトは個人や企業を表現する。
-
貨物
- 似たものでも区別する必要があるため、貨物オブジェクトはエンティティである。
-
荷役イベントと郵送機器移動
- 個々のイベントの留意するのは、何が起きているかを追跡できるようになるから。
こうしたイベントは現実世界での出来事を反映して、通常は置き換えられないのでエンティティである。
核輸送機器移動は、輸送スケジュールから得られるコードで識別される。
今回は、貨物ID、官僚時刻、タイプの組み合わせで荷役イベントが一位に特定できる。
同じ貨物に対して、荷積みと荷下ろしが両方同時に行われることはない。
- 個々のイベントの留意するのは、何が起きているかを追跡できるようになるから。
-
位置
- 2つの場所は名前が同じであっても、同一のものではない。
緯度と経度から一意なキーが得られるが、あまり実用的ではない。
位置は何らかの地理的なモデルの一部となるだろう。
地理的なモデルは、輸送経路やその他ドメイン特有の関心事に従って、場所同士を結び付ける。
位置を識別するためには、内部で使用される任意の自動生成識別子を用いれば十分。
- 2つの場所は名前が同じであっても、同一のものではない。
-
配送記録
- 配送記録は変更できないので、エンティティである。
しかし、配送記録には貨物と一対一の関係があるので、独自の同一性を持っていない。
同一性は、所有者の貨物から借用する。
- 配送記録は変更できないので、エンティティである。
-
配送仕様
- 貨物の目標を表すが、貨物に依存していない。
実際の表現は、配送記録の状態に対する仮説。
貨物と結びついた配送記録は、最終的に貨物の配送仕様を満たすことが望ましい。
同じ場所に向かっている2つの貨物がある場合、配送仕様を共有できるが、配送記録を共有できないので、配送仕様は値オブジェクトである。
- 貨物の目標を表すが、貨物に依存していない。
双方向の関連は、設計上問題になる。
関連をたどれる方向は、ドメインに対する洞察をとらえていることが多く、モデル自体を深めもする。
- 貨物と顧客
- 貨物すべてに対して、顧客が直接参照を持っていると、顧客が長年のリピータであった場合に扱いにくくなる。
顧客の概念は貨物に固有のものではない。
巨大なシステムは、顧客には多くのオブジェクトに対して果たす役割があると考えられるので、そういう特定の責務からは解放しておくことが一番。
顧客から貨物を検索する機能が必要ならば、データベースクエリで実現できる。
- 貨物すべてに対して、顧客が直接参照を持っていると、顧客が長年のリピータであった場合に扱いにくくなる。
- 輸送機器移動と荷役イベント
- 船の目録を追跡することが目的なら、輸送機器移動から荷役イベントへ辿ることが重要になるだろうが、今回のアプリでは貨物の追跡で充分。
荷役イベントから輸送機器移動への関連のみ辿れるようにするだけでよい。
- 船の目録を追跡することが目的なら、輸送機器移動から荷役イベントへ辿ることが重要になるだろうが、今回のアプリでは貨物の追跡で充分。
- その他の論理的決定は図7.2で説明しているので、そちら参照。
- 循環参照
- トレードオフ
貨物が配送記録を知り、配送記録は荷役イベントを保持し、荷役イベントが貨物を参照する。
循環参照は、設計上必要だが維持管理しにくい。実装上の選択として、同期させなければならない同じ情報を2か所で持たれないようにすることでうまくいくかもしれない。
今回のプロトタイプでは、配送記録に荷役イベントをリストで所持する。
いつか、コレクションを止めて、貨物をキーとしたデータベースを行うことにする。
リポジトリの選択において、良く取り上げられる。 記録を参照が稀であれば、DBを検索する実装で性能は工場、保守は単純になり、荷役イベントを追加する際のオーバーヘッドも減る。
多いの場合は、直接参照を管理する方が適している。 実装の単純さと性能の間でバランスがとられる。どちらも、モデルは変わらずに循環と双方向の関連を含んでいる。
- トレードオフ
貨物が配送記録を知り、配送記録は荷役イベントを保持し、荷役イベントが貨物を参照する。
顧客、位置、輸送機器移動には独自の同一性があって、多くの貨物に共有されている。
そのため、独自に集約をもって、かつルートになる。
貨物も集約ルートだが、どこに境界を引くかを決めるには検討が必要。
貨物の集約は、配送記録、配送仕様、荷役イベントを含むことになる。
これは、配送記録によくあてはまる。
貨物を必要とせずに、配送記録を直接探す人はいないと思えるからだ。
直接的なアクセスが必要なく、同一性が貨物から派生するだけなので、配送記録は貨物の境界内に収まってルートである必要はない。
配送仕様は値オブジェクトなので、貨物の集約でも問題ない。
荷役イベントに関しては話が別。
荷役イベントを検索する2つのデータベースクエリの可能性を考察した。
- コレクションの代わりとして、配送記録に関する荷役イベントを見つけるというもので、貨物集約内で閉じることになるだろう。
- 特定の輸送機器移動に向けて、荷積みと準備を行うすべての業務を見つけるのに使用されるだろう。 後者では、貨物自体から切り離して考えても意味を持つと思われるため、荷役イベントは独自の集約ルートであるべき、という結論。
集約のルートとなるエンティティは5つ。
リポジトリを検討する際には、これらに限定できる。
他のオブジェクトは、リポジトリを持つことが認められない。
- 予約アプリケーション
- 予約を取るために配送人、荷受人などの顧客を選択する必要があるので、顧客リポジトリが必要となる。
貨物に荷だし知として指定するための位置が必要なので、位置リポジトリを作成する。
- 予約を取るために配送人、荷受人などの顧客を選択する必要があるので、顧客リポジトリが必要となる。
- アクティブティ記録アプリケーション
- ある貨物が積まれた輸送機器移動をユーザが検索できるようにする必要がある多面、輸送機器移動リポジトリが必要。
ユーザはどの貨物が荷積みされたかについて、システムに伝えなければならないから、貨物リポジトリも必要。
- ある貨物が積まれた輸送機器移動をユーザが検索できるようにする必要がある多面、輸送機器移動リポジトリが必要。
- 荷役イベントリポジトリは?
- 最初のイテレーションでは、配送記録との関連をコレクションとして実装しているため、不要。
輸送機器移動に、何が荷積みされているかを調べるというアプリケーションの要求が無いからでもある。
どちらも変わる可能性があり、変わった場合はリポジトリを追加することになる。
- 最初のイテレーションでは、配送記録との関連をコレクションとして実装しているため、不要。
これまでの決定をすべてクロスチェックするためには、常に段階を追ってシナリオを見ながら、アプリケーションの問題を効果的に解決できることを確かめる必要がある。
貨物の輸送先を変更する機能。
配送使用は値オブジェクトなので、貨物のセッタを使用して、新しい配送仕様で古いものを置き換える。
同じ顧客によって繰り返される予約は似ている傾向がある。
新規の貨物のプロトタイプとして前回の貨物を利用したい。
これは、プロトタイプパターンを使用して設計する。
貨物はエンティティであり、集約のルートでもあるので、コピーは慎重にしなければならない。
- 配送記録
- 古い記録は適用できないため、新しいからの記録を生成する。
集約の境界内にあるエンティティは、新規作成が普通。
- 古い記録は適用できないため、新しいからの記録を生成する。
- 顧客の役割
- 役割をキーとした顧客を参照するコレクションは、キーを含めてコピーする。
新規の輸送でも、前回と同じ役割を果たす可能性が高いからだ。
しかし、顧客オブジェクト自体をコピーせず、前回貨物が参照していたのと同じ顧客オブジェクトを参照する必要がある。
顧客オブジェクトは集約の境界外にあるエンティティ。
- 役割をキーとした顧客を参照するコレクションは、キーを含めてコピーする。
- 追跡ID
- 新規の貨物を一から作成するのと同じように、新しい追跡IDを提供する。
貨物集約の境界内にあるものはすべてコピー、一部修正したが、集約の境界外のものに対しては全く影響を及ぼしていないことが大事。
貨物用の手の込んだファクトリがあったり、別の貨物をファクトリとして使用したりする場合でも、プリミティブなコンストラクタは必要。
そのコンストラクタを用いて生成したいのは、不変条件をを満たしているオブジェクトか、エンティティの場合に少なくとも同一性が侵されていないオブジェクトである。
これらの決定を踏まえると、次のようなファクトリメソッドを貨物上に作成することになる。
public Cargo copyPrototype(String newTrackingId)
あるいは、次のような独立したファクトリにメソッドを作成してもよい。
public Cargo newCargo(Cargo prototype, String newTrackingId)
独立したファクトリは、新規の貨物のために新しいIDを取得するプロセスをカプセルかすることもでき、その場合必要な引数は一つだけとなる。
public Cargo newCargo(Cargo prototype)
これらのファクトリが戻す結果はどれも同じ、配送記録がカラで配送使用がnullの貨物である。
貨物と配送記録との間にある、この双方向の関連が意味するのアh、貨物も配送記録も相手を参照していなければ完全ではないので、一緒に生成しなければならない。
貨物が、配送記録を含んだ集約のルートであることを忘れないでほしい。だからこそ、貨物のコンストラクタやファクトリに配送記録を生成させることができる。
配送記録のコンストラクタは、引数として貨物を取る。
public Cargo(String id){
trackingId = id;
deliveryHistory = new DeliveryHistory(this);
customerRoles = new HashMap();
}
このコンストラクタでは、自らを参照し返す新しい配送記録を持った新規の貨物である。
配送記録のコンストラクタは、それを含む集約のルート、すなわちかもつによってのみ使用され、それにより貨物の構造がカプセル化される。
貨物が取り扱われるたびに、イベント記録アプリケーションを使って荷役イベントを入力する。
// TODO
モデリングと設計は前進するだけのプロセスではない。
モデルと設計を改善する新しい洞察を活用しなければ、立ち往生する。
現時点でも機能するが、扱いにくい側面がある。
荷役イベントを追加するときに配送記録を更新する必要があるため、貨物集約がトランザクションに巻き込まれる。
荷役イベントの入力は迅速かつ単純でなければならない操作で、アプリケーションで重要なのは競合することなく入力できることである。
配送記録が持つ荷役イベントのコレクションをクエリで置き換えることで、整合性を意識せずとも、荷役イベント自体を追加することができる。
荷役イベントが入力されることが多く、問い合わせが稀であればこの設計はより効果的になる。
コレクションではなくクエリを使用することで、循環参照において一貫性を保つのも容易になる。
この責務のために、荷役イベント用のリポジトリを追加する。
特定の貨物に関係したイベントに対するクエリをサポートする。
これで、配送記録に永続された状態が無くなる。
エンティティが繰り返し生成されても、同じ貨物オブジェクトとの間に関連があるため、何度も生成されるインスタンスをつなぐ連続性が維持されているから。
貨物ファクトリは単純化され、からの配送記録を付け加えることはない。
このシステムで、貨物が到着するまでにユーザが問合せすることが稀であれば、多くの不要な作業も一緒に止めることができる。
追加機能はまだ要求されておらず、今後も要求されないかもしれないので、選択肢のために多くのことはしたくはない。 重要なのは、こうした自由が許されるのは同一モデルの中だけに限られる。
荷役イベントリポジトリを追加する必要もあったが、荷役イベント自体の再設計が必要になることはなかった。
多くのオブジェクトを取り上げなかったため、モジュール性は問題にならなかった。
今度は輸送モデルのもう少し大きい部分を取り上げて、モデルが影響を受けるところを見る。
図7.7では、結果として概念的にほとんど関係ないオブジェクトが一緒に詰め込まれ、すべてのモジュール間の関連が無秩序に走っている。
このパッケージは、輸送に関するものではなく、開発者が読み取ったストーリーである。
パターンに従って分割するのは一つの方法である。
大事なことは、凝集度の高い概念を探して、プロジェクトが伝えたいことに集中させること。
図7.8のモジュール名はチームの言語に貢献している。
モジュール一式を使って、ストーリーを語ることができる。
現段階では、モデル駆動設計を助け、ユビキタス言語に貢献している。
- 別システムの設計を受け入れると、モデルの維持が困難になる。 そのため、2つのシステムの間を翻訳する腐敗防止層を使用する。
- 「別システムインタフェース」というシステム名のインターフェースにするのではなく、「配分チェックサービス」等の責務を反映した名前を付けるべき。
- 自分のシステムにはない概念を相手のシステムの概念でそのまま取り込むのはもったいない。
自分のシステム用の観点で抽象化するチャンス。
貨物にカテゴリが存在するという知識に適用できるようにする必要がある。 - 今回の例では、分析パターンからエンタープライズセグメントという解決策を得られた。
エンタープライズセグメントが各貨物に対して、値オブジェクトとして追加される。
- (水上)学ぶべき内容はないので、スキップ。
エンタープライズセグメントを取得する責務をなぜ貨物に持たせないのか。
一見、貨物が持つべきのように見えるが、エンタープライズセグメントは、ビジネス戦略にとって有用な輪郭に沿って分割される。
目的が変われば異なるセグメントになる可能性がある。
今回は、予約を配分する目的で特定の貨物のセグメントを取得しようとしているが、税金会計のためであれば全く別のエンタープライズセグメントになるかもしれない。
この配分用エンタープライズセグメントも、新しい販売戦略のために販売管理システムが再構築されれば変わるかもしれない。
貨物が配分チェックサービスについて知る必要が生じるが、貨物の概念上の責務から外れて、特定の種類のエンタープライズセグメントを取得するためのメソッドを大量に背負い込むことになる。
この値を取得する責務は、セグメントのルールを理解しているオブジェクトに課すのが適切であり、データが持っているオブジェクトに課すべきではない
そうしたルールは別の戦略オブジェクトに切り出して、貨物に渡し、エンタープライズセグメントを取得できるようにしてもよい。
この解決策は、要求を超えているが、のちの設計における選択肢の一つになりうるし、破壊的な変更にはならないはずだ。
- DBのテーブルはMVPから複雑なInnerJoinしてあるべき?
- externalは腐敗防止層?repository <= 高木も同じ疑問、XXXXでいうexternalは��腐敗防止層�なきがする、本来のHexagonal�Archtecutureの externalと、腐敗防止層の違いがよくわからない
- �機能追加があった時に、単体テストが邪魔になった、、、
- クラスに対してテストを書くのではなく、PXXXで習ったようにフリーダムさを重視したい、でもそうするとパッケージングが重要になるなぁ(感想)
- 設計変更で、配送仕様が荷役イベントのリストを�持つのではなく、クエリ=リポジトリにしたが、実装上は配送仕様のgetHistory()に荷役リポジトリ.find(貨物ID)みたいな感じであってるか
- エンタープライズセグメントの具体例が欲しい、行っていることはわかるが実装できない、ようは���異なるEntityは多様な分類があるから、分類を値オブジェクトにするということ?必ずしもそうした方が良いのか?
- �IDDDではドメインイベントのモデリングが載っている�ぽいが知っている人がいたら教えて欲しい