『エリック・エヴァンスのドメイン駆動設計』読書メモ

TODO: 『実践ドメイン駆動設計』を読んでまとめなおす。

少し前に話題になっていたDDDは『実践ドメイン駆動設計』で述べられている、より最近の知見に対するものが多い感じ(ヘキサゴナルアーキテクチャ等)。そんなこともわかっていなかったのでエヴァンス本を読んでよかった。

第1部

ドメイン駆動設計とは、ビジネスで本当に役に立つソフトウェアを解決するためのパターンランゲージ

その基本は開発者とドメインエキスパートが同じ言葉(ユビキタス言語)で会話し、その言葉でモデリングすること。そしてモデルと実装は完全に一致させること。

開発者はドメインに関係ない技術的な問題に取り組むことを好みがちだが、設計も高度な能力が要求されるやりがいのある仕事である。

第2部 モデル駆動設計の構成要素

第4章 ドメインを隔離する

  • モデル要素を見た際に一つの体系として理解できるようにすべき(関心の分離)。そのためにドメインオブジェクトを他の機能から切り離す
  • ドメインオブジェクトを他の機能から切り離す技術: レイヤ化アーキテクチャ

レイヤ化アーキテクチャ

  • 各レイヤがプログラムにおける特定の側面だけを専門的に扱うようにソフトウェアシステムを分割する
    • 各側面の凝集度が高まり理解しやすくなる
    • 各側面は別々の速度で進化して別々の要求に対応する傾向があるため、レイヤに隔離することで保守にかかるコストを下げられる
    • 分散システムにも配置しやすくなる
  • レイヤ内の要素は同じレイヤの他の要素か、その下にあるレイヤの要素にしか依存しない
  • 重要で凝集度の高い側面をレイヤ化することが大切だが、どうやれば良いのか?=> うまくいっているアーキテクチャのほとんどは以下の4つに分けている (上のレイヤから順に列挙)
    • ユーザインタフェース層(プレゼンテーション層)
      • ユーザに情報を表示し、ユーザのコマンドを解釈する責務を負う
      • アプリケーション層と厳密に区別しないこともある
    • アプリケーション層
      • ソフトウェアが行うことになっている仕事を定義し、ドメインオブジェクトが問題を解決するように導く
      • ビジネスルールや知識を含まず、やるべき作業を調整するだけ。実際の処理はドメイン層に委譲
      • ビジネスの状況を反映する状態(??)は持たないがユーザやプログラムが行う作業の進捗を反映する状態を保つことは可能
      • 他システムのアプリケーション層と相互作用する
      • ユーザインタフェース層と厳密に区別しないこともある
    • ドメイン層(モデル層)
      • ビジネスソフトウェアの核心
      • ビジネスの概念、ビジネスが置かれた状況に関する情報、ビジネスルールを表す責務を負う
      • ビジネスの状況を反映する状態はこの層で制御・使用される
      • ビジネスの状況を反映する状態を格納するという技術的な詳細はインフラストラクチャに委譲
    • インフラストラクチャ層
  • ドメイン層を分離して初めてモデル駆動開発が可能になる
  • ドメインオブジェクトがドメインモデルを表現する責務に専念できれば、本質的なビジネスの知識を捉えて機能させることができるようにモデルを進化させられる

例: オンラインバンキングの機能をレイヤに分割する

提供する機能
  • 資金振替
    • 2つの口座番号と金額を入力するか選択した後に送金を開始する
ビジネスルール

「全ての振り込みには、それに対応する引き落としがある」

ビジネスルールに関する責務を負っているのはドメイン層。アプリケーション層ではない。

レイヤを関係づける

  • ここまではレイヤを分離することでプログラムの各側面の設計が改善されるという話
  • (とはいえプログラムとして機能するためには)レイヤ同士は(どこかで)繋がっていなければならない
  • レイヤ分離の恩恵を失わずにレイヤ同士を繋げたい => 様々なパターン
  • レイヤ同士は疎結合であるべきで、設計の依存関係の向きは1方向
    • 上位のレイヤが下位のレイヤにある要素を直接使用・操作するのはOK
    • 下位のレイヤにあるオブジェクトが上位と通信するには? => コールバックパターンやオブザーバパターン
  • ユーザインタフェースをアプリケーション層とドメイン層に結びつけるパターンの原型 => MVC
  • インフラストラクチャ層はドメイン層におけるアクションを開始させることはない
    • ドメイン層より下位にあるため、対象ドメインについて特定の知識を持っていてはならない
    • 技術的な機能を提供するそうであるため、多くの場合サービスとして提供される
      • 例: インフラストラクチャ層によるメッセージ送信インタフェースの提供。アプリケーション層はメッセージをいつ送信するかは知っているが、それをどのように行うかまでは知らずに済む
  • 技術的コンポーネント(「アーキテクチャフレームワーク」)によっては、インフラストラクチャはサービスの形では提供されない
    • 例: (全てのドメインオブジェクトに抽象基底クラスを提供するなど)他のレイヤの基本的な機能を直接支援するように設計されている
    • 例: (MVCの実装など)他のレイヤを関係づけるための仕組みを提供する

