これは何?
DDDを読んだときのメモ書きです。
大事なところを写したり、理解できないところを手打ちすることによって頭に入れようとしています。
後半はアクティブリコールを実践しました。
時々読んでる中で思ったことや調べたことも書いてます。
他人に読まれることを意識しておらず、チームに還元できるような形で別の記事にする予定なので、これを他人が読んでどうこうというよりはただの読みましたよアピールです。
DDDの3原則
- コアドメインに集中する
- ドメインの実践者とソフトウェアの実践者が共同作業を通じてモデルを探求すること
- 明示的な境界づけられたコンテキストの中でユビキタス言語を語る
ユビキタス言語が大事、ととにかく言っている
ユビキタス言語: 設計上の意思決定を行うためのフレームワーク、ドメイン設計について議論するための技術的な語彙
モデルを意識するだけでなく、スタイルやテクニックが必要
- ほとんどのソフトウェアプロジェクトにおいて、一番の焦点は、ドメインとドメインロジックに合わせなければならない
- 複雑なドメインの設計はモデルに基づかなければならない
ドメインとモデルの違いは?
DDD(ドメイン駆動設計)の概要をまとめてみた https://zenn.dev/arsaga/articles/e095eb6574fcca
コンテキストをモデルに詰めすぎない
1UseCase1Modelの考え方で対応する?
DDDは設計とプロセス(プロジェクトの進め方)、両方を扱う
本の構成
- ドメイン駆動開発の基本的な目標の紹介。用語の定義と、ドメインモデルを使用するということがどういう意味をもつのかを概観
- オブジェクト指向ドメインモデリングにおけるベストプラクティスのコアを要約して、基本的な構成要素の集まりを作る。モデルと実際に動作するソフトウェアの溝を埋める
- 個別の構成要素を超えて実用的なモデルを組み立てる。発見のプロセスを重視する
- 大規模システムとの相互作用において発生する状況に対処する
ドメイン駆動設計におけるモデルの有用性
モデルとは当面の問題を解決する上で関連する側面を抽象化し、それ以外の詳細を無視することによって行われた、現実に対する一つの解釈
-
モデルと設計の核心が相互に形成し合う
モデルと実装が密接に結びつくことによってモデルはドメインと深く関連したものになり、モデルに対する分析が最終的に動くプログラムに適用されることが保証される -
モデルはチームメンバ全員が使用する言語の基盤である
-
モデルは蒸留された知識である
モデルはドメインの知識を構成して最も関心のある要素を区別するためのチーム内で取り決めた方法である
用語を選択し、概念を分解して関連付ける際に、ドメインについてどう考えることにしたかということが、1つのモデルによって捉えられる
蒸留?
=> なんか余計な概念が削ぎ落とされた、みたいな意味っぽい
ソフトウェアの核心
ソフトウェアの核心は、ドメインに関係した問題をユーザのために解決する能力である
技術者やアナリストが自分の専門スキルにのみ関心を寄せると、全体の目的から外れた仕事になってしまう
知識を噛み砕く
ネット: 任意のコンポーネント(チップ)に接続できる導体で、接続されたものすべてに電気信号を伝える
コンポーネントインスタンス(レフデス): チップでなくてもよい、同じコンポーネントが複数ある場合もあるのでインスタンスをつけている
ネット: あるインスタンスの特定のピンを別のインスタンスの特定のピンに接続する。ピンは1つのコンポーネントインスタンスにのみ属し、一つのネットにしか接続されない
トポロジ: すべてのネットに存在する、ネットの要素がどう接続されるかを決定する配置の仕方
プローブシミュレーション: 信号の伝播を追跡し、設計において問題がありそうな場所を検出する機能
コンポーネントはpushによってあるピンから送られてきた信号を特定のピンに伝達する
ホップ: 信号がネットを通過するたびにカウントが増える
プッシュ: コンポーネントタイプに依存し、同じタイプを持つコンポーネントインスタンスで共通している
効果的なモデリングの要素
- モデルと実装を結びつける: 素早いプロトタイプによって本質的な結びつきが早い時期に作り出され、維持された
- モデルに基づいて言語を洗練させる
- 知識豊富なモデルを開発する: 単なるデータスキーマではなく、オブジェクトにはふるまいと守るべきルールがある
- モデルを蒸留する: 不要な概念を削ぎ落とし、本質的な概念を切り分ける
- ブレインストーミングと実験を行う
知識豊富な設計
モデルによって捉えられる知識は「名詞をみつけること」にとどまらない。
ビジネスの活動やルールも、ドメインに含まれるエンティティと同じようにドメインにとって中心的
例1.1
航海(Voyage) has many 貨物(Cargos)
10%のオーバーブッキングを認める
オーバーブッキングの判定をベタ書きからポリシーにする
=> ドメインエキスパートが開発者の助けを得ながらも、そのまま理解できるようなコードを書く
輸送について、はじめは貨物の物理的移動だとモデリングしていたが、法的責任の移動の方が本質的だった
ユビキタス言語
モデルを言語の骨格として使用する。チーム内のすべてのコミュニケーション、コード、図、ドキュメントにおいてその言語を厳格に用いること
ユビキタス言語における変更はモデルに対する変更である
ドメインエキスパートはドメインについての理解を伝えるには使いにくい用語や構造に意義を唱えるべきであり、
開発者は設計を妨害することになるあいまいさ、不整合に目を光らせるべき
例2.1
貨物は0または1つの輸送日程を持つ
貨物には経路仕様(荷受け、荷出し、通関地点を持つ)が設定されており、初回入力時や属性の変更時には輸送日程を毎回生成する
輸送日程が新しくなったときに、補助的な計画まで全て再生成するのは無駄なので、変更された経路仕様から輸送日程を見て仕様が満たされなくなったときのみ経路選択サービスに輸送日程を再生成させる
荷受け、荷出し地が変わったときは必ず輸送日程が変わるだろうが、毎回仕様の比較を行ったほうがシンプルなのでケースを分けない
ドメインエキスパートに理解できないモデルには何らかの問題がある
ドメインエキスパートがモデルの言語を使ってユースケースを書いたり、受け入れテストを定義できるようになる
技術的な分野、ビジネスの分野におけるスコープ外の言語(方言)も同じドメインに対して、異なるモデルを反映した別の語彙を含んではならない
ドキュメントと図
図を用意して皆で見ることで集中が保たれ、また図内の言葉を自然に使うようになる
すべてを詳細に書く必要はなく、そもそもUMLはモデルが表している概念の意味
とオブジェクトが何を行うことになっているか
を伝えることはできない
図はルールに縛られることなく、不必要な詳細を排除して注意すべき部分に目が向くように簡略したものになるようにする
=> 技術的にスキのない図を作るんじゃなくて伝えたいことにフォーカスしろということと理解
設計に関する本質的な詳細はコードにおいてとらえられる
コードが語る、というのは真だが詳細すぎて圧倒してしまうことがある。
コードがやっていることをドキュメントでもやろうとするべきではない(コードと会話の補足にとどめる)
意味を説明し、大規模な構造への理解を与える。設計ドキュメント
説明のためのモデル
実装・設計、チーム内のコミュニケーションに関するモデルは1つであるべきだが、機能を満たす最小限のスコープとなっている
説明のためのモデルを使うことで、特定の話題について人々の理解を助けることができる
説明のためのモデルではメタファを使うことはあるが、UMLは避け、設計を推進するモデルと区別できるようになっていなければならない
純粋な分析モデルは人間の理解を助けることには役立つが、設計の問題に留意しないまま作られるため、実装とはゆるやかな関連を持つだけになる
モデル駆動設計のアプローチは、分析としても設計としても機能するモデルを要求する
設計で使用する用語法と責務の基礎的な割当をモデルから引き出す
コード側の要請でモデルを変更することもある、というのは難しそうだなぁ
プッシュ通知とかってなってくるとどうするんだろう?プッシュ通知クライアントとかってモデルにとって重要な存在ではなくない?とも思う
=> 別に実装上の全ての要素をモデルにするわけじゃないっぽい?(リポジトリとかインポートとか)
例2.2
PCBエンジニアはネットを同じルールを共有すべき自然なグループに帰属するものととらえる
ネットにはバスを形成するものがある
ネットをバスにする最、一度に8,16や256の単位で行うことでまとまりを扱いやすい量に切り詰める
ネットを1つずつ編集するのではなく、バスとして一括でルールを適用させたいがレイアウトツールがそれに対応していない状況
ネットの一覧から、ある名前に前方一致するを持つネットを全て取得し、レイアウトルールに入力されたバスルールで挿入するスクリプトを書いていた
モデル駆動設計
ネットとバスは抽象ネットを継承し、どちらに対してもルールの適用ができるようにする
なぜモデルがユーザ(システムの使用者)にとって重要なのか
ドメインモデルをそのままユーザに見せてもほとんどの場合はユーザにとって都合が良いものにはならないが、ユーザインタフェースの中にドメインモデル以外の幻想を作り出そうとすれば、
その幻想が完璧でない限り混乱が生じるものである
実践的モデラ
製造業のように高度なスキルを持つ技術者が設計し、スキルのない労働者が製品を組み立てるのとソフトウェア開発は違う。
ソフトウェア開発はすべてが設計である。責任を過剰に分離することはモデル駆動設計の妨げになる。
モデルに貢献する技術的な人は全て一定の時間をコードに触れることに費やさなければならない
コードの変更に対して責任を負う人はだれでも、コードを通してモデルを表現することを習得しなければならない
すべての開発者はモデルに関する議論にいずれかの段階で参加してドメインエキスパートと話をしなければならない
モデル駆動設計の構成要素
モデル駆動設計は責務駆動設計・契約による設計から多くの要素を引き継いでいる
レイヤ化アーキテクチャ
UIやDBへのアクセスに関するコードがビジネスオブジェクトに直接書かれてしまうと、コードを見て理解するのが困難な量になり、凝集度の高いモデル駆動のオブジェクトを実装することが現実的でなくなる
UI
アプリケーション層:
責務はビジネスにとって意味があるものか、ほかシステムのアプリケーション層と相互作用するもの。レイヤは薄く保たれ、ビジネスルールや知識を持たずやるべき作業を調整するだけ。
ビジネスの状況を反映する状態は持たないが、ユーザやプログラムが行う作業の進捗を反映する状態を持つことはできる
ドメイン層(モデル層)
インフラストラクチャ層
MVCで構成されるRailsと比較すると、UI=> View Application => Controller Domain,Infra => ActiveRecordになっているから
トランザクションをActiveRecordのモデル内で貼る、みたいなのが気持ち悪く感じるんだ、というのがわかった
例4.1資金振替
- アプリケーション層はインフラ層にTransactionの開始を指示し、ドメインに振替のメッセージを送ったあとコミットする
レイヤを関連付ける
下位のレイヤにあるオブジェクトが情報と通信をしなければならない場合に活用されるのが、コールバックやオブザーバといったレイヤ同士を関係づけるためのアーキテクチャパターンである
モデルビュー分離パターンのアプリケーションコーディネータ: 画面遷移をコントロールするパターン(?)
とにかくUIとドメインを分離できればパターンは何でもよい
インフラ層の技術的な機能はほとんどの場合サービス
として提供され、アプリケーションはいつ使用するかは知っているが、どのように行われるかまでは知らない
アーキテクチャフレームワーク
フレームワークが規定したクラスのサブクラスとして実装するとき、サブクラスが親クラスより上位のレイヤにあるのは直感に反しているようだが、他方の知識をより反映しているのがどちらのクラスなのかを意識すること
すべてのドメインオブジェクトをエンティティBeanとして実装するのではなく、粒度の大きいオブジェクトだけにして、ほとんどのビジネスロジックを普通のJavaオブジェクトで実装することでフレームワークの持つ欠点を回避する
ドメイン層はモデルが息づく場所
モデルの概念を反映するドメイン層
利口なUI(SmartUI)
SmartUIとドメイン駆動設計は両立しない
SmartUIが有効なパターンもある。その際はJavaのような柔軟な言語ではなく、4GL風のツールを使うべき
4GL(第四世代言語):
4GLはプログラマだけではなく、エンドユーザーでも簡単なパラメーターを対話形式で指定するだけで、表計算のような業務処理を行ったり、あるいはプログラムを作成したり出来るようになっているのが特徴である。
アーキテクチャがドメイン層を隔離して、システムの他の部分と疎結合にできるのであれば、そのアーキテクチャはドメイン駆動設計を支えられるだろう
ソフトウェアで表現されたモデル
関連を設計して無駄をなくすところから始める
モデル上の選択と実装上の関心事との間にある関係性を精査することで、モデルを表現する3パターンの要素の区別に専念する
パターン: エンティティ、値オブジェクト、サービス
サービス: オブジェクトとしてよりアクションとして表現したほうが自然になるもの
関連をあつかいやすくする
- 関連を辿る方向を強制する
- 限定子を付加して多重度を効果的に減らす
- 本質的ではない関連を除去する
2: 限定子を付加して多重度を効果的に減らす
多対多の関連のうち、ある属性によって限定されるようなものを見つけアクセサを単純化する
関連に対する制約が見つかったらそれをモデルと実装に含めるべき
Railsでいうとhas_many をスコープつけてhas_oneにするとかそういうのかな
エンティティ
概念上の同一性: オブジェクトの複数の実装やその格納形式、実世界の登場人物の間で一致しなければならない
エンティティ: 主要な定義が属性によってなされず同一性のつながりを表現するオブジェクト
同じ2つのオブジェクトを同一であるとみなす上では同じ属性を持たなくてもよく、必ずしも同一クラスである必要もない
e.g. 明細書の取引を小切手台帳の取引と照合するとき
あるオブジェクトが属性ではなく同一性によって識別されるのだれば、モデルでこのオブジェクトを定義する際はその同一性を第一とすること
形式や履歴に関係なくオブジェクトを識別する手段を定義すること
エンティティにとって最も基本的な責務はふるまいが明確で予測可能になるよう、連続性を確立すること
エンティティから余計な属性やふるまいを他のオブジェクトに移動するなどで削ぎ落とし、その概念にとって本質的なふるまいと、そのふるまいが必要とする属性だけにすること
エンティティは所有するオブジェクトの操作を調整することによって責務を果たすようになる
値オブジェクト
エンティティの同一性を追跡するのは本質的なことだが、それ以外のオブジェクトに同一性を与えてしまうとシステムの性能を損なうことになり、分析作業が増え、
さらに全てのオブジェクトの見た目がおなじになってしまうことでモデルが台無しになりかねない
ドメインにおける記述的な側面を表現し、概念的な同一性を持たないものを値オブジェクトという
値オブジェクトはエンティティを参照することもできる
あるモデル要素について、その属性しか関心の対象とならないのであれば、その要素を値オブジェクトとして分類すること
値オブジェクトを不変なものとして扱うこと、同一性を与えず、エンティティを維持するために必要となる複雑な設計を避けること
フライウェイトを使って、値オブジェクトの重複を減らしパフォーマンスを向上させることができる
インスタンスを共有するとき
- DB内でスペースやオブジェクト数を節約することが極めて重要である場合
- 通信オーバーヘッドが低い場合
- 共有オブジェクトが完全に不変である場合
値オブジェクトの同士の双方向の関連は完全に取り除くようにすること
双方向の関連がある場合はエンティティであることを見落としている
サービス
エンティティや値オブジェクトにはなじまない、活動や行動を表すオブジェクト
ドメイン層の一部である
優れたサービスの特徴
- 操作がドメインの概念に関係しており、その概念がエンティティや値オブジェクトの自然な一部ではない
- ドメインモデルの他の要素の観点からインターフェースが定義されている
- 操作に状態がない
イベントモデリングみたいなもんかなぁ
サーバと隔離されたドメイン層
ドメイン層に属するサービスを他のレイヤのサービスから区別すること
ドメイン層とアプリケーション層のサービスはインフラストラクチャ層のサービスと協力して動作する
メールなどの通知などカプセル化されたインターフェースがインフラストラクチャ層のサービス
アプリケーションサービスとドメインサービスの区別
アプリケーションサービス: 通知の指示を行う
ドメインサービスやアプリケーションサービスはエンティティと値オブジェクトで構成される集合体の上に構築され、ドメインに本来備わっている能力をまとめ上げて、実際に何らかの処理を行うスクリプトのように振る舞う
ドメインオブジェクトと外部リソースの間に直接インターフェースを設けるのではなく、モデルの用語で入力を受取り結果を戻すファサードを被せる
細粒度のサービスだとドメイン層からアプリケーション層へ知識が流出するかもしれない。
クライアントの操作や用途の広さよりも、インターフェースの単純さが優先される
モジュール(Package)
DDDの文脈だと、いくつかのオブジェクトのまとまり、みたいな意味合いっぽい
モジュールを選択する際には、システムに関する物語を伝え、概念の凝集した集合を含んでいるものを選ぶこと
モジュール間は低結合になるようにし、そうでなければモジュールの基礎となる概念を見逃していないか模索すること
モジュールのリファクタは難しいが、頑張って再構成するしかない
インフラストラクチャ駆動パッケージングの落とし穴
レイヤ化アーキテクチャはUI, インフラのコードを別々のパッケージに入れてドメイン層の分離を促すので効果的
ティア化アーキテクチャはモデルオブジェクトの実装が断片化されるかもしれない
J2EEではデータとデータアクセスをエンティティBeanに、ビジネスロジックをセッションBeanに置くが、オブジェクトモデルから凝集度が奪われてしまう
更に、エンティティBean, セッションBeanは別々のパッケージに分離されてしまうため、頭の中で組み合わせて単一の概念的なエンティティにするのは難しい
例外としてコードが宣言的設計に基づいて生成される場合は邪魔にならないよう別個のパッケージに入れた方が良い
モデリングパラダイム
基本はオブジェクト指向が理解しやすく普及しているので良いが、高度に数学的なドメインやシステム全体に関わる論理的推論に支配されるドメインなどオブジェクト指向パラダイムにうまくなじまないものもある
オブジェクトの世界におけるオブジェクトでないもの
ドメインモデルはオブジェクトモデルでなくてもよい
Prologで実装され、モデルが論理ルールとファクトから構成されているモデル駆動設計もある
局所的にそれに適した別のパラダイムを使用することによって、より簡単・柔軟に実装を進めることができる
しかし、インフラストラクチャ(e.g. RDB)では複数のパラダイムにまたがる一貫したモデルを作成することは困難でサポートツールを共存させるのも単純にできることではない
パラダイムを混在させる際にはモデル駆動設計に忠実であること
ルールエンジンの実装を見てみる
ルールパラダイムとオブジェクトパラダイム、どちらの実装でも使える単一のモデルを見つけなければならない
2つの環境で一貫した名前を適用し、その名前をユビキタス言語で使い続けることで両者の溝を埋めるのに役立つ
パラダイムを混在させるという負担を引き受ける前に、支配的なパラダイムの中にある選択肢を徹底的に検討すべき
この辺全部大事だな
ドメインオブジェクトのライフサイクル
寿命の長いオブジェクトには2つの課題がある
- ライフサイクルを通じて整合性を維持すること
- ライフサイクルを管理するのが複雑でも、モデルが浸食されないようにすること
集約: 所有権と境界を定義する
ファクトリ: ライフサイクルの始まりに焦点を合わせ、集約やオブジェクトを生成、再構成する
リポジトリ: ライフサイクルの中期と終わりに対応して、永続化されたオブジェクトにアクセスする手段を提供しつつ、アクセスに伴って必要となるインフラをカプセル化する
集約が区切るスコープによって不変条件が維持されなければならない範囲が示される
集約
集約にはルートと境界がある
境界: 集約の内部に何があるかを定義する
ルート: 集約に含まれている特定の1エンティティ
集約のメンバの中で外部のオブジェクトが参照を保持してよいのはルートだけ
ただし、教会内のオブジェクトは互いに参照を保持し合っても良い
ルート以外のエンティティは局所的な同一性を持っているが、その同一性は集約内部でのみ識別できれば良い
不変条件
データが変更されるときは常に維持されなければならない一貫性のルール
- 集約のルートエンティティはグローバルな同一性を持ち、不変条件をチェックする責務を負う
- ルートエンティティはグローバルな同一性を持つ。境界内部のエンティティは集約内でのみ位置位となるローカルな同一性を持つ
- 集約の境界外にあるオブジェクトはルートエンティティを除き、境界内部への参照を保持できない
- DBに問い合わせて直接取得できるのはルートエンティティのみで、それ以外のオブジェクトはすべて関連をたどって取得しなければならない
- 集約内部のオブジェクトは他の集約ルートへの参照を保持できる
- 削除の操作は集約境界の内部に存在するあらゆるものを一度に削除しなければならない
- 集約境界の内部に存在するオブジェクトに対する変更がコミットされるときには集約全体の不変条件がすべて満たされていなければならない
競合頻度や変更の反映速度に応じて境界を設定することで不必要なロックを避けることができる
ファクトリ
オブジェクトの生成はそれ自体が主要な操作になりうるが、複雑な組み立て操作は生成されるオブジェクトの責務には合わない
複雑なオブジェクトの生成はドメイン層の責務であるが、その仕事はモデルを表現するオブジェクトのものではない
ドメインぽくないけどドメイン層として定義されるものがあってややこしいな(サービスとかファクトリとか)
集約全体をひとまとまりとして生成し、その不変条件を強制すること
ファクトリの要件
- 生成メソッドはそれぞれアトミックであり、生成数ロブジェクトや集約の不変条件をすべて強制する
- ファクトリは生成される具象クラスではなく、要求される型に応じて抽象化しなければならない(?)
集約のルートにファクトリメソッドを生成することもできる
他のオブジェクトの生成に密接に関わるオブジェクトにファクトリメソッドを置くこともできる
生成したあとはそのオブジェクトが生成物を所有するわけではない
=> カートがOrderをつくるみたいなやつ?
自然な置き場所がない場合は専用のファクトリオブジェクトかサービスを作成しなければならない
ただし、アクセスを集約の内部に制限するルールを尊重し、集約の外部からは生成物を一時的にしか参照しないようにすること(?)
ファクトリオブジェクトより公開コンストラクタのほうが良い場合(?)
- クラスが型である。階層の一部ではなくインターフェースを実装することで多態的に使用されることもない
- クライアントが実装に関心がある。ストラテジーを選択する方法としてなど
- オブジェクトの持つ属性をすべてクライアントが取得できるため、クライアントが見えにするコンストラクタの内部にさらに別のオブジェクトの生成がネストされることがない
- 構築が複雑でない
- 公開コンストラクタはファクトリと同じルール(不変条件を満たすアトミックな操作であること)に従わなければならない
インターフェースを設計する
念頭におくべきこと
- 各操作はアトミックでなければならない
- ファクトリはその引数と結合する: 入力パラメータを慎重に選択しないと複雑な依存関係を作り出してしまう
最も安全なパラメータは下位の設計層に由来するもの(?)
他適切なものとして、モデルにおいて生成物と密接に関連するオブジェクト
ファクトリでは引数として渡されたものの具象クラスではなく、その抽象型を使用すること
不変条件のロジックはどこに置くべきか
ファクトリは不変条件の検証を生成物に委譲することができ、それが最善であることも多い
集約のルールに対して、不変条件をファクトリに入れ生成物をスッキリさせるほうがよい場合もある
逆にファクトリメソッドが他のドメインオブジェクトに付加されている場合ではやめたほうがいい
不変条件は各操作の終わりに適用されるのが原則
値オブジェクトのような不変なものであればライフサイクル中に再適用されることはないのだから不変条件をオブジェクトが持っている必要はなく、ファクトリに置けば良い
エンティティファクトリvs値オブジェクトファクトリ
値オブジェクトファクトリは常に完全な生成物を供給する
エンティティファクトリは、詳細を後から追加すれば良いので有効な集約を生成するのに必要な本質的な属性だけを受け取る傾向がある
格納したオブジェクトを再構成する
再構成に仕様されるファクトリは生成に使用されるものと以下の点で違う
- 新しい追跡IDを割り当てることがない
-
不変条件の違反を違うかたちで制御する
=> すでに存在しているオブジェクトが不変条件に違反していてもそれを認めなければならない
リポジトリ
オブジェクトを用いて何かを行うには、まずそのオブジェクトの参照を得る必要がある。
参照を得る方法には、生成と関連を辿る、があるがそれだけでは関係性のもつれを生むことになる
RDBは第三の方法としてグローバルなアクセスを提供する。検索の疎結合と関連の高凝集の間でトレードオフを図ることになる
技術的な観点ではオブジェクトを再構成することは、生成することのサブセットだが、ライフサイクルにおける中期であることを区別したい
クライアントはすでに存在するドメインオブジェクトへの参照を手に入れる実用的な手段を求めていて、インフラストラクチャによってそれが簡単にできるようになると
たどりやすくするためだけに関連を追加したり、クエリを使用して直接データだけを引き出したり、集約のルートから進む代わりに数個の特定オブジェクトを取り出したりするかもしれない。
そうなるとドメインロジックがクエリとクライアントコードに移され、エンティティと値オブジェクトはただのデータコンテナになってしまう
=> だからリポジトリを作って、それを概念上の集合として扱い、ルートに対するアクセスを提供することでDBアクセスの濫用を防ぐ?
集約内にあるどのオブジェクトもルートから辿る以外の方法は禁止されている
永続化された値オブジェクトを見つけるには集約のルートであるエンティティから辿るのが普通だが、元の状態のままDBから取り出されることもある
列挙の場合も直接取り出される可能性がある
もしすでにある値オブジェクトをDBから検索する必要が生じたなら、実はエンティティではないか再検討する
リポジトリは特定の方のオブジェクトをすべて概念上の集合として表現する
コレクションのように動作するが、より手の込んだクエリ機能を持っている
クライアントがリポジトリにオブジェクトを要求するときはクエリメソッドを使用する
クエリメソッドはクライアントが指定した条件を満たすオブジェクトを返すほか、集計結果や集計演算の結果を戻すことができる
グローバルアクセスを必要とするオブジェクトの各型について、あるオブジェクトを生成し、その型の全てのオブジェクトで構成されるコレクションがメモリ上にあると錯覚させることができるようにすること
オブジェクトの追加と削除を行うメソッドを提供し、データストアにおける実際のデータ挿入や削除をカプセル化すること
ある条件に基づいてオブジェクトを選択し一致するような、完全にインスタンス化されたオブジェクトのコレクションを戻すメソッドを提供すること
それによって問い合わせの技術をカプセル化すること
集約のルートにのみリポジトリを提供すること
クライアントをモデルに集中させ、おあらゆるオブジェクトの格納とアクセスをリポジトリに委譲すること
仕様パターンに基づいたクエリの仕様(?)
QueryオブジェクトみたいなCriteriaを使う感じ?
クライアントのコードはリポジトリの実装を無視するが、開発者はそうでない
パフォーマンスなどのために、開発者はリポジトリの中で何が起こっているのかを把握していなければならない
リポジトリを実装する
- 型を抽象化すること: リポジトリは特定の方のインスタンスをすべて含んでいるが、それは各クラスにリポジトリが1つ必要というわけではなく抽象基底クラスのリポジトリだけでも構わない
- クライアントから切り離す利点を活かす: テストのためにインメモリにしたり、パフォーマンスのためにキャッシュしたり永続化の戦略を切り替えられることを活かす
- トランザクション制御をクライアントに委ねること: リポジトリはDBへの削除・挿入を行うが、クライアント側のUoWのコミットに任せる
フレームワークの範囲内で作業する
使っているフレームワークと争わないこと
=> ここだ、俺はRailsと仲良くしたいんだよな
フレームワークと対立してしまったときはドメイン駆動設計の基本を保ちながら詳細は捨て去る方法を模索すること
ドメイン駆動設計の概念と、フレームワークの概念に似通った部分がないか探すこと
ファクトリとの関係
ファクトリとリポジトリには別々の責務がある。ファクトリは新しいオブジェクトを生成し、リポジトリは古いオブジェクトを見つけ出す
リポジトリがオブジェクトの生成をファクトリへ委譲すればよく、そのファクトリはオブジェクトを1から生成するのにも使うことができる
ファクトリとリポジトリを組み合わせたいとb思わせるものの一つにfind_or_initializeがあるが、こういうのは避けなければならない
実際には状況を混乱させてしまうため
RDBに合わせてオブジェクトを設計する
分析モデルと設計モデルが分離するのを避けるというモデル駆動設計の考え方から、テーブルとオブジェクトの対応関係を複雑にしてはならないということになる
オブジェクトの関係性の省略や、選択的な非正規化によって両者を近づける必要がある
また、マッピングツールがある場合でもマッピングを透過的にし、コードを調べたりマッピングツールの説明を読んで簡単に理解できるようにしなければならない
不変条件が破られてしまう可能性があるためオブジェクトシステムの外部にあるプロセスから、オブジェクトの格納先にアクセスしてはいけない
データがレガシー・外部システムからくる場合は、実のところ同一のシステムに2つのドメインモデルが存在しているといえる
貨物輸送システムを導入する
- 顧客貨物に対する主要な荷役(積み下ろし)を追跡する
- あらかじめ貨物を予約する
- 貨物が荷役の過程で所定の場所に到達した際に、自動的に請求書を顧客に送付する
- 荷役イベント: 貨物に対して行われる個別のアクション(積み下ろし、通関手続きなど)
- 配送仕様: 配送の目標。荷出し地と到着日時を含んでいる。仕様パターン(述語的なboolを返すクラス)で実装される。貨物と1:1
- 役割: 貨物において顧客が果たす要素
- 輸送機器移動: 特定の輸送機器(トラック、船など)によって実現される、ある位置から他の位置への移動
- 配送記録: 実際に貨物に起ったこと
アプリケーション層の導入
ドメイン層を区切るために下記のアプリケーション層を定義する
- 追跡問い合わせ
- 新規の貨物を登録できるようにして、それに対するシステムの準備を整える予約アプリケーション
- 貨物に対して行われた各荷役を記録できるイベント記録アプリケーション(追跡問い合わせで検索される除法を提供する)
これらのアプリケーションクラスはCoordinatorであり、自分で質問に答えてはならない
エンティティと値オブジェクトを区別する
DDDはアプリケーション層の導入で、イベントモデリング的なことはあんまりしないのかなと思ってたけど、荷役イベントっていうのがあるぐらいだからそうでもないっぽい
エンティティ: 顧客、貨物、荷役イベント、輸送機器移動、位置、配送記録
値オブジェクト: 配送仕様、役割、タイムスタンプや名前といった属性
配送記録は交換することができないからエンティティだが、配送記録には貨物と1:1の関係があるので、実は独自の同一性をもっていない
同一性は所有者である貨物から借用する
関連を設計する
顧客は貨物と関連しているが、顧客の責務を限定するために貨物から顧客への参照にしておいたほうがよい。顧客から貨物を検索する場合はリポジトリを使用する
貨物は配送記録について知っており、配送記録は一連の荷役イベントを持っている。
そして、荷役イベントは貨物を取り扱っている。ここに循環参照がある
配送記録がもつ荷役イベントの参照をリストからデータベース検索に変えるなど、トレードオフを考慮した設計を行う
集約の境界
顧客、位置、輸送機器移動は独自の同一性を持ち、多くの貨物に共有されているので、それぞれ独自に集約を持ちそのルートにならなければならない
貨物も集約ルートだが、境界の決め方には検討が必要: 特定の貨物がなければ存在しないもの(配送記録・配送仕様・役割・荷役イベント)を集められる
ただし、荷役イベントは特定の輸送機器移動に向けて荷積み準備を行う全ての業務のにも使用され、貨物と切り離して考えることができるから独自の集約ルートであるべきだ
=>境界は単に参照や所有によって決められるわけではなく、アプリケーションの要求(?)に影響を受けるということか
集約の境界をあたらしく引き直したり、あとから集約のルートにするのはいいのかな?
リポジトリを選択する
リポジトリを持ちうるのは集約のルートだけだが、実際に実装するにあたってはアプリケーションの要求を考慮しなければならない
シナリオをウォークスルーする
貨物から新しい予約を作るとき、集約の境界内にあるものはすべてコピーし修正したが、集約の境界外の物に対しては全く影響を及ぼさない
リファクタリングのために立ち止まる
荷役イベントを追加するときに配送記録を更新しなければならないことから、貨物集約がそのトランザクションに巻き込まれる
配送記録が持つ荷役イベントのコレクションをクエリで置き換えることで、荷役イベントをルートとした集約の外部に整合性の問題を起こさないようにできる
貨物から荷役イベントを検索するリポジトリを追加する
配送記録自体を永続化する必要がなくなる(貨物IDから荷役イベントを引っ張って、都度配送記録を生成すれば良い)
輸送モデルにおけるモジュール
インフラストラクチャ駆動のパッケージング(エンティティ・値オブジェクト・サービスなどを別々のディレクトリに配置する)だと、ドメインについての知識を伝えられない
顧客、輸送、請求のようなドメイン概念に基づいたモジュールのほうが他の人々に伝わりやすい
ディレクトリも分けたくなっちゃうよなあ
2つのシステムを接続する
外部サービスへのアクセスを担当するクラスをxxインターフェースと名付けるのではなく、利用側のドメインモデルの観点から必要な機能のみを抽象化し、実装する
x 販売管理インターフェース o 配分チェックサービス
モデルを強化する: ビジネスのセグメント化
エンタープライズセグメント: ビジネスの分割方法を定義した次元軸の集まり
エンタープライズセグメントと呼ばれるクラスが、これまで考えられてきたドメインモデルと設計に値オブジェクトとして追加され、各貨物に設定される
他の分野のビジネスにおける概念をオブジェクト化して、実装対象のドメインモデルに組み込んで利用する。そこにまとめておくことで影響が他に広がらないようにする、みたいなことかな?
もしかしてこのエンタープライズセグメントって別にパターンの名前とかじゃなくて貨物の割当空間みたいなドメイン固有のやつか?
かつ、特定期間、場所の割当空間っぽいのでエンタープライズセグメント値オブジェクトはEnterpriseSegmentIdを持って、貨物レコードからbelongs_toの関係になるような感じ?
エンタープライズセグメントパターンっていう記述が出てきたので、結局はパターンではあるのか?
アナリシスパターンを読まないとわからないかも
=> 腐敗防止層とかと並列なのでパターンで確定ぽい
エンタープライズセグメントを処理するオブジェクトはそのセグメントのルールを知っているオブジェクトに課すのが適切
予約アプリケーションではなく、配分チェックサービスがセグメントを取得する。(貨物が直接セグメントを取得することも避ける)
=> ウチの例でいうとGameとSteamGameみたいなもんだよな。GameがSteamGameを取りに行く、みたいな書き方してもいいけど、取り方って一個ずつじゃなくて複数いっぺんに取れる場合もあるわけだし、サービスに任せたほうがいい
より深い洞察へ向かうリファクタリング
リファクタリングを行うイテレーティブなプロセスの中でドメインエキスパートと開発者が協力することで、ソフトウェアがよりドメインを体現するようになる
リファクタリングのレベル
ユニットテストを揃え、コードに関する実験を比較的安全に行える状態にすることで、開発者はずっと先まで見通さなくて済む
リファクタリングでは技術的な詳細や、パターンについて解説されがちだが、システムの活力に最も大きく影響するのはドメインについての新たな洞察によって動機づけられたものや
コードを通してモデルの表現を明確にするもの
深いモデル
深いモデルはドメインエキスパートの主要な関心事とそれに最も深く関連した知識に関する明快な表現を提供するが、一方でドメインの表面的な側面は捨て去る
輸送アプリケーションを作るにあたって、船舶・コンテナのような単に物理的なものを模したオブジェクトを定義するのではなく、船荷証券のような一見すると明らかでないオブジェクトが前面に来る
関係ないけどサービスに関する話がちょっと分かってきたので調べたら同じことを考えている人がいた
Railsでサービスクラスを書く時に知っておきたいこと #Rails - Qiita https://qiita.com/tatsumi_t2/items/dfd29454cae0463eb5c2
Railsだとドメインとインフラ層がくっついてるので、Serviceを作ろうとすると必然的にServiceも層の区別がつかなくなるんだよな
特にApplicationService, DomainService, InfrastructureServiceって名前なしに単にConfirmOrderServiceみたいなのができるとやばい
なので、ApplicationService => UseCase, DomainService => EventModelingとかで対応, InfrastructureService => HogeClientとかで対応?(プッシュ通知が必要なとき、PushService.push_user_notification(user_notification)みたいな形で書いてコントローラに置くのもありだよな。ActionMailerだったらどう書くか、みたいなのを考えてもいいのかも)
が良いのじゃないのかなぁ
Railsは確かにActiveRecordでドメインとインフラが癒着してはいるんだけど、ActionMailerとかによってメールインフラとかは分かれてるわけだからActiveRecordのインフラの癒着もRDBとだけにしておいたほうが良い、というのはあるかもしれないなぁ。書いてて思ったけどuseCaseとかって名前にするんじゃなくて、ApplicationService,DomainServiceとかっていうディレクトリ切ったほうがいい気がするね。省略してなんでもServiceって読んでるのが混乱の原因なのであって、省略しなければみんな納得できるんじゃないの?
ブレイクスルー
リファクタリングで得られる効果は普段は労力に正比例するものではないが、あるとき深い洞察によってブレイクスルーを迎えることがある
商業銀行ドメインにおけるファシリティとは融資する側の企業による確約(クレジットカードの与信枠など)
技術側にとって都合がいいものの、ドメインエキスパートが理解できない概念は結局は不要で、ドメインに関する不完全な理解によって生まれていたものだった
ローンシンジケートとは複数の融資会社が1つの融資案件に対して、シンジケート団を組成し一つの融資契約に基づいて同一条件で融資を行う資金調達方法
ローンの支払や手数料などは各企業のシェアパイの割合と種類によって分割される、というのが本質であることがわかった
確かにコードを書いていてここってこういうことじゃん!みたいなブレイクスルーはあって、その気持ちよさが設計を学ばせる動機になっていると思う
また、DDDのいうような技術的なものから独立したビジネスロジック、みたいなのって本当に存在するの?みたいな意見にも納得感があって、
DXのように現実にある業務を置き換えるのであればうまくいくこともあると思う一方で、ドメインが技術と密接に結びついているようなサービスでは難しいこともある気はする(いやそれでもやりようはある気がするから不安感みたいなのが正しいのかもしれない。なんか感覚的にはこれは科学じゃなくて錬金術なんじゃなかろうかと疑う気持ちに似てる)
帳票のような概念をコードに落とし込む、といってもその帳票もなんらかシステムの形ではあるよね、という感じ(大量生産できるフォーマットに基づいているのだからシステム化されている)
システム化できる形に業務が整えられているわけだから…いやまぁRDBとかは現実世界にはないわけだからいいのか?
あとはDDDをやる!というときにシステムが密林化する、というのはまぁ発言者の技量を低く見積もるのであればこの本に書かれている技術的な設計をそのまま使用することで、本来は不要であったレイヤの分離が起こってしまい、オブジェクト量が増えてしまう、ということなのかなぁ。
DDDが必要になるほどドメインが複雑であるときに、新しくジョインしたひとがコードを見て複雑だ!というパターンも有り得そうで、それはちょっと可哀想かなと思う。それを書いた人たちが密林だと感じているかどうかをヒアリングしたほうが良いとは思うなぁ
あとオブジェクト指向ってビジネスロジックの記述にいらなくね?みたいな話もあり、結局状態遷移を管理しなければいけないのはIOなんだから、その中身のコードはステートレスな関数型で書いたほうがいいでしょという話は納得できる。
ただ、その場合でもモデリングというのは必要だと思うし、関数型で書く人がどれぐらいそれを軽視しないでいられるか、というのは不安な部分でもある(関数型に行く人がオブジェクトモデルも大事だよね、みたいなマインドを維持しているのか、という不安)
やれる機会があるのであれば、関数型でやってみたいものだが(関数型DDDという本もあったはず)
https://www.amazon.co.jp/-/en/Scott-Wlaschin/dp/1680502549
Domain Modeling Made Functionalの読書メモ https://zenn.dev/labbase/articles/b1c513c32fe15e
DDD ではデータベース内のデータ表現を気にすることなく、ドメインをモデリングすることに集中するべきだということですね。
ここまでは書いてなくない?DBとオブジェクトの乖離は少ないほうがいいって書いてあったし、お互いの制約によって妥協するべき部分もある、って書いてあった気がする
サービスに代表されるように、同じ名前でも思想や出典によってちがう意味合いを持つパターンがあるので、このプロジェクトにおけるサービスとはこういうものですよ、みたいなドキュメントを整備することが大事だと思った
あとDDDを呼んだ感想を発表とかするときって、自分の持ってる前提条件を含めて発表するのって結構難しいよなぁと思った
たとえばRailsのなかでServiceをどう扱うべきか、みたいな話をするときにレイヤの話は当然するとして、エンティティとかの話をどこまでするか、とか、そもそも本の中身すべてを伝えることはできないので、今実際使える知識だけを伝達しておいて、直感的な反論がなければ従ってもらい、根拠が知りたければ該当の部分を読ませる、ぐらいしかできることってないよなぁと思った
インフラも考え方とか根底に流れる思想を伝えることはできるけど、S3のポリシー設定とかまで教えてられないというか…
集約ってめちゃ難しくないか、と思う。単にオブジェクト間の関連だけでなくアプリケーションの要求にも影響を受けるので…リポジトリを作っていいのは集約のルートだけ、というのもまぁわかるんだけどあんまりうまくやれる自信がないなぁ。Railsでどこからでもオブジェクトとれる、みたいなことをやってるからなんだけど。PostLike.create!(post: post, user: user)じゃなくてpost.likes.create!(user)から始まる感じなのかなぁ。post.like!(user)までいくのがいいんだろうけど。postがどこまで大きくなるかって感じだよね。将来postにいいねできるのはuserだけじゃない、ってなったときにどうあるべきかって感じもあるな
dependent: :destroyから集約を考えるみたいなのもありそうではあるね。
あとはスコープ付きのユニーク制約みたいなのをどこに書くべきか、みたいなのある。集約に対する不変条件なわけだからルートにあったほうが自然だとは思うけど、現実問題書いてられないよねっていう。
ルートエンティティだからといってすべてを知らないといけないというわけではない気がするなぁ
Railsではどこからでも取れるんだから集約を考えるメリットが薄いというのはあるかも
Railsって密結合なんだけどそのおかげで設計に対する関心が高まっているのを感じるので感謝がある
暗黙的な概念を明示的にする
ドメインエキスパートの使う単語に注意し、複雑なものを簡潔に述べている用語がないか、言葉の選び方を正されていないかなどをモデル発見の手がかりにする
単に名詞を探してオブジェクトにする、というのとは違う作業
エキスパートもしくは開発者が設計の中にない用語を使用するのは警告である
例 輸送モデルに欠けている概念を聞き分ける
船舶による貨物輸送を予約するアプリケーション
貨物は荷受け地、荷出し地を持ち、経路選択サービスによって航路が設定され、各工程での荷積み位置、荷下ろし位置、運行IDなどが貨物予約のレコードとして保存される
っていうのを考えてた。各貨物予約の日付は運行から取得することができる、という話だったが、途中で輸送日程そのものが必要とされているものだったことに気づく
貨物が直接各貨物予約レコードを持つのではなく、0..1の輸送日程を持ち、輸送日程が行程のコレクションを持つ構造にした
なんかこの例の話し合い、あんまりうまくいってる風には見えないけどな…
荷積み時刻、荷下ろし時刻は運行IDから取れるようにしておいたほうがよくない?そうなってはいるのか
どちらかというと単に正規化した貨物予約のみを持つんじゃなくて、間に輸送日程と呼ばれるモデルを用意することで概念が整理される、みたいな話かも
輸送日程を明示するメリット
予約帳票と輸送業務支援アプリケーションの両方に対して輸送日程が使えるようになる
帳票からドメインロジックを移すことができる
例 利息を得る 難しい方法
夜間バッチスクリプトが毎日、資産に対して収益総額を計算させ、その金額と元帳の名前を会計プログラムに渡して記帳させていた
また、手数料に対しても同じように資産に命じ、会計プログラムに記帳させた
資産から利息計算サービス、手数料計算サービスを取り出した状態からスタート
得られた利息と支払いは全く別の記帳になるので、利息から支払いを分離した。(未払額などの項目を消した)
基本的にどんな資産も、それを価値あるものにしているのは利息や手数料などのかたちで発生させることができる何かである
結局のところ資産から何か(手数料・利益)が発生する、というのが根本にあるということがわかった
手数料や利益を発生させる、発生スケジュール群を資産が持っている、という形にすることで
- その発生がどこの元帳に記されるべきか、という知識がスクリプトからドメイン層に移された
- 手数料と利息の新しいバリエーションを追加するときには発生スケジュールを追加すればよくなった
文献を読む
モデルの概念を探すときには自明のことを見過ごしてはならない
文献があるのであれば、それでも引き続きドメインエキスパートと協業しなければならないが、それを読んで一貫した見方から始めることができるかもしれない
それほど明白でない概念をモデル化する方法
「名詞と動詞」以外で重要な概念のカテゴリもモデル化することができ、1つは制約である
制約を実装するオブジェクトの設計が制約によって歪められていることを示す警告
- 制約を評価するために、本来であればオブジェクトの定義に合わないデータが必要になっている
- 関連するルールが複数のオブジェクトに存在し、重複が発生したり、不必要な継承が行われている
- 設計や要求に関数する多くの会話が制約を使って行われるが、実装では制約が手続き型のコードの中に隠されてしまっている
ドメインオブジェクトとしてのプロセス
プロシージャをモデルの主要な側面にしたくない
サービスはプロセスを明示的に表現しつつ、オブジェクトにカプセル化する方法の1つ
モデルに明示すべきプロセスと隠蔽するプロセスを区別する鍵はドメインエキスパートとの会話に出てくるかどうか
制約とプロセスはオブジェクト指向でプログラミングしているときにすぐには思い浮かばない2大概念
仕様(SPECIFICATION)
あらゆる種類のアプリケーションにおいてブール値による評価を行うメソッドは実際は小さいルールの一部であるように見える
例えば請求書が延滞しているかを判定するとき、請求書の期限から始まるだろうが、顧客の口座情報によっても結果が変わるかもしれない。延滞請求書は2回目の通知を準備するものもあれば、回収機関への連絡を準備するかもしれない。これによって請求書の本質的な部分のコードが埋没してしまう
この時点で請求書クラスを守るため、開発者はルール評価コードをリファクタリングしてアプリケーション層にいれることが多いが、それではドメインからルールが分離されデータコンテナとなったオブジェクトが残るだけになってしまう
そう考えるとControllerで使ってるPolicyってドメイン層においておくのがよいのか?
うーん、Userのアクセス権コントロールの話だからApplication層にあるのは自然な気もする、Domain層にもSpecificationという形で用意するのがいいのかな?
ビジネスルールは明らかなエンティティや値オブジェクトの責務のどれとも合致しないことがしばしばあり、その多様性によってドメインオブジェクトの基本的な意味を圧倒しかねないが、ルールをドメイン層から取り出してしまうのはより悪い
手に負えなくなった評価メソッドは、うまく拡張して独立オブジェクトにする、これが仕様である。
特殊な目的を持った術後的な値オブジェクトを明示的に作成すること。仕様とはあるオブジェクトが
ファクトリは仕様を構成するうえで、他の情報源からの情報を使用できる
これらの情報源に請求書から直接アクセスできるようにしてしまうと、本来のオブジェクトの責務とは無関係な方法でオブジェクト同士の結合を強める形になってしまう
仕様 = 延滞請求書仕様.new(Date.current)
仕様.is_satisfied_by?(inv123)
ドメインモデルの中では判定せず、ドメインサービス内で判定に使うということなのかな?
メソッドの中でガードを書くべきか、呼び出し側で保証するべきか、みたいなの毎回迷うので
仕様の適用と実装
仕様の価値の多くは全く異なるように見えるアプリケーションの機能を統一することにある
その機能とは以下の3つの目的がある
- 検証: オブジェクトを検証して、何らかの要求を満たしているか、目的のための用意ができているかを調べる
- 選択: コレクションからオブジェクトを選択する、またはDBに問い合わせる
- 要求に応じた構築: 何らかの要求に適合する新しいオブジェクトの生成を定義する
選択(問い合わせ)
- selectのようなメソッドに仕様を渡してメモリ上のコレクションから取得する
- 仕様を満たすクエリをasSQLのような形でカプセル化する
=> Criteriaみたいなのをつかって検証とクエリを一致させるのかな?と思ったけど、結局条件が同じところに書かれている、っていうのは変わらないわけだから仕様に直接書いてもいいのか
テーブルの詳細構造がドメインに流れ出しているのは良くないが、実際はORマッパーによってモデルオブジェクト属性として表現する形を提供してくれているものがあるので、それを使うのが良い
インフラの助けが得られず、SQLを直書きしなければならない場合はドメインオブジェクトからSQLを取り除くために
仕様からリポジトリを使用する形をとりつつ、リポジトリ側からもselectでSpecificationを受け取るみたいなことをやっていて、双方向依存というか、クライアントがどっちから使うか悩まないか?と思う
要求に応じた構築
何かを要求するときは仕様に合った形の成果物を期待するはずで、生成は検証と選択に適用したのと同じ仕様の概念である
仕様を用いなくても、ジェネレータを書いて、そこに必要なオブジェクトを生成する手続きか命令一式を与えることができる
その場合、コードはジェネレータの振る舞いを暗黙的に定義することになる
その代わり、ジェネレータのインターフェースを記述的な仕様のかたちで定義すれば、ジェネレータの生成物を明示的に制約できる。このアプローチには下記の利点がある
- ジェネレータの実装がインタフェースから分離される。仕様は出力に対する要求を宣言するが、どうやってその結果を出力するかについては定義しない
- このインタフェースは明示的にルールを伝えるので、開発者は操作の詳細をすべて理解していなくても、ジェネレータに何を期待すべきかがわかる
- インタフェースはより柔軟になるか、さらなる柔軟性によって機能が強化される。要求を提示するのはクライアントの役目であり、ジェネレータは仕様の文面を満たしさえすればよいからである
- この種のインタフェースのほうがテストしやすい。モデルにはジェネレータの入力を明示的に行う方法が含まれており、これが同時に出力に対する検証にもなるからだ
全く意味がわからん。
どういう使い方をするのかがわかってない状態なのでサンプルコードがほしい。一旦読み進めてみるか…
続きのサンプルコードを読んだ感想
- 2段階の設計
格納サービスが手続き的にならないようにするために、ドメインモデル自体に仕様を検証するメソッドを定義する
格納サービスのプロトタイプを作り、他の開発が進めるようにする
格納サービス = 仕様に基づいた生成を行うジェネレータってことだよな
サンプルコードの例だと、ジェネレータは自分で仕様を満たすコンテナを作るわけじゃなくて、仕様オブジェクトを使った選択を行っているだけ。格納サービスの中に手続き的に仕様に関するコードを入れるなってことなのかな?
なんかそうっぽいな。
DDD - 仕様パターンの紹介
https://bamboo-yujiro.hatenablog.com/entry/2020/03/21/215005
上記はDDD 的には正しいが、アプリケーション上、大きな問題を孕んでいる。
別にEvansこれが正しいと言ってなくない?
フィルタリングで仕様パターンを使うのを諦め、リポジトリに条件をベタ書きする。(ドメイン知識がリポジトリににじみ出る)
SQL を遅延実行させるようにする。
これもそんなこと全く書いてないし…
まず遅延実行に関しては言及がない上、別に遅延実行でなんか変わるわけじゃない(結局コード例だとisSatisfiedでアクセスしているので、どこで実行しようが取得クエリ数は同じ)
リポジトリに条件をベタ書きするのも、べつにそこまで推奨してるわけじゃない。まずORマッパーのような抽象化したデータアクセスがあるんだったら、仕様にそれを書くことを勧めているし、仕様オブジェクトに知識がまとまるようになるので自分もそれがいいと思う。もしそれでもだめなときはダブルディスパッチを使って仕様からリポジトリを引けって言ってるので、まぁそれも大して良いとは思わないけど、
ドメイン駆動設計(DDD)を整理 https://zenn.dev/sutamac/articles/7e864fb9e30d70
こういうのも値オブジェクトの説明がPoEAAの説明なんだよな。
良いか悪いかは別として原典を読むことの大事さを感じるし、読んだとしても間違って解釈している可能性がある
てか別にそこって本質じゃないんだよな
DDDの仕様パターン - pospomeのプログラミング日記 https://www.pospome.work/entry/20151010/1444497269
この記事も仕様パターンで探すときはパフォーマンスが確保できない、って書いててまぁコード上の1箇所に判定が集中してないと意味ないじゃんっていう気持ちはわかるんだけど、別にそこは本質じゃなくて仕様をモデル化することと、それがオブジェクトとしてまとまってることが大事なのであってクエリとコード上の判定を1つの条件文でやれって言ってるわけじゃないと思うんだよな
生成もなんかRouteSpecificationからorigin取ったりしてるけどそんなん全く書いてないけどどこから出てきた発想なんだろう?
てか本のサンプルも生成やってなくてやってるのは結局選択なんだよな。そこが欠けてるからこういうよくわからんのが出てくる
翻って自分だったら仕様からの生成をどう実装するか、なんだよな
1つはファクトリの機能を仕様にもたせてしまうこと、そうすればロジックが集約される
ほかはファクトリで普通に作って、最後に仕様で確認すること。Assertionとかに近いけどこれも悪くないと思う
クライアントの気持ちになって考えると、仕様を満たすオブジェクトを生成したい、となったらファクトリに仕様を渡すのがいいな。あぁそうするとRouteSpecificationからorigin取るみたいな話になるのか、なるほどね。
うーん、その場合仕様から値を取ってくるのがどれくらい許されるか、みたいな話だよね。
まぁたしかにContainerSpecificationがあって、そこからContainerの種類をとってくるって話になったらspec.containerFeatureってして取ってくるか…てかそれしかないか。
specification側に諸々セットするメソッドをつけるのもなんか違う気はするか?例えばspecificationが属性しかとらないようなやつだったらそうかも。でもその場合でもspecification側に構築のメソッドがあったほうが気持ち的にはいい気がするんだよな
しなやかな設計
抽象化や間接化のためにレイヤを作りすぎてしまうと邪魔になることのほうが多い
設計は、それを使うクライアント利用者と設計そのものを変更しようとする開発者両方に役立つものでなければならない
意図の明白なインターフェース
オブジェクトのインターフェースを見ても、クライアント開発者がオブジェクトを有効に使用するうえで何を知っているべきかがわからなければ、詳細をなんとか理解するためにコードの内部を掘り下げなければいけなくなる
ある開発者があるコンポーネントを使用するために、その実践についてじっくり考えなければならないのであれば、カプセル化の意味は失われる
クラスと操作にはその効果と目的を記述する名前をつけ、約束したことを実行する手段については言及しない
こうした名前はユビキタス言語に基づいていなければならない
手段ではなく目的の観点から語らせる
カプセル化を行い、開発者の認知負荷を下げユビキタス言語でコードを読み解くことができるようにするのが意図の明白なインターフェース
メソッドを使用した結果を予測可能なものにするのが、複雑なロジックにおける副作用のない関数とシステムの状態を変更するメソッドにおける表明である
副作用のない関数
操作は大きく2つのカテゴリにわけられる=>コマンドとクエリ
クエリ:システムから情報を取得する
コマンド:システムに何らかの変更を与える
複数のルール間に存在する相互作用や、演算の合成は予測するのが極端に困難になる
開発者は結果を予測するためにインターフェースによって隠されたものを見る必要が生じ、抽象化の効用が薄れてしまう
安全に予測できるように抽象化されていないと、開発者組み合わせの爆発を制限しなければならなくなり、現実に構築できるふるまいの豊かさが低く制限されてしまう
副作用を起こさずに結果を戻す操作を関数と呼ぶ
コマンドの影響を抑える方法2つ
-
コマンドとクエリを別々の操作に厳密に分離しておく。変更を引き起こすメソッドに関してドメインデータを戻さないようにすること。可能な限りシンプルに保つことクエリと演算はすべて副作用を起こさないメソッドで実行すること
-
今ある設計に代わる、既存のオブジェクトを一切変更しないようなモデルや設計が大抵の場合あるはず
オブジェクトを変更する代わりに新たな値オブジェクトを生成して返す。値オブジェクトは不変のためイニシャライザ以外はすべての操作が関数である。
変更と混在している操作はリファクタリングして2つの別々の操作にするべき
副作用を分離して単純なメソッドにする方法はエンティティにしか適用されない
=> 値オブジェクトとエンティティを分ける意味みたいなのがここにもある
プログラムロジックのうちできる限り多くの部分を関数に置くこと
コマンドは厳密に分離して、ドメインの情報を戻さない非常に単純な操作にすること
ある概念が値オブジェクトの担う責務に合致スル場合は複雑なロジックをその値オブジェクトに移す事によって副作用をさらに制限すること
表明
操作の副作用が実装によって暗黙的にしか定義されていない場合、委譲が多く行われている設計では原因と結果がもつれてしまい、カプセル化の価値が失われる
事後条件: 操作の副作用
事前条件: 事後条件が成り立つことを保証スルために満たさなければならない条件
不変条件: あらゆる操作が終わったときのオブジェクトの状態に関する表明。集約全体に対しても宣言できる
表明は手続きではなく状態である
操作の事後条件とクラス及び集約の不変条件を宣言すること、
プログラミング言語で表明を直接コーディングできない場合は、そのかわり自動化されたユニットテストを書くこと
そう考えると、コードを書いている最中にテストも書くという作業は自然に思える
引数を変更することは、副作用の中でもとりわけリスクが高い
概念の輪郭
ドメインの根底には一貫性があるので、ドメインの一部と共鳴するモデルが見つかったら、そのモデルは後から発見される部分と矛盾しない可能性が高い
新しい発見にモデルを適合させるのが難しいときは、より深い洞察に向けたリファクタリングを行い、次の発見とは適合することを期待する
新しく理解された概念や要求にコードが結合するにつれ、概念の輪郭が姿を表す
各オブジェクトは単一の完結した概念、つまり完結した値(?)でなければならない
目標はシンプルなインターフェースの集まりを作ること
独立したクラス
モジュールも集約も相互依存関係の網の目を制限するためのもの
1つのモジュールの中で合っても依存関係が加わるにつれて設計の解釈が難しくなる
暗黙的な概念は明示的な参照よりもその負荷を大きくする
精緻なモデルはそこに含まれる概念間のつながりが、それらの概念の意味にとって根本的なものを表現するもの以外は残らなくなるまで蒸留される。重要なサブセットにおいては依存関係の数をゼロにできる
あらゆる依存関係はオブジェクトの背後にある概念にとって本質的であると証明されるまでは疑われるべきだ
閉じた操作
戻り値の型が引数の型と同じにできる場合はそのように操作を定義すること
実装クラスが処理で使われる状態を持っていれば、そのクラスは事実上操作の引数となるから、引数と戻り値は実装クラスと同じ型でなければならない(?)
あるクラスのメソッドの中でそのクラスのプロパティを使っているということは、操作の引数にそのクラスをとっていることと同じであるから、そうであるなら戻り値をそのクラスの型にしなければならないみたいな話?
このパターンが最も頻繁に適用されるのは、値オブジェクトの操作である
一般にエンティティは処理の結果となるような類の概念ではない
引数が実装側と一致するが戻り値の型が違う、戻り値の型がレシーバと一致するのに引数が違う、みたいな操作は閉じてはいないが、閉じた操作の強みを多少持っている
また、その型以外の余分な型がプリミティブや基本ライブラリクラスだけなら閉じた操作と同程度まで精神的な負荷を下げてくれる
コレクションをイテレータで詰め直すより、selectで直接コレクションにしたほうがわかりやすいよね、みたいな話
宣言的な設計
ルールベースの言語や宣言型のプログラミングスタイルを使うことでモデルの特徴を文字通りの意味で解釈できるようになるが、フレームワークの制約によってモデルの概念が削ぎ落とされたり、ルールベースでも制御述語によって
迂回されたりする
ORマッピングや永続化の分野では開発者を単純作業から開放する一方で、設計する際の完全な自由を残している
ドメイン特化言語
DSLを使うことでユビキタス言語との結びつきを強くすることができるが、技術的に複雑であり保守チームの技術力を評価したうえで選択する
DSLは成熟したモデルにおいて有効かもしれない
設計の宣言的スタイル
宣言的スタイルで仕様を拡張する
論理演算子を使用して仕様を結合する。SpecificationInterfaceを使って仕様を結合する
単純な要素から複雑な使用を組み立てる能力があると、コードの表現力が高まる
包含(Subsumption)
新しい仕様であれば古い仕様も満たされているとき、新しい仕様が古い仕様を包含している、という
コンポジット仕様がANDで結び付けられたリーフ仕様をすべて集めることができれば、検証するべきことは包含している側の仕様が包含されている側のリーフをすべて含んでいるということと、余分なものもあるかもしれないということだけだ(?)
containsで比較してるけど、実際はsubsumesを呼び出さないといけないんじゃなかろうかと思うけど、まぁここはA&&B&&Cの例でA&&Bであるかだけ考えてるからまぁいいのか
このcontainsもよくわからんというか、仕様がフライウェイトになってるのを想定しているのか、等価をオーバーライドしているのか。多分前者かな。
攻める角度
サブドメインを切り取る
設計全体に一度に取り組むのではなく、一部を切り出すこと。
モデルの一部が特殊な数学と見なせることがわかったら、それを切り離すこと
状態の変更を制限する複雑なルールが、アプリケーションによって強制されているのであれば、それを抜き出して分離したモデル化単純なフレームワークにし、そのルールを宣言できるようにすること
全体に広く薄く努力するよりも、ある領域にだけ大きな衝撃を与えるほうが効果的である
~算と呼べるようなものを作り上げるのが大事なのかも。顔料色算、つるかめ算、シェア算など
シェアパイの設計によってローンのコードで宣言テキスタイルが使えるようになった。
これによって作り出されるコードは計算としてではなく、ビジネスにおける取引の概念的な定義のように読める
-
複雑なロジックが副作用のない関数を持つ、特化した値オブジェクトにカプセル化されている
既存のオブジェクトを変更しないため、中間的な計算で自由に使うことができ、分析機能を構築することもできる -
状態を変更する操作が単純で表明により特徴づけられている
minusとかplusみたいな感じで直感的に書けるようになっている -
モデルの概念が分離されているので操作に巻き込まれる他の型は最小限になる
-
よく知られた形式主義によってプロトコルが把握しやすくなる
シェアを操作する完全に独自のプロトコルを金融用語に基づいて考え出すこともできるが、まず考え出すことも習得することも難しいので、自分の知っている数学の体系とマッチするように設計した
アナリシスパターンを適用する
アナリシスパターンは技術的な解決策ではなく、特定のドメインでモデルを考え出すのを支援する手引
これらのパターンには実装に関する意義深い議論があり、ある程度コードも含まれている
うーん、設計をマスターするためには設計パターンを知るだけじゃなくて、いろんな分野のモデリングも知っておいたほうがいいってことだよな…じゃあそこ読んで勉強しないとなぁ
特定のO/Rマッピングフレームワークを使っていたこともあって具体的なあs部クラスを作成する以外になかった
我々はトレードオフを計算したうえで妥協し、モデル駆動設計を投げ出すことなく先へ進まなければならないのだ
よく知られたアナリシスパターンの用語を仕様する場合は、そのパターンによって規定される基本的な概念を元のまま保つこと
ここのコード例、なんとなくわかったようなわからんような感じ
アナリシスパターンを使ったら利息や手数料はそれぞれの勘定科目に仕分記帳として記録されることがわかって、最初は本に従って発生と支払いが相殺するようにしてたけど、エキスパートに聞いたらその必要はなくなって、かつ発生は生まれた瞬間に計上するものだから、日付を単位にして生成すればよいことがわかった
夜間バッチの方は勘定科目への記帳の際に生じるコールバックみたいなもんをどうやって管理するか、って話で、最初はバッチスクリプトが記帳ルールに指示を出して、記帳ルールが処理の必要な記帳を探して処理する、って形を想定したけど、元帳の選択やエントリポイントの都合で資産のほうを起点にしたほうがよいとわかり、資産が各ルールを持ちそのルールに色々渡して実行させる、みたいな感じになった、という理解
デザインパターンをモデルに関係づける
デザインパターンをドメイン駆動設計で利用するためには、同時に2つのレベルで考えなければならない
- コードで用いられる技術的なデザインパターン
- モデルで用いられる概念的なパターン
ストラテジー
ドメインモデルに含まれているプロセスの中には技術的に動機づけられたものだけでなく、問題となっているドメインで実際に意味を持つものもある
振る舞いのバリエーションをプロセスにとっての主要な概念から分離したい
プロセスの中で変化する部分をモデルの中で独立したストラテジーオブジェクトに括り出すこと
ルールと、ルールが支配するふるまいは切り離して括りだすこと
経路選択サービスが仕様を満たす、最速/最安の経路を検索する
findFastest, findCheapestのメソッドは良さそうに見えるが、経路選択を行うコードを見てみるとすべての処理に条件文があり、最速か最安かの決定をいたるところで行っている(仕様側でfindFastest, findCheapestを呼び分けてるってことかな?)
選択基準を調整するためのパラメータをストラテジーに分離する。
行程規模ポリシー(ストラテジー)を経路選択サービスに渡し、経路選択サービスはすべての要求を同一の条件句を使わない方法で処理し(?)、行程の連なりの中から行程規模ポリシーに酔って計算される合計が元も少ないものを探す。
行程規模ポリシーは行程を受取り、Volumeを返す(Volumeは行程時間規模であれば行程の経過時間を返し、行程金額規模であれば行程の経過時間*行程の運行の日時運賃を返す)
ドメインにおける本質的に重要なルールは、輸送日程を構築する上で、ある行程を選択する際の基準であり、それがほかと明示的に区別されるようになった
コンテキスト内で共有できるステートレスなオブジェクトとしてのストラテジって何?
コンポジット
ネストされたコンテナの関係性がモデル荷反映されていないと、共通する振る舞い階層の各レベルで繰り返さなければならず、さらにネスト荷柔軟性がなくなる
クライアントは階層の様々なレベルを扱うにあたって各階のインターフェースを使わなければならない
コンポジットを構成するすべてのメンバを包括的に含む抽象型を定義すること
Compositeでは構造の各レベルで同じふるまいが提供され、意味のある質問は小さい部品にも大きい部品に対しても行える
ドア行程、行程、コンポジット経路はすべて経路を実装している
経路は複数の他の経路で構成されており、経路の各レベルはコンテナをある場所から他の場所へ移動させることを表していて、分解していけば個々の行程に行き着く
なぜフライウェイトではないのか
フライウェイトはドメインモデルにおいて全く一致するものがないデザインパターンの好例
より深い洞察へ向かうリファクタリング
探求チーム
モデルに対する不満の原因が何であれ、次の段階は、意図を明確かつ自然に伝達するようなモデルを改良する方法を探し出すことである
- 少人数のチームの自主的な意思決定
- スコープと休憩 数日の感覚を開けて短いミーティングを2, 3回すれば試す価値のある設計が作り出せるはず
- ユビキタス言語の駆使
先達の技
アナリシスパターンはソフトウェアを実装する経験に直接基づいているはずで、とらえにくいモデルの概念を提供し、知識を噛み砕くプロセスの糧となる
デザインパターンがドメイン層で採用されることも多いが、それは実装上の必要性とモデルの概念の両方に適合したとき
開発者のための設計
ソフトウェアはユーザのためのものではなく、開発者のためのものでもある。
しなやかな設計は意図を伝え、コードの実行結果、変更結果を予測しやすくする
タイミング
変更を完全に正当化できるまで待つのは待ちすぎである
ソフトウェア開発は変更することで得られる利益や、変更しないことで生じるコストを正確に割り出せるような、予測可能なプロセスではない
次の場合にはリファクタリングすること
- 設計がドメインに関するチームの現在の理解を表現していない
- 重要な概念が設計で暗黙的になっている(かつ、それを明示的にする方法がわかっている)
- 設計において重要な部分をよりしなやかにする好機がある
好きなときに好きな変更をすることを正当化するものではない
好機となる危機
モデルを安定して改良しているところで、突然ある洞察へと導かれ、それによってあらゆるものが刷新されるのがより深い洞察に向かうリファクタリング
そうした状況は好機というよりは危機として捉えられてしまいがち
成長曲線の話と似てる部分があるよな。
リファクタリングの労力と効果の間にもプラトーがあって段差があってみたいな話
戦略的設計
システムが複雑になりすぎて、個々のオブジェクトのレベルで完全に理解することができなくなると、巨大なモデルを扱い、あるいは把握するためのテクニックが必要になる
システムは概念も実装もより小さい部分に分解しなければならない。統合による恩恵を失わずにこのモジュール性を実現する必要がある
大規模なシステムであってもドメイン駆動設計が実装と結びつかないモデルを作り出すことはない
システムにおいて概念的にコアとなる場所、つまりシステムのビジョンをとらえるモデルに集中しなければならない
しかも、これらはプロジェクトが行き詰まることのないように行わなければならない
コンテキスト、蒸留、大規模な構造
明示的に境界づけられたコンテキストを定義したうえで、モデルを適用するのはその内部に限定し、さらに必要に応じてコンテキスト同士の関係も定義することによってモデルの質が低下するのを避けられる
蒸留によってシステムに於いて最も付加価値のある特別な側面を目立たせて、その部分にできる限り多くの力を与えるような構造にする必要がある
大規模な構造によって全体像が完成する
責務のレイヤ
このへんよくわからんがまぁ読み進めてみるか
境界づけられたコンテキストが一番わからないんだよな
モデルの整合性を維持する
2つのグループが異なるモデルを持ちながら、そのことを認識しておらず、またそれを検出するプロセスもなかったために問題がおきた
モデルに対する最も根本的な要件は、内部で一貫していることだ。用語の意味が常に同じであり、矛盾したルールがふくまれていないことでもある。モデルの内部におけるこうした一貫性は統一性と呼ばれる
巨大なシステム向けのドメインモデルを完全に統一することは現実的ではないし、コストにも見合わない
境界づけられたコンテキストによって各モデルを適用できる範囲が定義され、コンテキストマップによってプロジェクトのコンテキストとそれらコンテキスト間の関係性を全体的に概観できるようになる
共有カーネル?
境界づけられたコンテキスト
複数のモデルに起因する問題を解決するにあたっては、ある特定のモデルのスコープを明示的に定義しなければならない
モデルが適用されるコンテキストを明示的に定義すること。明示的な境界はチーム編成、そのアプリケーションに特有の部分が持つ用途、コードベースやデータベーススキーマなどの物理的な表現などの観点から設定すること。その境界内ではモデルを厳密に一貫性のあるものに保つこと
ただし、境界の外部の問題によって注意をそらされたり、混乱させられたりするのを避けること
境界づけられたコンテキストはモジュールではない
モジュールは1つのモデルの中で複数の要素を構成するものであり、別のコンテキストに対して必ずしも意図を伝えるものではない
境界づけられたコンテキストの中でモジュールが個別の名前空間を作成することにより、実は偶発的に発生する
モデルの断片化が見つけにくくなっているのだ(?)
なんか読んだ感じだと、チーム編成とかデータベーススキーマとか、アプリケーションとかの形式的な都合で分離することが多いのかな?
なんか意味論的なところで区切るのかなと思ってたけど
なんか楽天のカートを販売とクーポンで区切るみたいなやつ見て、それってどうなんっておもったんだよね
https://techblog.yahoo.co.jp/entry/2021011230061115/
うーん、確かに本で挙げられてる予約アプリケーションにおける貨物と追跡アプリケーションにおける貨物、のようにBasketのユースケースみたいなもんが違っていると言えるのか
Coupon::Basket Order::Basketのようになっていたほうがいい?
モジュールとどう違うように表現するのがいいのかな
カートチームとクーポンチームが分かれているっていうことは確かに境界があるってことだな
更にクーポンコンテキストの中でも計算と適用で商品の考え方が違う、
最初の一歩は状況をあるがまま認識することである。同じコンテキスト内にないのだから、何かが代わるまでは、コードを共有しようとするのをやめるべきである
境界づけられたコンテキストの一番のメリットは予約モデルチームと運行スケジュールチームの間で、非公式にモデルを共有することのリスクが認識されたということ
コンテキストマップは領域を図示し、コンテキストとそれらのつながりについて全体像がわかるようにする
コンテキスト感のさまざまな関係の性質を定義するパターンがいくつかある
継続的な統合のプロセスによって、ある境界づけられたコンテキスト内におけるモデルの統一性が維持される
境界づけられたコンテキスト内での分派を認識する
モデルの違いが認識されていないことを示す兆候
- コーディングされたインターフェースが組み合わせられない
- 予期していないふるまい
- 自動テストを用いた継続的な統合でとらえやすくなる
- 言語の混乱
- 重複した概念と偽同族語
重複した概念: その情報が変更されるたびに2箇所で変換を伴う変更が必要になる
偽同族語: 同じ言葉を使っているのに、違うものを指しているとき
継続的な統合
多くの人が同一の境界づけられたコンテキストで作業していると、モデルが分裂する傾向は強くなるが、だからといってシステムをさらに小さいコンテキストに分割してしまうと、結局は有益なレベルで統合し、一貫させることができなくなる
XPが有効
- モデル概念の統合
- ユビキタス言語を継続的に練り上げるなど、チームのコミュニケーションを絶え間ないものにする
- 実装の統合
- 段階的に行われ、再生可能なマージ/ビルドテクニック
- 自動化されたテストスイート
- 変更が統合されない期間に対して、妥当な短さの上限を設定するルール
すべてのコードと外の実装成果物を頻繁にマージさせるプロセスを策定すること
その際、分裂に対して素早く警告する自動化されたテストも一緒に用いること
ユビキタス言語の執拗な鍛錬を絶え間なく行い、モデルに対する共有された見方を練り上げること
仕事を必要以上に大きくしないこと
コンテキストマップ
他のチームに所属する人々は、コンテキストの境界をあまり意識せず、境目をぼかしたり相互の接続を複雑にしたりする変更をそうとはしらずに加えるかもしれない。
別々のコンテキスト同士を接続しなければならない場合、コンテキストは相互に滲み出す傾向がある
境界づけられたコンテキスト間でのコードの再利用は危険なので避けるべき
混乱を減らすためには、別々のコンテキスト同士の関係を定義し、プロジェクトにあるモデルのコンテキスト全てに関する全体的な視点を作成しなければならない
コンテキストマップはプロジェクト管理とソフトウェア設計が重なり合う場所にある
境界がチーム編成の輪郭に従うのは自然な成り行きだ。
プロジェクトで機能しているモデルをそれぞれ識別して、その境界づけられたコンテキストを定義すること
非オブジェクト指向のサブシステムにある暗黙的なモデルも含まれる
境界づけられたコンテキストに名前をつけること
やっぱこういうチームが複数あって、みたいなところが経験できないのはあるよな~
まず現在存在する領域の地図を書くこと
コンテキストマップはパターンを無理にそのまま使うのではなく、見出した関係をそのまま記述する方が良い
例 輸送アプリケーションにおける2つのコンテキスト
輸送アプリケーションから利用する、経路選択サービスで輸送機器の運行を実現することになる拡張されたドメインモデルを使い、さらにそれを輸送日程を構成する行程と結びつけようとしたが、既存のアルゴリズムに適合させるために最適化ネットワークとして実装し、運行の各工程をマトリクス内の要素として表現する必要があった
経路仕様 -> 位置コードのリスト
ノードIDのリスト -> 輸送日程
要するに2システム間の接点を明確にする、みたいな話だと思ってるけど
複数チームがないと…みたいな事も考えたけど、外部のAPIから情報を取ってくるとかクローリングする、とかでも同じようなことはやってるよな。デジゲのGameとSteamのGameは名前は一緒だけど、コンテキストが違うので、変換層を設けている。
でその変換層を2つのチームで協業してテストスイート上で編集していくっていうことか
変換: Translatorクラス
SteamGameDetailTranslatorとかってしたら、もっとわかりやすかったかもな~
他のコンテキストと快適に共存するための秘訣の1つは、インターフェースに対して効果的なテストを一式作ることである
これってどういう風に実装するのがいいんだろうね
1つのシステム内だったらそのまま実装できるけど、マイクロサービスだったりすると考えないといけなさそう
公開APIをシステムから生成して、テスト用に公開するとかかなぁ
DDDって話は難しいんだけど、ソフトウェア工学に従っていれば普通にやっていることではあるんだよな
コンテキストマップを共有してドキュメント化する
- 境界づけられたコンテキストには名前をつけ、ユビキタス言語に組み込まなければならない
- メンバーは誰でも境界がどこにあるのかを知っていなければならず、コードの一部、あるいはどの場所をとってもコンテキストがわかっていなければならない
共有カーネル
機能面での統合が制限されると、継続的な統合(CI)にかかるオーバーヘッドが大きくなりすぎると思われるかもしれない
密接に関連した巨大なアプリケーションを複数チームでなんとなく触っていると、しばらくはうまく言っても限界が来る。
チーム間で別のコンテキストを作成し、ドメインの中のサブセットを慎重に共有すればうまくいくかも
切り出されたサブセットを変更するときは一方のチームからもう片方のチームへの承認が必要
稼働しているシステムは頻繁に統合すること、ただしチーム内での統合よりも頻度はおさえること
チームがカーネルに変更を加える問は、共有カーネルを別にコピーして行い、時間をおいて他のチームと統合する
共有カーネルはコアドメインでもいいし、複数の汎用サブドメインであることもある。
目標は重複を減らして2つのサブシステムの統合を容易にすることである
顧客・供給者の開発チーム
あるシステムがもう一方のシステムに入力を与えるようなものがある
依存関係にあるサブシステムは上流・下流のように境界付けられたコンテキストが2つに分かれる
ここで発生する問題は以下の2つ
- 上流チームが下流チームの変更承認を取るのに時間がかかったり、下流を壊してしまわないかを恐れて自由な舵取りができなくなる
- 下流チームが上流チームの優先順位のいいなりになってしまう
2つのチームに明確に、顧客・供給者という関係を確立する。下流チームを顧客として上流チームの会議に加え、直接やりとりすることで合意を得て作業が進められる
期待されるインターフェースを検証する、自動化された受け入れテストを共同で開発すること
システムは顧客の要望を叶えるためのものだから顧客の要望が最優先、下流チームが貧者となって富者である上流チームにお願いをする、というのとは反対である
上流チームが安心して開発できるよう、自動化されたテストスイートを用意する
収益予想と予約
共有カーネルは同じ開発環境じゃないと難しい
上流/下流の関係ができたとき顧客・供給者の関係になるには2つのチームが同じ経営陣のもとにあり、目標が同じであるか、実際に2つの企業として存在している必要がある
順応者
上流/下流の関係ができたとき、上流チームと経営階層上離れていたり、利用者がたくさんいたり、経営そのものが厳しくなっていたりして供給者・顧客の関係を結ぶのが難しいときにどうするか
下流チームにはどうすることもできないので、以下の3つが選択肢となる
- 別々の道: 下流チームが上流のソフトウェアを使うことを完全に断念する
- 腐敗防止層: 下流チームでも独自のモデルが必要とされているときは、腐敗防止層を設けてそこに責任を持つ
- 順応者: 例え多少モデルが合わなくても上流チームのモデルに従う。供給者のユビキタス言語を使い、境界づけられたコンテキスト間での変換を廃止すること
実際巨大なインターフェースを持つコンポーネントを利用するときは、異なる境界付けられたコンテキストにあるのでアダプタは必要になるものの、モデルは等しくあらねばならず、そうでないならそもそも利用することの価値を疑うべき
たしかにその外部で独自のモデリングが必要になるものの、接続部分ではコンポーネントの導きに従う
順応者と共有カーネルは似ているが、共同作業を行うのに興味があるかないか、という点に違いがある
腐敗防止層
新しいシステムを構築していて、それが他のシステムと接する巨大なインターフェースを持たなければならない場合、2つのモデルを関係付けることが難しいと、最終的に新しいモデルの意図が完全に圧倒されてしまうかもしれない
2つのシステムの間でデータに対する解釈の違いから問題が発生する
また、2システム間のデータ通信がプリミティブな値でやりとりをする低レベルなものであるとき、例え同じデータの解釈であったとしても、それがシステム内でどう関連付けられるか、という微妙な違いから問題が生じる
隔離するためのレイヤを設けることによって、クライアントに対して独自のドメインモデルの用語で表現された機能を提供すること
腐敗防止層は2システム間の間で行われる技術的な通信の詳細ではない、他のモデルやプロトコルの持つ、概念やアクションを別のものに変換するものである
腐敗防止層のインターフェースを設計する
腐敗防止層の持つ公開インターフェースは、通常サービスの集合として実装されるが、エンティティとして現れることもある
全く新しいレイヤを構築することで、2つのシステム間の意味的な変換を行い、自システムのモデルにとって一貫性のある形で情報を得ることができる
これらのインターフェースは単一のコンポーネントとして表現されるよりは、サービスの集合として実装されたほうがよいこともある
腐敗防止層を実装する
腐敗防止層を構成する方法として、ファサード、アダプタ、変換サービスの組み合わせがある
自サブシステム => サービス => アダプタ => ファサード => 他サブシステム => ファサード => 変換サービス => サービス => 自サービス
ファサードは他サブシステムの呼び出しを効率化する。自サブシステムのモデル概念を持たず、他サブシステムの境界付けられたコンテキストに属する
アダプタはファサードの前に位置し、サービスからの自サブシステムのモデル概念に沿ったリクエストを他サブシステムのリクエストに変換する
GoFとの違いはアダプタが変換するのは、オブジェクトだけでなくXML,JSONなどのデータも含まれるが、概念的には同じ
変換サービスは他サブシステムからのレスポンスを自サブシステムのモデル概念に沿った形に変換する
- 腐敗防止層が想定するリクエストの流れは、基本自サブシステムから他へだが、双方向にすることもできる
- 2つのサブシステムの通信リンクをどこに置くか
- 他サブシステムにアクセスできない場合: ファサードと他サブシステムの間
- できる場合: アダプタとファサードの間。通信のプロトコルより、ファサードのインターフェースのほうがシンプルなはずだから
- 相手のサブシステムを変更できるのであれば、変更すると楽になる場合があるかも。明白なインターフェースを作り、自動化されたテストを用意することから始めると良い
- 統合が広範囲に及んでいるときは変換にかかるコストが膨大になるため、慎重に見極めながら相手側のモデルに寄せるのがよい。もし、この問題のほとんどの部分で相手側に寄せるのがよいとなったのであれば、順応者パターンにして変換そのものを取り除いたほうがよい
- 他のサブシステムがシンプルであるか、インターフェースが整理されている場合ファサードは必要ないかもしれない
- 2つのサブシステムに固有な機能(監視とか追跡)であるならば腐敗防止層に実装してよい
別々の道
統合は高いコストを支払わなければならないので、その効果が薄いときは境界付けられたコンテキストを完全に分離して別々の道を歩んだほうがよいこともある
他システムの機能が必要なときは、総合メニューに機能へのショートカットを置いておくとか、画面内に他システムへのリンクを置いておくとか
XML、HTMLにエクスポートしておいて共有するとかがある
ロジックを共有することはせず、変換層を通じてのデータ転送は最小限におさえるか、できればないほうが良い
公開ホストサービス
あるシステムが他のたくさんのサブシステムと統合しないといけない場合、変換層をそれぞれに用意しているとチームが疲弊する
そこでこちらのサブシステムに対してアクセスするときのプロトコルををサービスの集合として定義公開し、他のサブシステムがそれを使用できるようにする。
新しい機能が必要になったときはプロトコルを拡張するが、あるサービスに固有の機能の場合は1回きりの変換サービスを用意し基礎となるプロトコルはシンプルに保つこと
他サービスは公開ホストのモデルと結合しなければならなくなり、ホストの使用する特定の方言を習得せざるを得なくなる
状況によっては知名度の高い公表された言語を交換可能なモデルとして使用することで、結合度が低くなり理解が用意になる
公表された言語
2つのドメインモデルを共存させ、その間で情報をやり取りすることが必須であれば変換プロセス自体が複雑化し、ドキュメント化するのが困難になっていくかもしれない
新しいモデルを構築しているときには自分たちのモデルが最高のものだと考えるので、直接そのモデルに変換することを考えるだろう。しかし、古いシステムの方も機能改修があるかもしれない。結局どちらかのうちましな方を選ぶことになってしまう。
複数のビジネス間でコミュニケーションが必要なとき、どちらか一方の言語を採用するのは非現実的だ
また、複数のサービスの中でどれか1つの基礎になっているモデルをコミュニケーションの媒体として使用すると、そのサービスは安定していなければならないという条件から新たな機能開発が難しくなる
つまり既存のドメインモデルへの直接的な変換や、既存のドメインモデルからの変換はうまくいかないことがある
なぜなら既存のモデルは過度に複雑だったり、分割されていなかったり、ドキュメント化もされていないだろうからである
そのモデルをデータ交換用の言語として使ってしまうと結局すべてのシステムが新しい機能開発ができなくなる
公開ホストサービスは他サブシステム間との統合に対して標準化されたプロトコルを使用する。
そのプロトコルが採用するのはシステム間での交換というドメインのモデルだが、そのモデルはホスト自身でも使われていないかもしれない
そこで、その言語を公表するか、すでに公表されている言語を探すのがよいだろう
公表されている、というのはその言語に興味があるコミュニティがいつでもその言語を入手することができ、さらにドキュメント化されている状態である
当時XMLがデータの交換を容易にすると期待されていたのは、DTD(DocumentTypeDefinition)やXMLスキーマが存在していて特殊なドメイン言語(遺伝子とか化学式)を正式に定義し、データをその形式に変換できたからである
必要なドメインの情報をコミュニケーションの媒体として使用できる、明確にドキュメント化された共通言語を使用し、その言語からの変換やその言語への変換を行う
DB2上のアプリケーションをBtrieveでも使えるようにしようとしたとき、そのアプリケーションはDB2の上にオブジェクト格納に関する抽象を用意していた。Btrieve版のそれを実装することで対応したが、用意されている抽象に関するドキュメントもなくて時間がかかったし、永続化のロジックに深く入り込んでいたのでリファクタリングも難しかったと想定される
結局何をすべきだったかというと、DB2の機能はドキュメントとして公開されている(=公表した言語)わけなのだから、そのアプリケーションが使用しているDB2インターフェースのサブセットを識別し、Btrieve版を用意するのがもっと良かったかもしれない
化学のための公表された言語
XMLの方言としてのCML(化学式)が定義されたことで、1つのDBに対してでは採算がとれないようなアプリケーションが登場できるようになった。
XMLが広まったことで学習曲線が平坦になったし、XMLを解析するパーサやライブラリが使えるようになった
象のモデルを統一する
象に対して、壁・縄・蛇・木という印象を持っているとき
- 別に統合が必要なければそのままでいい、意見が一致しないことを理解するだけで価値がある
- それぞれに独立した境界づけられたコンテキストがあり、位置によって見えているものが違うということが分かればよい
- 単一の境界づけられたコンテキストを持ちたいと思ったとき、統一されたモデルを持つのは難しいが、足は象を支えている、縄・蛇がついている、象は壁のようになっている部位がある、といった各部分がコンテキストにどう関連付けられているかを知るところから始める
- あるモデルの一側面に対して、解釈の異なるモデルがある場合に必要なのは新たな抽象である
- そのモデルにどの特徴があるか、というよりはどの特徴がないのか、ということに気をつけてモデリングを深めていく。結局のところ各々が自分たちの認識に不足があるということをわかっていれば問題は処理できるようになる
モデルコンテキスト戦略を決定する
まずは現状をコンテキストマップに起こす。そうするとそれを変更したくなるだろうから、コンテキスト境界間の関係を意識的に選択できるようになる
チームでの意思決定とより上層での意思決定
チームがまず決めなければならないのは、境界付けられたコンテキストをどこに定義するか、とコンテキスト間にどんな種類の関係をもたせるかということだ
こうした意思決定はチームで行うか、少なくともメンバー全員が知っている必要がある。
しかし、実際にはチームより上層での合意が必要になることも多い。技術的には統合したほうがいいが、政治的な理由や報告系統の問題で不可能になるかもしれない。
現状をコンテキストマップに落とし込んだ後は、現実的に変換を選択すること
コンテキストに自らの身を置く
ソフトウェアプロジェクトを始めるとまず開発者の関心を惹くのは自分の関わるシステム、次にそれと通信するシステムである
典型的には、主要な1,2個の境界付けられたコンテキストが切り出され、更に1,2個のコンテキストがサポート役となる。そして、それらコンテキスト間の関係と、外部システムとの関係が加わる
開発者は自らが取り組んでいる主要なコンテキストの一部であることを認識しなければならないが、先入観を持っていることと、コンテキストマップの適用範囲外に出たときのことを忘れなければこれは問題ではない(?)
=> 自分の立ち位置を認識しとかないといけないよ〜、みたいな話かな?
境界を変換する
境界付けられたコンテキストの境界線の引き方には様々な種類があるが、結局のところ大きな境界付けられたコンテキストを好むか、小さなコンテキストを望むかの間でバランスを取ることになる
大きな境界づけられたコンテキストのメリット
- ユーザータスクのフローがスムーズになる
- 複数のコンテキストよりも理解が容易
- 変換がいらなくなる
- コミュニケーションが促進される
小さな境界付けられたコンテキストのメリット
- 開発者間のコミュニケーションのオーバーヘッドが減る
- 統合を頻繁にできる
- 特殊なドメインや専門的な要求を実現するにはユビキタス言語の方言を使ったほうがいい
- 大きなコンテキストのためのより抽象的なモデルを用意するのは難しい
別々の境界づけられたコンテキスト間で機能を深く統合するのは現実的ではない。なぜなら統合はあるモデルのなかで他のモデルを厳密に述べられる部分に限られるし、そうであってもコストが高いから
しかし、インターフェースが小さくなるなら機能を深く統合したほうがよいこともある
変更できないものを受け入れる、外部システムの輪郭を描く
境界線を引く際は最も単純なところから始めると良い
境界付けられたコンテキストの中に明確に入っていないものとして、すぐに置き換える予定のないレガシーシステムや、必要なサービスを提供する外部システムが上げられる。
それらを最初の段階で設計から分離する
ただし、境界付けられたコンテキストっていうのはそもそもの意味として、その内部で統一されたモデルが存在していることを前提にしているから、レガシーシステムのチームと合意して継続的な統合をできるときは良いが、できないときはその中にあるセマンティクスの矛盾に注意すること
外部システムとの関係
大きく3つある
- 別々の道: そもそも統合が必要なければ外部システムを含める必要はないが、決定は注意深く検討しなければならない
- 順応者: レガシーシステムが大きくて、新しいシステムがその拡張にすぎない場合や、Excelなどの単純な意思決定支援ツールを拡張する場合は順応者になったほうがいい。そのシステムとのインターフェースが巨大になるのであれば、変換層自体が新しいシステムの機能よりも大きくなってしまうかもしれないからだ。また、そうであったとしてもレガシーシステムの背後にあるドメインを新システム内でより明示的にすることはできる。ただし、その場合は拡張することのみを行い、既存のモデルを修正しないこと
- 変換層,腐敗防止層: レガシーシステムのモデルが貧弱だったり、新システムが単純な拡張以上のものになるときは自分自身の境界付けられたコンテキストがほしくなる
設計中のシステム
設計中のシステムとは、まさに開発者が取り組んでいるシステムのこと。その中で境界付けられたコンテキストの決定を型にはめるのは難しい
通常10人未満の小さなチームであればシステム全体を1つの境界づけられたコンテキストにするが、大規模なチームでもそうすることもできなくはない。
大規模なチームの場合、基本的には共有カーネルを探して10人未満のチームに分割し1つのチームで1つの境界付けられたコンテキストを持つ。そのとき、コンテキスト間の関係が一方向であれば顧客・供給者パターンにすればよい。
2つのチームの考え方が異なっているせいでモデルが衝突し、それを解消したくない場合はモデルを別々の道に進ませることができる。2つのチームは変換層を用意し、そこを継続的な統合の対象とすればよい
外部システムとの結合の場合は腐敗防止層を用意して、外部のサポートなしに対応する必要がある。
1つの境界付けられたコンテキストを複数のチームで共有するのは難しい
別のモデルで特殊な要求を満たす
同じビジネスに関わるさまざまなグループが独自の専門用語を発達させることがあり、それはより使う人の要求にあつらえられていることが多い
そうした用語を社内の標準的な用語で置き換えようとすると違いを分析しなくてはいけないし、そもそも役に立たないだろう
こういうときは別の境界づけられたコンテキストで対応することにして、別々の道を歩ませて良い。
ユビキタス言語の方言の間で重なり合うところがあれば共有カーネルを用いることで変換コストを最小限にできる
統合が必要なかったり限定的なときは別のモデルにすることで今までの用語法が引き続き使用できるが、コストとリスクも伴う
例えばコミュニケーションが減ったり、オーバーヘッドが増えたり、重複が発生することがあるが、中でも一番は独特の突飛なモデルを正当化する口実にされてしまうことだ
そういうときはその用語がどれほどの価値を提供するのか、という点を注意深く判断する必要がある
時にはそれらに統一的な見方を提供する深いモデルが現れることもあるが、それはあくまでリファクタリングの結果として現れたものであって計画に組み込めるようなものではない
統合の要求が広範囲に渡わたると変換のコストが急上昇するので、チーム間の調整を行うことによって容易に変換できるようにする
デプロイ
複数のシステムのパッケージングとデプロイを調整する作業は想像よりはるかに辛く退屈
境界付けられたコンテキスト間の関係性はデプロイ戦略に影響を及ぼす。例えば顧客・供給者パターンではリリースタイミングを同時にして複数のバージョンが存在しないようにしたい
単一の境界付けられたコンテキスト内であってもデータ移行に時間がかかったりするときは結果的に複数のバージョンが共存することになるかもしれない
デプロイにかかるコストは境界付けられたコンテキストの関係性の決定にフィードバックするべき。
トレードオフ
機能の統合による利益と調整やコミュニケーションにかかるコストとのトレードオフで関係性を決める
すでにプロジェクトが進行中の場合
まず現在の境界づけられたコンテキスト、コンテキスト間の関係性をコンテキストマップに反映し、現在チームが実際に行っていることを統制するところから始めなければならない
変換
コンテキスト間の関係性は1度決定されてから変更されることもままあるが、大規模なものになりがちなので一連のステップに分解できるようにする
別々の道=>共有カーネル
変換する箇所があまりに大きかったり、重複がはっきりしていることは境界づけられたコンテキストをマージする動機になるが、根気が必要
最終的に単一のコンテキストにしたいときでも、共有カーネルを定義するところから始めること
- まず各コンテキスト内部でモデルが統一されているかを確認する
- プロセスを準備する、週に1回は統合を行い、テストスイート一式を用意する
- 重要でない部分から取り組み始めること。すでに変換層が用意されているものをマージすると実績もあるし変換層自体を薄くできて良い
マージ戦略
- 2つのうち片方に寄せちゃう
- 少しずつマージしていって2つを同じものにしていく
- 両方の機能を満たす新しい深いモデルを発見する
- サブドメインの共有モデルを考案するため、各チームから選ばれた2~4人のメンバーで詳細に渡って精査する(同義語を特定するとかテスト一式の概略を示すとか)
- 各チームのメンバーが実装する。問題があれば3からやり直す
- 各チームの開発者は共有カーネルによる統合を引き受ける
- 不要になった変換層を削除する
代替案としてサブドメインを一方の境界づけられたコンテキストのほうに移行することが挙げられる
共有カーネル => 継続的な統合
2つの境界づけられたコンテキストを完全に統一するというのは単にモデルの相違を解消するだけでなく、チーム編成を変えることから始まり人々の話す言葉を変えることにつながる
- 継続的な統合に必要なプロセスが両チームで準備されているようにすること
- 両チームのメンバーをローテーションさせ、知見を貯める
- 各モデルの蒸留を個別に明確化する
- コアドメインのマージはオーバーヘッドが大きいので着手したらそれを最優先で進める
- モデルを片方に寄せるか、より深いモデルを発見する
- 共有カーネルが大きくなるにつれ統合の頻度を下げる
- 共有カーネルが元々の2つのカーネルを包括するころには、大きなチームができているか、2つの小さいチームがコードベースの共有を行い、頻繁にローテーションしているかのいずれかになる
レガシーシステムを段階的に廃止する
レガシーシステムを廃止するときは、新システムは腐敗防止層を経由してレガシーシステムと通信し、少しずつ置き換えていく。
新旧のシステムが並行して稼働する期間が存在する
- 1回のイテレーションで望ましいシステムのどれかに追加できるような、レガシーシステムの特定の機能を識別する(?)
- 腐敗防止層で必要となる機能を識別すること
- 実装すること
- デプロイすること
- 腐敗防止層にある不必要な機能を削除する
- レガシーシステム側の段階的な廃止を行うが、難しければ無視する
公開ホストサービス => 公表された言語
他システムとの統合をその場しのぎのプロトコルで行っていたが、更なる統合や理解の困難さによってシステム間の関係を公表された言語を用いて形式化する必要が出てくる
- 業界標準の言語が入手できるならばそれを使用すること
- そうでなければ、システムのコアドメインを明確にするところから始めること
- コアドメインを交換用の言語として使用すること。その際はできればXMLのような標準的な交換パラダイムを用いること
- 新しい言語を共同作業に携わる人全員に公開すること
- 新しいシステムアーキテクチャが関係する場合にはそれを公表すること
- 強調するシステムそれぞれに変換層を用意すること
- 切り替えること
公表された言語は安定していなければならないが、リファクタリングのためにホストのモデルを変更する自由も必要だ。それゆえ交換用の言語とホストの言語を同一視しないこと
確かに両者を近づけておけば変換のオーバーヘッドが減り、ホストを順応者とすることもできるが、必要であれば変換層を強化する選択肢も残しておくこと
コンテキストはチーム編成やその巨大さによって分割されてしまうかもしれないが、少なくともコンテキストマップを描き、維持できる境界を定義すること。もしモデルそのものの複雑さ以外で1つのコンテキストを作ることができるなら、分割は良い選択肢でないかもしれない
蒸留
レイヤ化などを行ってもなお、モデルが複雑すぎるときにどうするか
蒸留とは混ざりあったコンポーネントを分割するプロセスであり、価値があって役立つ形式で本質を抽出するためのもの
蒸留によって分離された副産物(汎用サブドメイン、凝集されたメカニズム)も重要なものになる
ソフトウェアを価値づけ、特徴あるものにするコアドメイン
- システムの全体的な設計と、設計同士がどう関係するのかをチームメンバ全員が把握できるようにする
- ユビキタス言語に入れやすいサイズのコアモデルを識別する音で、コミュニケーションを促進する
- リファクタリングの指針となる
- モデルで最も価値のある領域に作業を集中させる
- アウトソーシングと既成コンポーネントの利用、その割当について決定する際の指針となる
コアドメイン
巨大なシステムでは寄与するコンポーネントの数が多くなり、コードは次第に属人化し重複が発生したりする。このことはドメインの全体像についても起こりうる
設計のすべての部分が等しく改良されるわけではないので、優先順位を設定しモデルの重要なコアを洗練し、アプリケーションの機能に活用してドメインモデルを資産化しなければならない
コアドメインを見つけてそれをサポートする大量のモデルから区別する手段を提供すること
最も才能がある人をコアドメインに割当て、それ以外の部分を作業するときはどうコアドメインに寄与するのかによって正当化すること
競合時に優位に立つために秘匿される側面があるのならそれはコアドメインだ
コアを選択する
選択するコアドメインは視点によって決まる
1つのプロジェクト、企業を通じて一貫したコアドメインを定義することはできる
誰がこの作業をやるのか
熟練した開発者とドメインエキスパートが協力できるようなチームを作る
専門家を雇ってきて短期間開発してもらう、みたいなことはできないが、教育は効果的
コアドメインを購入することもできないが、特化したフレームワークを使用してコアドメインに集中するようにはできる
ただし、制約の大きいフレームワーク、中枢がコアドメインではなかったりしたものは避け、特別な要求がないのであれば購入を検討すること
蒸留の拡大
ドメインビジョン声明: 最小限の投資でその価値と概念を伝える
強調されたコア: 設計を変更しないまま、コミュニケーションを改善し意思決定を支援する
汎用サブドメイン:
凝集されたメカニズム
隔離されたコア
抽象化されたコア
そう考えると、アーキテクトっていう職業ってどうなんだろうね
最初に入って共有しながらガーッと作ってみたいな感じなのかな
汎用サブドメイン
モデルの中には専門的な知識をとらえることもなく、複雑さを付け加えるだけのものもあるがそれでもモデルを完全に表現するには必要不可欠である
タイムゾーンの問題とか
プロジェクトの動機となっていない高凝集のサブドメインを識別し、そこからくくりだした汎用的なモデルをモジュールに入れて、特殊な要求が入り込まないようにすること
コアドメインより開発の優先度を下げ、購入を視野にいれる
解決策として
- 既成品による開発
- 公表されている言語やモデルの流用
- 外注
- 内製
まぁ要するにgem使えってことなんだよな
時と共にコアドメインを構成するものはなにかということについての考え方は狭まっていき、多くの汎用的なモデルが実装フレームワークや公表されたモデル、アナリシスパターンによって手に入るようになる
たとえ汎用サブドメインを自分で実装することになったとしても、それをコアドメインと区別することには価値がある
汎用とは再利用可能という意味ではない
コードの再利用は考えなくても良いが、モデルの再利用はありうる
プロジェクトのリスク管理
最初の動くプロダクトを作る際に、汎用サブドメインに関する実装も行ってしまいたい気持ちになるが、リスクの観点から考えるとやはりコアドメインに注力するべきである
コアドメインなしにプロジェクトの成功はありえないから
ドメインビジョン声明文
開発の後期になるとモデルを詳細に調べなくても分かる形て、システムを説明する必要がある
ドメインモデルの重要な側面が複数の境界づけられたコンテキストにまたがっているときもあるが、構造上共通の焦点を示すような構造にはできない
ドメインモデルの本質とエンタープライズにどう役立つかがドメインビジョン声明文の焦点
1ページでコアドメインとそれがもたらす価値に関する簡潔な記述を作成すること(価値の提議)
早期に作成し、新しい洞察を得たら改訂すること
強調されたコアドメイン
ドメインビジョン声明文だけでは広い視点からの情報を伝えるのみで、各要素に対する解釈はメンバーに委ねられてしまうので、コアドメインをわかりやすく強調する必要がある。
このときコードの構造を変更して強調しようとするのは、大胆すぎであり実際は数点のドキュメントや図で事足りる
蒸留ドキュメント
技術的な詳細というよりはコアに境界を引くための最低限の説明のエントリポイントであり、3~5ページ程度の非技術者にもわかるようなものにしなければならない
コアにフラグを立てる
モデルの主要なリポジトリ内においてコアドメインの各要素にフラグを立てること
役割を明らかにするまではせず、コアに入るものと入らないもの区別できるようになってさえいればよい
これは結構使える考え方かも
Railsではmodelsに全部突っ込んじゃうけどSteamClientとかPushClientはコアではないから、別でくくりだしたいよね
GameとGenreを結ぶGameGenreはどうかなぁ。
プロセスツールとしての蒸留ドキュメント
蒸留ドキュメントを変更しなければいけないようなときっていうのはコアドメインを変更するときで、コアドメインの変更の影響は大きいわけだからそれをチームに共有することで、各開発者の自律が保たれる
凝集されたメカニズム
処理が複雑になると設計が肥大化し、モデルを圧倒することがある。その処理がそれ自体概念的なまとまりがあると気づいたのであれば、、軽量なフレームワークとして切り出すこと。
そのフレームワークは意図の明白なインターフェースを実装し、モデルが処理を宣言的に行うことができるようにする
凝集されたメカニズムは形式主義(?)やすでにドキュメント化されているアルゴリズムの形をとることが多い
汎用サブドメインvs凝集されたメカニズム
汎用サブドメインはあくまでドメインの一側面を捉えているが、重要ではないもの
凝集されたメカニズムはドメインを表現せず、モデルによって提起された面倒な処理の問題を解決する
メカニズムがコアドメインの一部である場合
保険の計算ロジックなどメカニズムがコアドメインの一部であるような場合もある
蒸留して宣言的スタイルにする
汎用サブドメインをくくりだすことで雑音が減り、凝集されたメカニズムによって複雑な操作がカプセル化される
隔離されたコア
モデルの要素の中には一部はモデルに仕え、一部は補助的な役割をするものがある
モデルをリファクタリングしてコアの概念を補助的な役割を果たすものから分離すること
そうすることでコアを高凝集、低結合にする。例えコアと密接に結びついていたとしても汎用的な要素や補助的な要素は全て別のパッケージに入れる
- コアサブドメインを識別すること
- 関連するクラスをそれを関係づけている概念に由来する新しいモジュールへ移動する
- コードをリファクタリングして、その概念を直接表現していないデータとロジックを切り離す
- 新しい隔離されたコアのモジュールをリファクタリングして、関係と相互作用をより単純で伝達力あるものにし、他のモジュールとの関係を最小限におさえて明確化すること
- 隔離されたコアが完成するまでこれを繰り返す
隔離されたコアを作成するコスト
コアと密接に結合した、コアではないクラス群との関係があいまいになったとしてもコアを隔離する利点のほうが大きい
隔離されたコアを切り出すべきタイミングは、システムにとって重要かつ巨大な境界づけられたコンテキストがあり、モデルの本質的な部分が補助的な機能によってわかりにくくなってしまったとき
これはウチとはあんまり関係なさそうだなぁ
例えば求人サービスって人を求人に結びつけるのが主目的なわけだけど、コアはなんだろう?募集?
チームの意思決定を進化させる
隔離されたコアに対する変更は一方的に行ってはならず、なにか変更をするときは必ずチームに共有し、すぐに修正できるような状態にしておくこと
貨物輸送モデルのコアを隔離する
確かにアプリケーションは最終的には収益を目的としているものもあるが、それがコアとは限らない
抽象化されたコア
コアドメイン自体が複雑化したときは、モデルにおける最も根本的な概念を識別し、それを別のクラス、抽象クラス、インターフェースに括り出すこと
抽象化されたコアは最終的には蒸留ドキュメントに似たものになるはず
深いモデルの蒸留
蒸留はコアからドメインの一部を切り離すというだけでなく、コアドメインを改良することも意味している
深いモデルを生み出すブレイクスルーはどこで発生しても価値を生むが、プロジェクト全体の軌道を変えるのはコアドメインで起きたときだけ
リファクタリングの対象を選ぶ
- 困ったときのリファクタリングをする際に、原因がコアドメインかコアと補助的要素の関係を含むものかどうかを調べる。そうであれば頑張って修正する
- 自由にリファクタリングする余裕がある場合、最初に集中すべきはコアドメインのより適切な括りだし、コアの隔離の改善、補助的なサブドメインから不純物を取り除くことによる汎用サブドメイン化である
大規模な構造
モジュール化や境界づけられたコンテキスト、蒸留によってモデルの一部はわかりやすくなるが、全体の構造は見えてこない
巨大なシステムには包括的な原則が必要である
大規模な構造はシステムをおおよその構造から議論し、理解できるようにするための言語である
構造は1つの境界づけられたコンテキスト内に限られることもあるが、通常は複数のコンテキストにまたがっていて、プロジェクトに携わるチームとサブシステムを1つにまとめる概念的な構成を提供する
チームが適度に少人数でモデルが複雑すぎなければ、適切なモジュール化と開発者間の非公式なコミュニケーションによってモデルが組織化された状態は保たれる
進化する秩序
最初の設計を厳格に適用しようとすると、どこかで無理が生じたり取りこぼしが生じたりする恐れがある
局所的な部分に限ってはそれにあった設計をするほうがよい
アーキテクチャがアプリケーションやドメインの領域に乗り出すと、アーキテクチャ自体が問題の発生源になりうる
大規模な構造を適用すべきなのは、モデルの開発に不自然な制約を強いることなく、システムを大幅に明確化する構造が見つけられたとき
システムのメタファ
ファイヤウォールの例みたいにメタファがモデルの外観をわかりやすくしてくれることはある
ただし、メタファからの影響によって開発に問題が生じるようなことがあってはいけない
採用したメタファを中心に設計を構成して、ユビキタス言語に取り入れること
XPの話も結構出てくるからそれも前提知識として抑えたほうがいいのかも
素朴なメタファは使うのをやめろ(?)(素朴=ドメインそのもの)
責務のレイヤ
個々のオブジェクトにも責務が割り当てられているが、責務駆動設計はそれよりも大きな規模に対しても適用される
巨大なモデルに一貫性を与えるには、責務を割り当てる際に何らかの構造を課すことが有効である
モデルの中にある概念上の依存関係と、変化のスピードを調べること
ドメインに自然な階層が認められたら、それに幅広い抽象的な責務を割り当てること
責務のレイヤ化に最適なのは緩やかなレイヤ化システムで、上位のコンポーネントは下位の階層であればどのコンポーネントにもアクセスできる
詳細レベルの設計には選択肢があるが、責務のレイヤで一貫した意思決定を行えば、仮に詳細レベルで多少分かりづらくなったとしても全体としての設計は理解しやすくなる
- レイヤはビジネスの優先事項を明らかにしなければならない
- 上位層にある概念は下位層を背景とした意味を持ち、下位層はそれ自体で独立していなければならない
- 別々のレイヤにあるオブジェクトを見たときに、変化のスピードや原因が別々であればレイヤは両者を切り分ける場所となる
レイヤの例
- 意思決定支援: どのように活動し、どのようなポリシーを設定スべきか
- ポリシー: ルールや目標はなにか?
- 確約: 何を約束したか?ポリシーと業務の性質を併せ持つ
- 業務: なにがなされたか?
- 潜在能力: 何ができるか?人間を含む組織のリソース
保険のような業務が潜在能力の元となっているようなシステムでは確約が前面にでてくる
レイヤ化は4~5層にとどめておくのが無難
知識レベル
知識レベルとは他のオブジェクトグループがどうふるまうべきかを記述するオブジェクトグループである
オブジェクト自体をユーザーが設定できるような設計のとき、レイヤを分ける
知識レベルはリフレクションパターンをドメイン層に適用したものだが、対象はプログラミング言語ではなくモデルである
知識レベルは完全な汎用性を目指してはおらず、オブジェクトとその関係性の集合に関する非常に特殊な制約が一式あれば良い
アプリケーションの業務に関する責務を持つベースレベルと、ソフトウェアの構造とふるまいに関する知識を表すメタレベル
オブジェクト間の関係を表すからと言って何でもかんでも知識レベルに置くのではなく、業務は区別して通常のモデルに入れておく
着脱可能コンポーネントのフレームワーク
インターフェースや相互作用にある抽象化されたコアを蒸留し、そのインタフェースの多様な実装を自由に置換できるようにするフレームワークを作成すること
シリコンウエハ製造のソフトウェアみたいにそのコンポーネントのインタフェースを実装していれば、組み込めて他の実装とは無関係に動かすことができるようなフレームワークを定義する
抽象化されたコアを変更するには多大なコストがかかる
構造による制約をどの程度厳しくするべきか
ソフトウェアは実際の業務を反映させなければならないがレイヤの上下関係を無視して双方向の通信はさせない。
上位層に通知を出さないといけないときはイベントやデザインパターンを使うこと。
構造上の各ルールは開発を容易にするものでなければならない
ふさわしい構造へのリファクタリング
大規模な構造はアップフロントなウォーターウォール開発からではなく、イテレーティブなアジャイルプロセスによってのみ作られる
ミニマリズム
最初のうちはシステムのメタファや2層の責務のレイヤといった緩やかな構造を選択するのがよい
コミュニケーションと自己規律
大規模な構造をプロジェクトのユビキタス言語に組み入れて、その言語を全員で執拗に鍛えることが不可欠
最高性によってしなやかな設計がもたらされる
構造が変化するたびに、新しい秩序を忠実に守るためにシステム全体を変更しなければならない
構造は変化させればさせるほど、新たな変化への対応が容易になる
蒸留によって負荷が軽減される
蒸留の原則とより深い洞察に向けたリファクタリングの原則は大規模な構造それ自体にも適用される
大規模な構造と境界づけられたコンテキストを組み合わせる
単一の境界づけられたコンテキストの中でもレイヤをうまく使うことで複雑さの限界を押し上げることができる
レガシーシステムでもレイヤのどこを占めるかを特定し、腐敗防止層が他のコンテキストにサービスを提供することで保てる
まず評価する
- 現在のコンテキストマップを描く
- プロジェクトでの言語の使われ方に注意を払い、ユビキタス言語を設定する
- 何が重要であるかを理解する。コアドメインは識別されているか?ドメインビジョン声明文はあるか?それを書くことはできるか?
- プロジェクトで使われている技術はモデル駆動設計に向いているか?
- チームの開発者は必要な技術的スキルを持っているか?
- 開発者はドメインについてよく知っているか?ドメインに関心を持っているか?
誰が戦略を策定するのか
権力のあるチームが決めたアーキテクチャが降りてくる、という天からの知恵スタイルはだめ
アプリケーション開発から現れる構造
優れたコミュニケーション能力を持つ自己規律型のチームは中央の権威がなくても機能し、進化する秩序に従って共有された原則の集合を生み出される
個人やチーム内の小さなグループに大規模な構造を監督する責任を追わせると構造の統一を維持しやすくなる
非公式なリーダが実践的な開発者である場合うまくいく。リーダは調停役や伝達役であってアイデアの唯一の源泉でないのがよく、その人は依然として開発チームの1メンバである
開発チームにはプロジェクト全体似影響することになる設計上の意思決定を行う才能のある人が少なくとも数人はいなければならない
顧客に焦点を合わせたアーキテクチャチーム
チームメンバが開発に対する真の協力者となり、開発者と一緒にパターンを見つけ、さまざまなチームと実験して蒸留に到達し、自ら手を動かす
戦略上の意思決定を行うために欠かせない6つのこと
-
意思決定はチーム全体に伝えなければならない
象牙の塔のアーキテクトは無視されたり抜け道を作られることも多い
どんなシステムであっても経営者が与える権威より、開発者が戦略に対して持つ実際の関係を重視すること -
意思決定プロセスはフィードバックを吸収しなければならない
アプリケーション開発チームが一番プロジェクトの要求とドメインの概念に対する深い理解を持っているのだから、それを吸い上げて設計に反映させなければならない -
アーキテクチャチームが最も優秀な人材をすべて吸い上げてはならない
すべてのアプリケーションチームにアーキテクトを入れるのが不可欠で、ドメインエキスパートも必要だ -
戦略的設計にはミニマリズムと謙虚さが必要である
上からのアーキテクトは障害になりやすいので、自生しながら、設計を特に明確にするものだけを含むように削ぎ落とされた構成原理とコアモデルを作り出さなければならない -
オブジェクトはスペシャリストだが、開発者ジェネラリストである
オブジェクトは単一の責務を任されるが、開発者をそのようにしてはいけない
開発者がフレームワークを試し、アーキテクトがアプリケーションコードを書く
戦略的設計と普通の設計を分けているが、人を分けるわけではない
過度の専門化はドメイン駆動設計から推進力を奪ってしまう
-
素人にも分かるフレームワークを作成してはならない
設計ができるほどには賢くないなら、ソフトウェア開発を担当させてはならない -
マスタプランに注意すること
変化によってマスタプランは失敗する。マスタプランは全体性は正確すぎ、詳細には不正確
設計が成功しているかどうかは、必ずしも均衡を保っているかどうかで評価されるわけではない