成瀬允宣さんの「ドメイン駆動設計入門」勉強メモ/備忘録です。
概要
DDDのコンセプト
- ビジネスの問題を解決するためにビジネスの理解を進め、ビジネスを表現する。
- ビジネスとコードを紐付けて継続的かつ反復的な改良を施せるように枠組みを作る
用語の大別は2種類
- ソフトウェアにとって重要な概念を抽出するためのモデリング
- 概念を実装に落とし込むためのパターン
ドメイン駆動設計とは何か?
DDDとは、ドメインの知識に焦点を当てた設計手法
- 開発者は、利用者の取り巻く世界(知識/用語)については無知
- 利用者にとって役に立つソフトウェアを開発するために、価値ある知識と無価値な知識を選り分ける
- 選び抜かれた知識をコードに落とし込めば、コード自体が有用なドキュメントになる
- 価値あるソフトウェアを構築するために利用者の問題を見極め、解決への最善手を常に考える
- 利用者の取り巻く世界と実装を結びつけること
- ドメイン駆動開発とは、「ドメインの知識に焦点を当てた設計手法」
- 実装だけに留まらず、関係者とのコミュニケーションやチームビルディングと密接に関わるものが存在する
- ただし、DDDはソフトウェア開発を包括的に取り扱った設計手法のため、開発者の裁量で実践できるプラクティスもある
- DDDに限らず設計は理想の側面があるため、無理矢理現実に当て込むのではなく、現実に適合させるための落とし所をどこにするかが大切
ドメインとは、領域
- 「ドメイン」とは「領域」
例:会計システム、物流システム - ドメインに含まれる要素が重要
知識に焦点をあてるとは、ドメインと向き合うこと
- ソフトウェアの利用目的は、利用者のドメインにおけるなんらかの問題解決である
- 「利用者が直面している問題」を正確に理解することが重要
- 利用者の視点や環境を理解する = ドメインと向き合うこと
ドメインモデルとは、現実の概念/事象を抽象化した概念
- モデルとは、現実の概念/事象を抽象化した概念
- 対象が同じでも、何に重きを置くかは異なる
例:ペンの性質は?
- 小説家にとっては仕事道具、文字がかけることが重要
- 文房具店にとっては商品、値段などが重要 - 物流システムでのトラックは「荷運びできる」事を表現すれば十分
- このように現実の概念/事象を抽象化する作業がモデリングで、結果として得られるのがモデル
- DDDではドメインから得られたモデルを、ドメインモデルと呼ぶ
- 利用者はドメインの知識はあってもソフトウェアの知識はなく、開発者はソフトウェアの知識があってもドメインの知識はない
- そのため、両者が協力してドメインモデルを作り上げる必要がある
ドメインオブジェクトとは、知識をコードで表現する
- ドメインモデルはあくまでも概念を抽象化した知識
- ドメインモデルを表現して初めて問題解決の力が生まれる
- 利用者を取り巻く環境が変わったとしても、ドメインオブジェクトがドメインモデルを忠実に再現していれば、すぐにドメインの変化をコードに伝えることができる
- 「ドメインの概念」と「ドメインオブジェクト」は「ドメインモデル」を媒体に繋がり、お互いに影響し合ういてレーティブ(反復的)な開発が実現できる
DDDのパターンを俯瞰してみる
パターンは2種類
- ドメインの知識を実現するためのパターン
- アプリケーションを実現するためのパターン
知識を表現するパターン
- 値オブジェクト
- ドメイン固有の概念(金銭や製造番号など)を値として表現するパターン
- エンティティ(Clean Architectureの定義とは別物)
- 値オブジェクトと同じくドメインの概念を表現するオブジェクト
- ただし、値オブジェクトとは対をなす性質がある
- ユーザIDなど
- ドメインサービス
- 値オブジェクトやエンティティでは表現できない知識を取り扱うためのパターン
- ユーザの重複確認など
アプリケーションを実現するためのパターン
- リポジトリ
- データの保存や復元などの永続化や、再構築を担当するオブジェクト
- データの永続化を抽象化することでアプリケーションに柔軟性が生まれる
- アプリケーションサービス
- 「値オブジェクト」「エンティティ」「ドメインサービス」「リポジトリ」の要素で最低限の準備が整い、アプリケーションとして組み立てることができる
- これらの要素を強調させアプリケーションとして成立させるの場所が、「アプリケーションサービス」
- ファクトリ
- オブジェクトを作る知識に特化したオブジェクト
- オブジェクトの生成は至る所で発生するが、ファクトリを利用して生成に関する知識を一箇所にまとめる
知識を実現するための発展的なパターン
- 集約
- 整合性を保つ境界
- 値オブジェクトやエンティティなどのドメインオブジェクトを束ねて複雑なドメインの概念を表現する
- 集約は正しく実践するのが難しい
- 仕様
- オブジェクトの評価
- オブジェクトの評価をモジュールとしてうまく表現する
値オブジェクト(ドメインの知識を実現)
システム固有の値を表現する「値オブジェクト」
概要
- 値オブジェクトはドメインオブジェクトの基本
- とても単純でシステムに登場する金銭や単価といったシステム固有の値をオブジェクトとして定義する
- オブジェクトであり、値でもある、ゆえに値オブジェクトである
値の性質
- 値の性質
- 不変である
- setterもchangeメソッドも使わない
- 変換が可能である
- 交換以外の手段で変更できない
- newによる上書きのみ
- 等価性によって比較される
- 値オブジェクを比較の手段を提供する
- 値一つ一つを比較すると拡張性がないので、オブジェクト自体を比較する
- 不変である
値オブジェクトに「する」基準
- FullNameの例では、「FullName class」「FirstName class」「LastName class」それぞれ分けるべきかどうかの判断
- 可能な限り値オブジェクトを適用すると、「全て分ける」
- 判断基準として、「そこにルールが存在しているか」「それ単体で扱いたいか」
ふるまいをもった値オブジェクト
- 値オブジェクトは独自のふるまいを定義できる
例:お金オブジェクト
- お金は加算されることがあるので、Addメソッドを実装する - 値オブジェクトは自身に関するルールを語る
- 逆に定義されていなければ、そのオブジェクトには「できないこと」
ロジックの散在を防ぐ
- DRYの原則
- 値オブジェクトにルールをまとめる
エンティティ(ドメインの知識を実現)
ライフサイクルのあるオブジェクト「エンティティ」
概要
- Clean Architectureのentityとは別物
- 値オブジェクトと同じドメインオブジェクトだが、値オブジェクトとは「対をなす存在」
- 値オブジェクトとの違いは、「同一性(identity)によって識別されるか」どうか
- 人間オブジェクトだと、年齢は可変な属性だが、値が変わっても同一人物である
- 同様に体重が変わっても、同一人物である
- このように、その人がその人たる所以は属性とは全く別のところにある
- システム上の典型例は「ユーザ」
- ユーザは住所やメアドが変わっても、同一のユーザ(identity)
エンティティの性質
- 可変である
- 同じ属性であっても区別される
- 同姓同名の人がいるので、≠
- 同一性により区別される
- private readonly _id
- equals は id で判断
ドメインオブジェクトを定義するメリット
コードのドキュメント性が高まる
- 事前知識のない開発者は要件をコードから読み取れることが大切
- 仕様書はマクロな要件は有効でもミクロな要件は無力
- 仕様書は記載が誤っていてもソフトウェアが動作しなくなることはない
- ドメインモデルから実装されたドメインオブジェクトは「何をすべき」で「何をするべきでないか」を伝えてくれる
ドメインにおける変更をコードに伝えやすくする
- ドメインオブジェクトにふるまいやルールを記述すれば、ドメインにおける変更をコードに伝えやすくする
- ドメインオブジェクトにドメインモデルのルールが記載されていることは明白なので、修正も容易い
ドメインサービス(ドメインの知識を実現)
不自然さを解決する「ドメインサービス」
概要
- ドメインの概念をコードで表現しようとした時に、違和感が生じるものが存在する
- ドメインのものを表現しようとした時よりも、ドメインの活動(メソッド)を表現しようとする時に見られる傾向がある
- 複数のドメインオブジェクトを横断するような操作にも不自然さが多く見受けられる
- このような時にまた別のオブジェクトとして定義する
- DDDで取り扱われるサービスは2種類
- ドメインのためのサービス(ドメインサービス)
- アプリケーションのためのサービス(アプリケーションサービス)
不自然なふるまい
- Userオブジェクトに重複確認のふるまいを追加すると自信が重複しているかを自身にすることになる
不自然さを解決するオブジェクト、UserService
- ドメインサービスにすべてのふるまいを記述するとエンティティにはgetter/setterだけになってしまう
- serviceへのふるまいは「不自然なふるまい」に限定しなければ、ふるまいやルールの存在が読み取れない
- ふるまいやルールの存在が読み取れない = 貧血症
可能な限りドメインサービスを避ける
- 「迷いが生じたらエンティティや値オブジェクトに定義する」
エンティティや値オブジェクトと共にユースケースを組み立てる
- ドメインサービスは値オブジェクトやエンティティと組み合わせて利用される
- 今回の例では、ユーザーを作成する処理
- 利用者がユーザ名を指定して、ユーザ名が重複しなければ作成し保存
- 外部リソースのやりとりはリポジトリ
リポジトリ(アプリケーションを実現)
Dataにまつわる処理を分離する「リポジトリ」
概要
- リポジトリは永続化や再構築を担う(インフラストラクチャ)
- DB操作はプログラムの意図をぼやけさせるので分離する
- テスト実行を容易にさせ、変更の難易度を下げる
リポジトリのインターフェース
- リポジトリはインターフェースで定義されている
- リポジトリの責務はオブジェクトの永続化なので、重複チェックなどは前述した通りドメインサービス定義する
リポジトリに定義されるふるまい
- ロジックが特定のインフラストラクチャに技術に依存するのは、ソフトウェアを硬直化させてしまう
- データストアに対する詳細な操作に汚染され、処理の目的がぼやける
- リポジトリを利用するとデータの永続化にまつわる処理を抽象化することができる
アプリケーションサービス(アプリケーションの実現)
ユースケースを実現する「アプリケーションサービス」
概要
- アプリケーションサービスは、「ユースケースを実現する」オブジェクト
- ドメインオブジェクトが行うタスクの進行を管理し、問題の解決に導くもの
- 利用者の目的を達成するようなスクリプト
- あくまでもドメインオブジェクトのタスク調整に徹するべきで、ドメインのルールは記述しない
- ドメインのルールをアプリケーションサービスへ記述すると、同じコードが点在しバグの温床になる
アプリケーションサービスと凝縮度
- モジュールの責任範囲がどれだけ集中しているかを測る尺度
- 凝縮度を高めると、モジュールが一つの事柄に集中することになり、堅牢性/信頼性/再利用性/可読性の観点から好ましいとされている
- 凝縮度の計算式にLCOM(Lack of Cohesion in Methos)
- インスタンス変数は全てのメソッドで使われるべき
- 凝縮性を高めるには、クラスを分割するのが簡単な対処法
- ファイル数が多くなるので、俯瞰して見れるようにディレクトリ構成に反映する
例:ユーザ- ユーザ登録だけするクラス
- ユーザ退会だけするクラス
アプリケーションサービスのインターフェース
- アプリケーションサービスを呼び出すクライアントは、アプリケーションサービスの実態を直接呼び出すのではなく、インターフェース操作をして処理を呼ぶ
- interfaceを用意すれば、モックオブジェクトを利用してプロダクションサービスの実装を待たずに、開発を進めることができる
サービスとは何か?
改めてサービスとは?
-
サービスはクライアントのために何かを行うモノ
-
値オブジェクトやエンティティは自身のふるまいを持っているが、サービスは自身のためのふるまいを持たない
-
ドメインサービスはドメインの知識を表現したオブジェクト
-
アプリケーションサービスは利用者の問題を解決するために作られる
-
つまりユースケース
-
ドメインサービスとアプリケーションサービスは対象となる領域が異なるだけで本質的には同じ
-
向いている方向がドメインか、アプリケーションか
サービスは状態を持たない
- サービスは自身の振る舞いを変化させる目的の状態を「一切保持しない」
- bool値やプロパティを一切保持しない
柔軟性をもたらす依存関係のコントロール
概要
- ソフトウェアに柔軟性をもたらすために必要なことは依存関係を制御すること
- 依存はオブジェクトがオブジェクトを参照するだけで発生する
- 特定の技術的要素への依存を避け、抽象に移す
技術的依存がもたらすもの
- ソフトウェアの中核に位置するオブジェクトを変更することは、そのオブジェクトは多くのオブジェクトから依存されているので、多くのオブジェクトに影響する
- ドメインのロジックを技術的要素から解き放ち、ソフトウェアに柔軟性を与える
依存とは
- 具像ではなく抽象に依存しましょうというやつ、依存関係逆転の原則
依存関係逆転の原則/抽象に依存せよ
- 低レベルとは機械に近い具体的な処理を差し、高レベルとは人間に近い抽象的な処理を指す
- 上位レベルのモジュールは下位レベルのモジュールに依存してはならない
- 抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである
主導権を抽象に
- 抽象が詳細に依存するようになると、低レベルのモジュールにおける方針の変更が高レベルのモジュールに波及してしまい、おかしくなる
- 重要なドメインのルールが含まれるのはいつも高レベルなモジュールである
- 主体となるのは高レベルなモジュール、つまり抽象
- 低レベルのモジュールはインターフェースに合わせて実装を行うことで、より重要な高次元の概念に主導権を握らせることが可能になる
依存関係をコントロールする
- 本番用と開発用を切り分ける解決策として、Service LocatorパターンとIoC Containerパターンがある
Service Locator パターン
- コンストラクタでServiceLocator経由でインスタンスを取得する
- 大掛かりな仕掛けを用意する必要がないため導入しやすいが、アンチパターンであると言われている
- 依存関係が外部から見えづらくなる
- テストの意地が難しくなる
IoC Container パターン(DI Container)
- IoC Container パターンはまず、Dependency Injection パターンについて知る必要がある
- コンストラクタで依存するオブジェクトを注入しているので、コンストラクタインジェクションとも呼ばれる
- 他にはメソッドで注入するメソッドインジェクション
複雑な生成処理を行う「ファクトリ」
- ファクトリは作る知識に特化したオブジェクト
- オブジェクト生成は時に複雑な手順を必要とする
- オブジェクトの生成それ自体を独立したオブジェクトとする方がコードの意図を明確にすることにつながる
ファクトリの目的
- ファクトリが活躍するわかりやすい例として採番処理がある
ドメインのルールを守る「集約」
- データを変更するための単位として扱われるオブジェクトの集まりを集約という
- 集約にはルートとなるオブジェクトが存在し、全ての操作はルート越しに操作に制限がかけられる
複雑な条件を表現する「仕様」
- オブジェクトの評価は時に複雑な手順が必要
- 評価処理をオブジェクトのメソッドとして定義すると、オブジェクト本来の趣旨が見えづらくなる
- 評価自体をオブジェクトとして切り出す
- 仕様はとあるオブジェクトがある評価基準に達しているかを判定するオブジェクト
アーキテクチャ
- ドメイン駆動設計はドメインと向き合いながらモデルをコードに落とし込むことでドメインとコードを紐づけるプラクティス
- ドメイン駆動設計は特定のアーキテクチャを前提とすることはない
- アーキテクチャは知識を記述すべき箇所を示す方針
アンチパターン:利口なUI
- 本来であればドメインオブジェクトに記載されるべき重要なルールや振る舞いが、UIに記述されてしまっている状態
DDDがアーキテクチャに求めること
- ビジネスロジックを正しい場所に配置し続けることは難しい
- アーキテクチャはその解決策で、何がどこに記述されるべきかを明確にし、ロジックが無秩序になるのを防ぐ
アーキテクチャの解説
- レイヤードアーキテクチャ
- 最も伝統的で最も有名なアーキテクチャ
- プレゼンテーション:(UI層)
- アプリケーション層
- ドメイン層
- インフラストラクチャ層
- ヘキサゴナルアーキテクチャ
- 六角形がモチーフ
- コンセプトはアプリケーションとそれ以外のインターフェースや保存媒体はつけ外しできるようにする
- ゲーム機でいうと、コントローラーやモニター、違っても動く
- クリーンアーキテクチャ
- Clean ArchitectureのEntitiesはDDDでいうと、ドメインオブジェクトに近い
- UIやDBなどの詳細を外に追いやり、依存の方向を内側に向けることで、詳細が抽象に依存するという依存関係逆転の原則を達成する
- コンセプトはヘキサゴナルアーキテクチャと一緒
- 違いは、実装方針が明示されているか
文献