アーキテクチャフレームワーク

  • フレームワークを適用すると多くの実装がフレームワークによる影響を受けるが 、その場合でもドメインモデルを表現し、重要な問題を解決するためにそのモデルを使用するような実装を構築しなければならない
    • そのためにはフレームワークの一部の機能は使わないというような選択をしても構わない

ドメイン層はモデルが息づく場所

  • ドメインの実装を隔離することがドメイン駆動設計の必要条件
    • 隔離することで設計と実装を一致させることが可能に
  • ドメインモデル: 概念の集合
  • ドメイン層: モデルとモデルに直接関係する設計上のすべての要素が現れる場所。ビジネス層の設計と実装によって構成される
  • モデル駆動設計: ドメイン層によってソフトウェアが構成される

利口なUI「アンチパターン

  • レイヤ化アーキテクチャに従ってユーザインタフェース、アプリケーション、ドメインを分離することはよく試みられるが、ほとんど達成されることはない(!)
  • では実際に採用されているのは? => 洗練とは程遠い設計アプローチ。これを「利口なUI」と呼んでいる
  • 利口なUIはドメイン駆動設計のアプローチとは両立しない
  • 利口なUI「アンチパターン」 => 利口なUIが適用されない状況。この状況ならドメイン駆動設計のアプローチが適用可能
  • 以降しばらくは利口なUIについて、ドメイン駆動設計との違いを示し、ドメイン駆動設計が正当化される状況を明らかにする

  • ドメイン駆動設計が最も効果があるのは大掛かりなプロジェクトでかつ強力なスキルが必要
    • 未熟なチームでは習得できないし、小規模開発では各レイヤを管理するオーバヘッドがスケジュールを圧迫する
  • そのため以下のような「利口なUI」が正当なパターンとなる文脈もありうる(ドメインユーザインタフェースの分離を行わないとドメイン駆動設計の方法論が適用困難なので、ドメイン駆動設計の文脈ではアンチパターン
  • 利口なUIの利点(利点を明らかにすることで、アプリケーションとドメインを分離する理由と、どういうときに分離したくないのかを理解することが目的)
    • 単純なアプリケーションの場合、生産性が高い
    • 学習コストが低い
    • 要求分析が不足していても、プロトタイプを公開して要望を満たすように変更していくことで克服可能
    • アプリケーションが小さな機能で互いに分離しているので、小さなモジュールの納品スケジュールは比較的正確に計画可能
    • 関係データベースにより、データレベルでの統合が実現される
    • 4GLツールがうまく機能する(? 4GL: Fourth-generation programming language ?)
    • 変更による影響が特定のユーザインタフェースに限定されるため、作り替えが容易
  • 利口なUIの欠点
    • アプリケーションの統合は困難で、データベースを経由させるしかない
    • 振る舞いが再利用されない。ビジネスの問題が抽象化されない
      • ビジネスルールは適用先の操作それぞれで複製される
    • 抽象化されないためリファクタリングの選択肢が制限される
    • 複雑さによってすぐに覆いつくされてしまうので(??)単純な機能の追加しかできない
  • 利口なUIを適切な状況下で適用すれば、他のアプローチで必要なオーバーヘッドを避けられる
    • やり遂げられない高度な設計アプローチに手を出すのはやりがちな間違い
    • プロジェクトに対して過剰に複雑なインフラストラクチャを構築するのもやりがちな間違い
  • 利口なUIを適用するようなアプリケーションにとっては、Javaなどの柔軟な言語は過剰。4GL風のツールを選ぶべき
    • 利口なUIから他の設計アプローチに移行するにはアプリケーションを置き換えるほかないため、割り切って利口なUIに適合した開発ツールを選択すべき
    • 逆にモデル駆動設計から利口なUIに移行する方法もアプリケーションの置き換えのほかないため、最初からドメインレイヤを隔離したモデル駆動をしっかり行わないと行き詰まってしまう

  • アーキテクチャドメインに関連するコードを隔離して、凝集度が高いドメインの設計が、システムの他の部分と疎結合できるようにしているなら、そのアーキテクチャはおそらくドメイン駆動設計を支えられる」
  • 「アプリケーションが複雑で、モデル駆動設計に取り組むつもりなら、必要な専門家をそろえたうえで、利口なUIを避けるべき」

その他の隔離

  • 扱わなければならない他のドメインコンポーネントが自分のモデルに統合しきれていないといったいくつかの要因がモデルの有用性を損なってしまう問題への対応:
    • 境界付けられたコンテキスト
    • 腐敗防止層
  • 複雑なドメインモデルはそれ自体が扱いにくいという問題への対応: 蒸留
    • ドメインについての本質的な概念を他の詳細から解放

これらについて議論するのはまだ先。次はドメインモデルと実装をともに進化させるための土台を見ていく。

まとめ

  • ドメイン駆動設計では、モデルを体系的に理解し、ドメインの設計に集中できるようにするために、ドメインを他の機能(ユーザインタフェース、アプリケーション、インフラストラクチャ)から隔離することが重要
  • ドメイン駆動設計は大規模・複雑なソフトウェア開発向きで、かつ強力なスキルを持った専門家が必要
  • 未熟なチームや小規模開発では、アプリケーションを細かい機能に分割し、ユーザインタフェースビジネスロジックを持たせるような「利口なUI」が有効な場合もある
  • ドメイン駆動設計と「利口なUI」は両立できないので、どちらかに振り切って設計・ツール選定をすべき
  • 「利口なUI」の他にもモデルの有用性を損なってしまう状況があり、その対処についても本書で議論するがそれはまだ先の話
  • ドメインモデル自体が複雑で扱いにくい場合もあり、その対処についても本書で議論するがそれもまだ先の話

第5章 ソフトウェアで表現されたモデル

この章ではモデルを構成する詳細な個々の要素を扱う。具体的には以下についてドメイン駆動設計の文脈でとらえ、議論する。

  • 関連を設計して無駄をなくすことについて
  • モデルを表現する3パターンの要素: エンティティ、値オブジェクト、サービスについて
    • あるオブジェクトが、状態が異なったり、別々の実装をまたいだりしても追跡されるような連続性と一意性を持ったものを表現している => エンティティ
    • あるオブジェクトは、何かの状態を記述する属性である => 値オブジェクト
    • ドメインの側面によってはオブジェクトとしてよりも、アクションや操作として表現した方が明確になる場合 => サービスとして表現
      • サービスは操作に専念し、状態は管理しない
  • 関係データベースに格納する場合など、オブジェクトモデルを純粋な形で保てない状況への対処
  • モジュールについて
    • 「設計上のあらゆる意思決定は、ドメインについての何らかの洞察によって動機づけられなければならない」
    • モデル駆動設計ではモジュールもモデルの一部であり、ドメインにある概念を反映する

関連

  • モデルで示される(一対多、多対多といった)関連はソフトウェアにも現れる
  • 多対多・双方向の関連は現実世界では一般的だがソフトウェアの実装と保守が複雑になるうえ、関係の性質について理解するためには役立たない
  • => 関係性をできる限り制限することが重要
    • 関連をたどる方向を一方向に強制
      • 相互依存関係が減り設計がシンプルに
      • ドメインを理解することで、双方向の関連のどちらの方向性が本来重要であるのか明らかになる場合がある(ので初期のモデルに固執せず双方向性の排除を検討すべき)
    • 限定詞を付加して多重度を減らす
      • 例: 基本的に1つの国にはある時点に1人の大統領しかいないので、任期というルールをモデルに組み込み多重度を減らす
    • 本質的でない関連の除去

エンティティ(Entities)(別名 参照オブジェクト(Reference Objects))

多くの物事が定義されるのは同一性によってであり、属性によってではない。例えば人は月日がたてばあらゆる属性が変化するが同一性がある。

  • オブジェクトの中には、主要な定義が属性によってなされないものがある
    • 属性が異なっていても他のオブジェクトと一致しなければならないことがある
    • 同じ属性を持っていたとしても他のオブジェクトと区別しなければならないオブジェクトもある
  • 同一性によって定義されるオブジェクト: エンティティ
    • エンティティの根本的な概念は抽象的な連続性
    • ライフサイクルを通じた連続性を持ち、アプリケーションのユーザにとって重要な区別が属性から独立してなされるものはすべてエンティティ
  • 例: 同じ日に同じ口座に対して同じ額の預け入れが2つあっても、この2つは別々の取引である => 取引は同一性を持つエンティティ
  • あるオブジェクトが属性でなく同一性によって識別されるなら、モデルでもその同一性を第一としてそのオブジェクトを定義すべき
    • 形式などに関係なく各オブジェクトを識別する手段を定義
      • 識別手段は様々あるが、モデルにおける同一性の区別と一致させること
      • 各オブジェクトに対して結果が一意となることが保証される操作を定義(IDを付与)するなど
  • エンティティの責務: 振る舞いが明確で予測可能になるように連続性を確立する
    • 本質的な特徴・振る舞い・振る舞いが必要とする属性だけを持つようにする
      • 本質的な特徴: エンティティの識別や検索・突き合わせに使用されるもの(ID、名前など)
      • 本質的な振る舞い・振る舞いが必要とする属性については他のオブジェクトに移動できないかも調べること。移動させるかどうかはドメインにおける識別や突き合わせが実際にどのように行われるか次第

値オブジェクト(Value Objects)

  • 「何」であるかが関心あることで、「誰」であるかや「どれ」であるかは問われないような設計要素を表現するオブジェクトを値オブジェクトと呼ぶ
    • 色、文字列、数など
    • 住所は?使われ方による。同じ住所に複数人が住んでいることを想定する。通販ではその複数人が別々に注文をしても、その複数人が同じ場所に住んでいることを認識する必要はなく、ただ指定された住所に送れば良い。このような場合、住所は値オブジェクト。他方、電力サービスの目的地として住所が使われる場合は、同じ住所に住む複数人が別々に依頼を出した場合にその複数人が同じ場所に住んでいることを認識する必要がある。このような場合、住所はエンティティ。ただし後者の場合でも例えば住居エンティティが存在し、住居エンティティに関連づける形で住所が存在する場合には、住所は値オブジェクトになりうる。
  • 属性しか関心の対象にならない要素は値オブジェクトにすること
    • なんでもエンティティにしてしまうとシステムが複雑になるし、どのオブジェクトも似たような見た目になりモデルにおいて何が重要かわからなくなる
  • 値オブジェクトには属性の意味、属性に関係した機能を持たせ、同一性は与えない
  • 値オブジェクトは不変なものとして扱う

値オブジェクトを設計する

  • 不変にする
  • 多数のインスタンスを作成するより1つのインスタンスを共有することでパフォーマンスを改善できる場合がある。逆に分散システムなどではインスタンスを共有することで通信が増えパフォーマンスが悪くなるかもしれない。
  • インスタンスの共有は以下のような場合に限定するのが最適
    • データベース内でスペースやオブジェクトの数を節約することが極めて重要な場合
    • 通信オーバーヘッドが低い婆愛
    • 共有オブジェクトが完全に不変である場合
  • 以下のような特殊な場合には可変にすることもあるかもしれない
    • 値が頻繁に変化する場合
    • オブジェクトの生成や削除が高コストな場合
    • 値の変更ではなく値オブジェクトの置き換えによって変更を実現することでクラスタリングが妨げられる場合
    • 値を共有することがあまりない、あるいは技術的理由から値の共有をしない場合

  • エンティティであることを宣言して同一性の演算をプログラミング言語やツールの力で強制することは(現在のところ)できない
  • (値オブジェクトで実現したい)属性やオブジェクトの不変性は言語や環境によって宣言できたりできなかったりする

モデルで行う区別の多くは実装において明示的に宣言することはできない。これらの区別を言語が直接サポートしないからといって区別しなくて良いというわけではないし、実装を以って暗黙的に区別することは可能。ただ、実装によって暗示されているに過ぎないルールを守るためには規律が必要。命名規則や多くの議論など。

サービス(Services)

モデルにおいて独立したインタフェースとして提供される操作 => サービス

優れたサービスの3つの特徴:

  • 操作がドメインの概念に関係していて、かつその概念をエンティティや値オブジェクトで表現するのが適切でない
  • ドメインモデルの他の要素の観点からインタフェースが定義されている
  • 操作に状態がない
    • オブジェクトが内部状態を持たない
    • グローバルな情報の変更はありうる

ドメイン層に属するサービスを他のレイヤのサービスと区別すること、明確な区別を保つように責務を分解する

  • インフラストラクチャ層のサービス => 純粋に技術的
  • アプリケーション層のサービス => インフラストラクチャ層のサービスと協力

例: 銀行業アプリケーション

  • 取引をスプレッドシートファイルにエクスポートする => アプリケーションサービス
    • (銀行業ドメインにおいて)ファイル形式には意味はないし、ビジネスルールも関係していない
  • 資金をある口座から別の口座に振り替える機能 => ドメインサービス
    • 重要なビジネスルールを組み込んでいる
    • 「資金振替」は意味のある銀行用語
    • サービス自体は2つの口座オブジェクトに処理のほとんどを依頼するかたちになるが、口座オブジェクトに「振替」の操作を入れてしまうと扱いにくい。振替は2つの口座と何らかのルールを伴うため。

サービスをレイヤに分割すると:

  • アプリケーション: 資金振替アプリケーションサービス
    • 入力(XMLリクエストなど)を理解する
    • 処理を実行するよう、ドメインサービスにメッセージを送信する
    • 確認を待つ
    • インフラストラクチャサービスを使用して、通知を送信することを決定する
  • ドメイン: 資金振替ドメインサービス
    • 必要な口座オブジェクトおよび元帳オブジェクトとやり取りし、適切な引き落としと振り込みを行う
    • 結果(振替の可否など)の証跡を出力する
  • インフラストラクチャ: 通知送信サービス
    • アプリケーションの指示に従って、電子メールや手紙、その他のメッセージを送信する

サービスによるドメイン層のインタフェース粒度の制御

  • サービスはドメイン層のインタフェースの粒度を制御する手段としても有用
  • ドメインオブジェクトの粒度が細かすぎると、ドメイン層からアプリケーション層に知識が流出してしまう
    • ドメインオブジェクトの振る舞いを組み合わせることで問題を解決するのがアプリケーション層
    • ドメインオブジェクトの粒度が細かいと、アプリケーション層でたくさんのドメインオブジェクトを詳細なレベルで相互作用させることになる。つまり実質的にアプリケーション層が詳細に関する知識を持ち、処理を行うかたちになる
  • サービスは用途の広さよりもインタフェースのシンプルさを優先する。そうすることで再利用性が高まるし分散システムでも扱いやすくなる。

モジュール(Modules)(別名 パッケージ(Packages))

モジュールもモデルの一部。いくつかのクラスを一つのモジュールに入れるということは、それらをひとまとめに考えるよう伝えているということ。

技術的な評価と概念上の関係性の明確さはどちらも達成されるべきだが、技術的な視点では分割すべきでも、分割しないほうが概念的に明確であるといった場合は概念的な明確さを優先するほうが良い。

モジュールもリファクタリングを行っていくべき。大変でもやらなければより大変になるのでなんとかやるしかない。

インフラストラクチャ駆動パッケージングの落とし穴

  • パッケージングに際し非常に有効なフレームワーク標準: レイヤ化アーキテクチャ
  • 落とし穴: ティア化アーキテクチャ(tiered architecture)
    • 技術駆動でパッケージングするアーキテクチャ。データのカプセル化程度ならある程度有用だが...
    • 概念的には一つのはずのオブジェクトが複数のティアに分割されてしまい、コードがモデルを明らかにしなくなる
    • バラバラになっているティアを頭の中で一つにするのは大変なので、アーキテクチャが強制する分割に加えて意味的な分割をしようとはしなくなる・分割しても理解できなくなる。
    • 別々のサーバにコードを分散させるには有用だが、本当にそうするのか?

技術的な分割ルールは本質的、あるいは開発の助けになる最小限のものにすべき。別々のサーバにコードを分散させるなどの理由がない限り、単一の概念オブジェクトを実装するコードはすべて(同一オブジェクトにはならなくても)同一モジュールにまとめること。

ドメイン層を他のコードから分離するためにパッケージングをすべき。せめてパッケージング可能にしておくこと。ただしコードが宣言的設計にって(自動的に)生成される場合はこの限りでない(のちの章で説明)。

ドメインオブジェクトに、それが表現している概念と密接に関係しないものを追加するのは避ける。ドメインオブジェクトはモデルの表現に専念すべき。ドメインに関する実行すべき責務や管理すべきデータはそのドメインオブジェクトでなく補助的なオブジェクトで実現する(のちの章で説明)。

モデリングパラダイム

オブジェクト指向設計が主流。シンプルで理解しやすいが、ドメインの知識をとらえ表現できるレベルの力はある。また開発者コミュニティや設計文化も成熟しているし、インフラストラクチャやツールも成熟しており恩恵を受けられる。

とはいえドメインによってはオブジェクト指向によるモデリングが不自然で、別のモデリングパラダイムを用いるほうが自然な場合もある(高度な数学的ドメインなど)。この場合もドメイン駆動設計は可能。

ドメインの中には他のパラダイムのほうがはるかに簡単に表現できる場所があるものだが、僅かなら我慢すればよいし、逆にそういった場所が大半ならパラダイムを切り替えてしまうとよいかもしれない。

また相互依存関係が少なければ、別のパラダイムでサブシステムを作ってカプセル化することも可能。とはいえパラダイムを混在させるならより一層モデル駆動設計に忠実でないと破綻する。可能な限り支配的なパラダイムにある選択肢を検討すべき。

ただし最も一般的な非オブジェクト技術である関係データベースはパラダイムの混在の中でも特殊。オブジェクトそのものを構成するデータの永続ストレージとして機能するため。オブジェクトデータを関係データベースに格納することについてはのちの章で議論する。

第6章 ドメインオブジェクトのライフサイクル

  • 生成されアクティブになる
  • アクティブな状態から削除される
  • アクティブな状態で修正が加えられる
  • アクティブな状態からデータベース表現として格納される
  • データベース表現から再構成されアクティブになる
  • アクティブな状態からデータベースまたはファイル表現としてアーカイブされる
  • アーカイブから削除される

寿命が長く、他のオブジェクトと複雑に相互作用するドメインオブジェクトの課題:

  • ライフサイクルを通じて整合性を維持すること
  • ライフサイクル管理が複雑でも、モデルが侵食されないようにすること

これらの課題に対応する3つのパターンが以下。

集約(Aggregates)

ルートとなるエンティティをたて、関連するオブジェクトを集めて境界を作る。境界外からはルートエンティティの参照のみ保持できるようにする。内部のエンティティへの参照は一時的な使用のためだけなら渡してもよい。また値オブジェクトのコピーであれば、単なる値で集約とは関連がないため渡してよい。

ルートエンティティはグローバルな同一性を持つが、集約内部のルート以外のエンティティは、その集約内部でのみ識別可能なレベルのローカルな同一性があればよい。

ルートエンティティが不変条件をチェックする最終的な責務を負う。不変条件とはデータが変更される際に常に維持されなければならない一貫性のルールのこと。これは集約のメンバ間の関係も含む。

例)

  • 集約ルート: 自動車
  • 集約内のエンティティと値オブジェクト
    • タイヤ
      • 自動車というコンテキスト内では識別可能にしたいが、特定のタイヤを見つけてからそのタイヤがどの自動車に装着されているか調べる人はいない
    • 位置
    • 車輪

後述するファクトリとリポジトリも集約を対象としている。

ファクトリ(Factories)

ドメインオブジェクトの生成や再構成のためのパターン。

複雑なドメインオブジェクトの生成(例えば複合オブジェクトなど)は、生成されるオブジェクトの責務には合わず、分離したい。しかし生成の責務をクライアントオブジェクトに移してしまうと、クライアントがオブジェクトの内部構造を知る必要が出てきてしまう。そのため複雑なドメインオブジェクトの生成もやはりドメイン層の責務ではあるものの、オブジェクトの生成と組み立ては実装のために必要だがドメインにおいて意味を持たない。

この問題を解決するために、より抽象的なオブジェクト構築の仕組みとしてファクトリをドメインの設計に追加する。ファクトリは他のオブジェクトの生成を責務とするプログラム要素である。

ファクトリの設計方法は数多くある(ファクトリメソッド、抽象ファクトリ、ビルダなど)が、ここでは詳細には触れない。

優れたファクトリに共通する要件:

  • 生成メソッドはそれぞれアトミックであり、生成するオブジェクトや集約の不変条件をすべて強制する。つまり一貫した正しい状態のオブジェクトを生成する。また不変条件が満たされずに生成が失敗した場合どうなるかを決めておくべき。
  • ファクトリは具象クラスでなく要求される型に応じて抽象化しなければならない

ファクトリとその場所を選択する

  • 集約ルートにファクトリメソッドを持たせる
  • 密接にかかわるオブジェクトにファクトリメソッドを持たせる
    • 例)証券取引口座に取引注文の生成を制御させる
  • 自然な置き場所がない場合は無理に他のオブジェクトに入れ込まず、独立したファクトリを作成する

コンストラクタがあればよい場合

  • クラスが型の場合
  • クライアントが実装に関心がある場合
  • オブジェクトの持つ属性をすべてクライアントが取得でき、コンストラクタ内でさらに別のオブジェクトの生成がネストされるようなことがない場合
  • 構築が複雑でない場合
  • コンストラクタがアトミックで一貫したオブジェクトを生成する場合

不変条件のロジックはどこへ置くべきか?

ファクトリは不変条件がすべて満たされていることを保証する責務を負うが、不変条件の検証は生成物に委譲することができるし、それが最善であることも多い。

ただし生成時のみチェックが必要で、アクティブな状態の間は適用されない不変条件ロジックであれば、オブジェクトが持っている必要はなく、ファクトリに置くのが理にかなっている。

リポジトリ(Repositories)

クライアントコードがデータベースを直接使用すると、モデルの機能は失われ、ドメインのルールがクエリのコードに埋め込まれたり失われてしまう。

そこでリポジトリパターンを使用する。

リポジトリはデータベースクエリやメタデータマッピングの仕組みをカプセル化し、クライアントコードからはあたかもオブジェクトがメモリ上にあるかのように、モデルの言葉で必要なものを要求できる、シンプルでドメインモデルと概念的に結びついたインタフェースを提供する。

またリポジトリを用いることによって、アプリケーションとドメインの設計を、永続化技術やデータベース戦略、データソースから分離できる。

リポジトリの実装は多岐にわたるが、以下は覚えておくべき。

  • 型を抽象化すること
  • クライアントから切り離す利点を生かすこと
  • トランザクション制御をクライアントに委ねること

また、データベースをオブジェクトの格納先としてみる場合は、データモデルとオブジェクトモデルを可能な限り近づけておくべき。関係モデルと近づけるためにオブジェクトの関係性を制限することや、オブジェクトとのマッピングを単純化するために正規化をある程度妥協したりすることは認められる。

ファクトリとの関係

ファクトリは新しいオブジェクトを生成する。一方リポジトリは古いオブジェクトを見つけ出す。データからオブジェクトへの再構成が必要になる場合は、データを取得するまでをリポジトリが行い、再構成はファクトリに委譲すればよい。このように明確に分離することで、ファクトリから永続化に関する責務を取り除くことができる。

また、「探して、なければ生成する」機能は避けるべき。おそらくエンティティと値オブジェクトの区別がついていない。これらを正しく区別すれば、このような機能が便利な場面はほとんどない。また通常、新しいオブジェクトと既存のオブジェクトを区別することはドメインにおいて重要で、これらを透過的に組み合わせてしまうとかえって混乱を招く。

第3部 より深い洞察へ向かうリファクタリング

第8章 ブレイクスルー

継続的なリファクタリングによってコードとモデルを改善するたびに、開発者の視界が明確になる。するとモデルの複雑さが大幅に解消し、かつ用途の広さや説明力が急激に増大する(より「深い」モデルになる)ことがある。これがブレイクスルー。

より深いモデルへのブレイクスルーが起こりそうなときは恐怖を感じることが多い。設計の大幅な変更を必要とし、リスクも大きいため。

ブレイクスルーは引き起こそうとしてもうまくいくものでない。適度なリファクタリングを何度も行えばブレイクスルーが「起こり得る」。わずかな改良をためらわずに行うこと。

第9章 暗黙的な概念を明示的にする

設計の中に暗に存在する概念に気づき、モデルの中で明示的に表現し、リファクタリングを繰り返すことで、より深いモデルへとつながるブレイクスルーが起こる土壌ができる。

概念を掘り出す

暗黙的な概念に気づくためにはどうしたらよいか:

  • ドメインエキスパートの使う言葉に注意する。複雑なものを簡潔に述べる用語がないか。開発者がドメインエキスパートに話す際、表現を修正されることはないか。開発者があるフレーズを用いると、ドメインエキスパートに困惑されることはないか。
  • ドメインに関係する文献を読む。

それほど明白でない概念をモデル化する方法

明示的な制約

複雑な制約は独立したメソッドに分解し名前を付けることで、設計上でも制約を明示できるようになる。

  • 制約を評価するために、本来であればオブジェクトの定義に合わないデータが必要
  • 関連するルールが複数のオブジェクトに出てきて、コードを重複させるか、同じ系統でないオブジェクトを継承しなければならない
  • 設計や要求に関する多くの会話が制約をめぐって行われるのに、実装では制約が手続型のコードの中に隠されてしまっている

このような場合は制約をくくりだすべき。

ドメインオブジェクトとしてのプロセス

サービスによって複雑なアルゴリズムカプセル化するほかに、アルゴリズムを独立したオブジェクトにするアプローチもある。プロセスの実行方法が複数ある場合にそれぞれをストラテジーとして表現するなど。ドメインエキスパートが話題に挙げるものは明示すべきプロセスで、そうでないものは隠蔽すべきプロセス。

仕様

ルール評価コードはドメイン層に入れるべき。しかしルール評価が複雑すぎて、どのエンティティや値オブジェクトの責務とも合致しない場合がある。

そのような場合は論理プログラミングにおける「述語」の概念を利用して、評価結果を返す独立したオブジェクトを作成するとよい。このオブジェクトが「仕様」。

第10章 しなやかな設計

深いモデルによりドメインにおける幅広いシナリオを表現できること、設計が容易に理解でき変更できること、どちらも実現するしなやかな設計をする決まった方法はないが、役に立つパターンを紹介する。

意図の明白なインタフェース

クラスと操作には効果と目的を表す名前をつけ、内部を理解する必要がないようにする。ユビキタス言語に従って名前付けすること。そうすれば意味を推測できる。

表明(Assertions)

操作の副作用が明確でない場合、抽象化の意味はなく、結局具体的な実行を追跡する必要が出てきてしまう。

操作の事後条件とクラスおよび集約の不変条件を明確に宣言することで、開発者が操作やオブジェクトを使用した結果を理解できるようになる。

言語が表明を直接サポートしていない場合はユニットテストを代わりに書く。

概念の輪郭(Conceptual Contours)

モデルや設計の要素の一つ一つが大きすぎると機能が重複する(必要な機能だけを切り出せない、あるいは見つけられないため)。かといって分割しすぎるとクライアントが無意味に複雑になりかねない。

設計要素(操作、インタフェース、クラス、集約)を凝集した単位に分割すること。概念的に意味のある機能単位を見つけるとよい。

独立したクラス(Standalone Classes)

相互依存関係があるとモデルと設計は理解が難しくなる。なるべく依存関係を取り除き低結合にすること。

閉じた操作(Closure of Operations)

依存関係を減らしつつ豊かなインタフェースを維持する方法。操作の戻り値の型が引数の型と同じにできる場合は同じにする。

攻める角度

設計全体に取り組むのは現実的でない。ではシステムのどの部分をしなやかにするかというと、サブドメインを切り取って取り組むとよい。設計の一部を本当にしなやかにするほうが、広く薄く行うよりよい。

可能な場合は、形式化された概念フレームワークを用いる。数学等。

第11章 アナリシスパターンを適用する

深いモデルとしなやかな設計のためにはドメインについて学ぶ必要がある。しかし対象によっては先人の知恵が使える。アナリシスパターンは活用すべき。

第12章 デザインパターンをモデルに関係づける

書籍『デザインパターン』に書かれたパターンの一部はドメインパターンとして使用可能。ただし先の書籍ではこれらのパターンは技術的なコンテキストで語られている。ドメインモデリングや設計といったより広範なコンテキストで適用可能ということで、ドメイン駆動設計ではこれらのパターンを技術的なデザインパターンと概念的なパターンの2つのレベルで同時に考えないといけない。

ストラテジー(Strategy)(別名 ポリシー(Policy))

技術的に動機づけられたプロセスでなく、ドメインにおいて意味を持つプロセスもあり、こちらにもストラテジーが適用可能。

プロセスの中で変化する部分を独立したストラテジーオブジェクトにくくりだす。ストラテジーデザインパターンに従って実装すること。

コンポジット(Composite)

コンポジットを構成するすべてのメンバを包括的に含む抽象型を定義する。

第13章 より深い洞察へ向かうリファクタリング

主要なポイント

  1. ドメインに馴染む。
  2. 常に、物事に対して違う見方をする。
  3. ドメインエキスパートとの対話を途切れさせない。

(以下、印象に残った個所を記載)

  • 車輪の再発明が常に必要とは限らない
  • ソフトウェアは開発者のものでもある。しなやかな設計の恩恵は開発者も受けられる。結果が予想しやすくなり変更が容易になるなど。
  • 「変更を完全に正当化できるまで待つのは、待ちすぎというものである」
    • ソフトウェア開発は変更による利益や変更しないことによるコストを正確に割り出せるようなプロセスではない
    • とはいえリリース前日にリファクタリングしたり、技術を披露するためだけの変更をしたりしてはいけない
  • ブレイクスルーが起きる状況は好機というよりむしろ危機に感じられるもの。モデルに不適切な部分があると「わかる」ため

第4部 戦略的設計

モデリングプロセスをスケールアップさせて、大規模なシステム、複雑なドメインに対応させる原則を説明する。

いくら規模が大きくても、ドメイン駆動設計が実装と結びつかないモデルを作ることはない。

モデルに集中しかつプロジェクトが行き詰らないようにするため、3つのテーマを考察する。それが「コンテキスト」、「蒸留」、「大規模な構造」。

第14章 モデルの整合性を維持する

  • 境界づけられたコンテキスト: モデルを適用できる範囲を定義する
  • コンテキストマップ: プロジェクトのコンテキストとコンテキスト間の関係を概観するためのマップ

境界づけられたコンテキスト(Bounded Context)

別々のモデルに基づくコードが組み合わされるとバグの温床になるし理解しにくくなる。

モデルが適用される際のあらゆる条件の集合がコンテキスト。モデルが適用されるコンテキストを明示的に定義すること。

継続的な統合(Continuous Integration)

多くの人が同一のコンテキストで作業するとモデルが分裂しがち。継続的にすべての作業をマージして一貫性を保ち、分裂の兆しを検知すること。

コンテキストマップ

プロジェクトのコンテキストとコンテキスト間の関係を概観するためのマップを作る。常にありのままの状況を表すこと。

境界づけられたコンテキスト間の関係

コンテキスト間でやりとりする必要は当然出てくる。そのパターンを紹介する。

共有カーネル(Shared Kernel)

ドメインモデルのサブセットを2つのチームが合意の上共有する。サブセットに対して変更を加える際は他チームに相談の上行う。統合の際には両方のチームのテストを実行する。

顧客/供給者の開発チーム(Customer/Supplier Development Teams)

2チーム間に顧客/供給者関係を確立する。下流のチームが顧客として要求し、上流チームが供給する。インタフェースを検証する受入テストは共同で開発すること。そうすればテストによって上流チームは下流への副作用を心配することなく変更を行える。

下流(顧客)の要求が優先。上流チームにとって下流の要求にこたえる動機づけがないとうまくいかない。

順応者(Conformist)

2チーム間に上流/下流の関係があるが、上流が下流の要求に応えない場合のパターン。

  • 上流の機能を使用しない
  • 上流との依存関係を維持する必要があるが上流の設計を使うのが(品質の都合等で)難しい場合、独自のモデルを作る。この場合、下流はモデルの変換層にも責任を持つ必要が出てくる。具体的には腐敗防止層によって下流のモデルを守るといったこと。
  • 上流の設計品質がそれほど悪くない場合=>順応者

上流チームのモデルに隷従する。これによってコンテキスト間の複雑な変換を回避する。

下流のモデルが制限される可能性はあるが、結合が大幅に簡略化されること、隷属により上流チームとユビキタス言語を共有することになるためコミュニケーションしやすくなる等の利点はある。

腐敗防止層(Anticorruption Layer)

他システムとのやり取り、および内部モデルのための変換を隔離する。

別々の道(Separate Ways)

結合しない。

公開ホストサービス(Open Host Service)

サブシステム間の結合それぞれに対して変換サービスを作るのではなく、サブシステム自身が自身にアクセスするためのプロトコルを定義・公開する。

公表された言語(Published Language)

ドキュメント化された共有言語(XML等)を使用してやりとりする。

モデルコンテキスト戦略を選択する

より大きな境界づけられたコンテキストが好まれる状況

  • 統一されたモデルで処理させる量が増えることで、ユーザタスク間のフローがスムーズになる
  • 一貫性のあるモデルが1つあるほうが理解しやすい
  • モデル間の変換が難しい・不可能
  • 共有された言語によるチームのコミュニケーション促進

より小さな境界づけられたコンテキストが好まれる状況

  • 開発者間のコミュニケーションオーバーヘッドが減少
  • 継続的な統合はチームとコードベースが小さいほうが容易
  • 大きなコンテキストで必要となる、用途の広い抽象モデルを見出すための能力がない
  • 特殊な要求を満たすよりは別のモデルを作ったほうが良い

(以下省略)

第15章 蒸留

ドメインの中でも特に価値ある部分「コアドメイン」に集中したい。

蒸留は混ざり合ったコンポーネントを分離し、本質を役立つ形式で抽出すること。

コアドメイン

アプリケーションにとって中心的なモデルの部分がコアドメインを構成する。コアドメインは最大の価値を付加すべき場所。

コアドメイン選択・作成を外部の専門家を短期で雇って行うのはうまくいかない(教育や指導目的なら価値はある)。安定したチームがドメイン知識を累積し、設計スキルも磨くことで行っていくしかない。

(以下かなり省略)

第16章 大規模な構造

(ここだけで話題が多岐にわたるのでごく軽く)

システムのメタファ(System Metaphor)

メタファを使うと理解の共有が楽だが、不適切でないか絶えず調べなおすこと。

責務のレイヤ(Responsibility Layers)

ドメインに自然な階層がある場合、階層を表す幅広い抽象的な責務を割り当てる。

知識レベル(Knowledge Level)

リフレクションパターンをドメイン層に適用したもの。

着脱可能コンポーネントフレームワーク(Pluggable Component Framework)

インタフェースや相互作用にある抽象化されたコアを蒸留し、そのインタフェースの多様な実装を自由に置換できるようにするフレームワークを作成すること。同様に、抽象化されたコアのインタフェースを経由して操作される限り、あらゆるアプリケーションがこのコンポーネントを使用できるようにすること。

第17章 戦略をまとめ上げる

(戦略的設計のための原則・テクニックの組み合わせが紹介されている)

戦略的設計上の意思決定を行うために欠かせない6つのこと

  • 意思決定はチーム全体に伝えなければならない
  • 意思決定プロセスはフィードバックを吸収しなければならない
  • 計画は進化を許容しなければならない
  • アーキテクチャチームが、最も優秀な人材をすべて吸い上げてはならない
  • 戦略的設計にはミニマリズムと謙虚さが必要である
  • 同じことが技術的なフレームワークにも当てはまる

結論

本書にあるテクニックをすべて採用するプロジェクトはないだろうが、ドメイン駆動設計を真剣に行っているプロジェクトはすべて、対象となるドメインを理解し、その理解をソフトウェアにおいて具現化することを重視しているはず。それがすべての前提。