ドメイン駆動設計(DDD)に入門してみよう
さてエンジニアリング界隈でも設計の話になるとクリーンアーキテクチャと並んでよく話題に上がるドメイン駆動設計。『エリック・エヴァンスのドメイン駆動設計』という原典があるのですが難しすぎる、挫折したという声をよく聞きます。何を隠そう僕もむかーし電子書籍のセールか何かで買ったままKindleの奥に眠っていました。
2020年に出た本書がボトムアップでわかりやすく説明してくれている!と評判も高いのでこちらで改めてDDD入門してみることにしました。
- ドメイン駆動設計(DDD)に入門してみよう
- Chapter 1 ドメイン駆動設計とは
- Chapter 2 システム固有の値を表現する「値オブジェクト」
- Chapter 3 ライフサイクルのあるオブジェクト「エンティティ」
- Chapter 4 不自然さを解決する「ドメインサービス」
- Chapter 5 データにまつわる処理を分離する「リポジトリ」
- Chapter 6 ユースケースを実現する「アプリケーションサービス」
- Chapter 7 柔軟性をもたらす依存関係のコントロール
- Chapter 8 ソフトウェアシステムを組み立てる
- Chapter 9 複雑な生成処理を行う「ファクトリ」
合計392Pのがっつりした本ですが中は細分化されていて読みやすいです。『ITエンジニア本大賞2021』でも技術書部門ベスト10にランクインしています。
なお以下、C#の用語でのクラスの「フィールド」はメンバ変数と記述しちゃってます。まあ大体同じなので...
Chapter 1 ドメイン駆動設計とは
1.1 ドメイン駆動設計とは何か
- ソフトウェアの利用者を取り巻く世界を知り、その知識をコードに落とし込む。
- この洞察を繰り返すことで世界と実装を結びつける。
1.2 ドメインの知識に焦点をあてた設計手法
- 「ドメイン」:プログラムを適用する対象となる領域。ドメインに含まれるものが何かが重要。
- たとえば物流ドメインだったら倉庫や貨物が出てくる。
- 「モデル」:現実の事象や概念を抽象化した概念。必要なところだけを抽象化。
- 事象や概念を抽象化する作業が「モデリング」。DDDではドメインの概念をモデリングして得られたモデルを「ドメインモデル」と呼ぶ。
- ドメインモデルをソフトウェアの上で動作するモジュールとして表現したのが「ドメインオブジェクト」。オブジェクト指向言語のクラスのインスタンスなど。
- もともとのドメインに変化が生じたら、ドメインモデルにも変化が生じ、そして実装上のドメインオブジェクトにも変化が生じる。このように互いに影響しあって反復的な開発ができ、変更にも強くなる。
1.3 本書解説事項と目指すゴール
- DDDは基本難しいので、この本では概念や実践が難しいものは棚上げ、理解と実践がしやすい実装パターンにフォーカスしてボトムアップで解説。
1.4 本書で解説するパターンについて
- 知識を表現するパターン:値オブジェクト、エンティティ、ドメインサービス
- アプリケーションを実現させるためのパターン:リポジトリ、アプリケーションサービス、ファクトリ
- 知識を集約する発展的なパターン:集約、仕様
コラムによるとDDDが提唱されたのが2003年。昔はサービスを少しでも早く世に出すことが重要視されていたが結局ソフトウェアは後からも変化するものだということがわかり、安定運用のためにもDDDが注目されてきた…とあります。アジャイル関係の文脈でもよく登場するのも頷けます。
英文を和訳した難解な文章でなく、きちんと日本語の文章で書いてあって読みやすいです。各章、節で述べていることを細分化しているのもありがたいですね。
Chapter 2 システム固有の値を表現する「値オブジェクト」
2.1 値オブジェクトとは
- システム固有の値を表したオブジェクト。
- 例えば姓と名がある氏名なら、プリミティブな文字列型で別々に表現するより
FullName
クラスで表現する。
2.2 値の性質と値オブジェクトの実装
- 不変である:
FullName
クラスにChangeLastName()
メソッドは定義しない。別のFullName
インスタンスとして新たに作る。不変なオブジェクトを後で可変にするのは楽。 - 交換が可能である:値の変更の代わりに、同じ変数には新たな
FullName
インスタンスを代入すれば交換になる。 - 等価性によって比較される:例では
FullName#Equals(FullName other)
とFullName#Equals(object obj)
と型違いの2メソッドで比較を実現。 - また、例えば
FullName
内に新たな属性が後から増えても、このクラスの中で変更は完結する。
2.3 値オブジェクトにする基準
- 基準は様々。本書ではドメインモデルになっていなかった概念を値オブジェクトにするかは、「そこにルールがあるか」「単体で取り扱いたいか」で判断することを推奨。
- 例えば
firstName
とlastName
は単体で取り扱いたいならそれぞれ値オブジェクトで定義、一緒に扱うならそれぞれは文字列型でFullName
クラスの中で持つなど。
2.4 ふるまいをもった値オブジェクト
- 例えばお金を
Money
クラスで定義すれば、加算をAdd
メソッドにすれば不正な値を弾いたりの処理が入れられる。 Multiply(Rate rate)
のように乗算もメソッドで定義すれば、Money
同士の乗算はできないのがひと目で分かる。
2.5 値オブジェクトを採用するモチベーション
- 表現力を増す:
var modelNumber
のようなプリミティブ型の変数名ではわからないことも、値オブジェクトのクラスなら分かることが増える。 - 不正な値を存在させない:値オブジェクトのコンストラクタ内で文字数をチェックしたりする処理をすれば、値オブジェクトの中で完結。
- 誤った代入を防ぐ:値オブジェクトのコンストラクタやメソッド経由で常にアクセスすれば、数字必須の
userId
に文字列を入れたりのミスも初期で防げる。 - ロジックの散在を防ぐ:バリデーションが値オブジェクトの中で完結すれば1つにまとまり、DRY原則が守れる。
値オブジェクトに相当するクラス群はデータを持つだけで、バリデーションは外側の別の層のクラス群が専門で処理する…というやりかたで自分は開発したこともあります。DRY原則が守れていればその方法でも別に行けるんですが、こうして改めて整理されると値オブジェクトが一見地味だけど強力なのがわかります。
本書のC#のサンプルコードはかなりシンプルなので、他言語メインの方でもだいたい分かると思います。
Chapter 3 ライフサイクルのあるオブジェクト「エンティティ」
3.1 エンティティとは
- ドメインモデルを実装したドメインオブジェクトで、値オブジェクトと対になるもの。値オブジェクトと違い、同一性によって識別される。例えば
User
エンティティなら中にuseId
を持っている。 - データベースの文脈で出てくるエンティティとはまた違い、ドメイン駆動開発での固有の「エンティティ」。
3.2 エンティティの性質について
- 可変である:その属性を変化させてよい。値オブジェクトとしての
User
クラスだったら氏名を変えられないが、エンティティとしてのUser
クラスならChangeName
メソッドを実装。なおChangeName
実行前の値のチェックは基本的にエンティティの外側、クライアント側で行う。 - 同じ属性であっても区別される:値オブジェクトとしての
User
クラスは氏と名が同じだったらEquals
メソッドで同一と認識。だがエンティティのUser
クラスはメンバ変数に持つuserIdで
同一と認識。氏名が同じでも別ユーザー扱い。 - 同一性を持つ:あとから氏名の値が変わっても、
useId
が同一なら同一と認識。C#ならreadonly
を付けたメンバ変数にする。
サンプルコードはEquals
メソッドはuserId
の比較の前にC#特有のReferenceEquals()
でインスタンスの比較を行っていました。
3.3 エンティティの判断基準としてのライフサイクルと連続性
- 例えば
User
オブジェクトだったらある時作られてその後更新、ずっと存在を続けてやがて削除される。このようにライフサイクルを持ち、連続性のある概念ならエンティティにする。 - ロジックの中で一時的に使うお金クラスのようなものは値オブジェクトにする。
3.4 値オブジェクトとエンティティのどちらにもなりうるモデル
- 例えば車のタイヤは値オブジェクトだが、製造工場のシステムだったらエンティティになるかもしれない。それを取り巻く環境によってモデルの捉え方は変わる。
3.5 ドメインオブジェクトを定義するメリット
- 値オブジェクトもエンティティも、両方ともドメインモデルの表現であるドメインオブジェクト。
- コードのドキュメント性が高まる:例えば
User
クラスの中に取りうる値のチェックロジックが入っている饒舌なコードだったら、そこからルールを学べる。無口なコードだったら全コードを洗い出したり仕様書を全部見たり、労力が増える。 - ドメインにおける変更をコードに伝えやすくする:「ユーザ名は最小x文字」とルールが変わったら、対象のドメインオブジェクトを変えるだけ。変更に強くなる。
冒頭にもありますが「エンティティ」という言葉はDB周りでも使うし、レイヤードアーキテクチャのデータ永続化の層などでも使うし、DDD固有の別の「エンティティ」として注意する必要がありますね。
Chapter 4 不自然さを解決する「ドメインサービス」
4.1 サービスが指し示すもの
- ソフトウェア開発の文脈でのサービスは「クライアントのために何かをするオブジェクト」だが、様々な意味で使われる。
- DDDではドメインのためのサービスを「ドメインサービス」、アプリケーション全体のためのサービスを「アプリケーションサービス」と2つ定義して区別している。
4.2 ドメインサービスとは
- ドメインサービス:値オブジェクト、エンティティに記述すると不自然な振る舞いを配置するオブジェクト。
- 例えば
User
を作ってDB上の既存ユーザと重複していないかをチェックするExists()
メソッドをUserクラス内に置くと、user.Exists(user)
で自分自身に聞くことになり不自然。 - これを
UserService#Exists(User user)
のように定義すると不自然さが解消される。
4.3 ドメインサービスの濫用が行き着く先
- 値オブジェクトとエンティティにあったふるまい(実装上はメソッド)はすべてドメインサービスに移動できてしまう。
- こうすると値オブジェクト、エンティティがデータを持っただけのオブジェクトに退化してしまい、「ドメインオブジェクト貧血症」と呼ぶ。
- 違和感を感じない限り、迷ったらまずは値オブジェクトとエンティティ側に定義する方を選び、ドメインサービスは使わない。
4.4 エンティティや値オブジェクトと共にユースケースを組み立てる
- ユーザーを表現する
User
クラスをエンティティとして定義。氏名が同じでも別人はありえるのでエンティティ扱い。中にはUserId
クラスとUserName
クラスがあり、こちらは値オブジェクト。 - ユーザ新規作成処理は
Program#CreateUser(string userName)
として別クラス定義、DBにINSERT。 - 重複確認は
UserService#Exists(User user)
としてドメインサービスに定義。中でDBにSELECT文発行。 - これで動くが、ドメインサービスは本来ドメインモデルのコード上の表現なので、DB関連処理のコードが大半を占めるのはおかしい。→「リポジトリ」に移動する。
- そのドメインに基づく処理ならドメインサービスとして定義する。
4.5 物流システムに見るドメインサービスの例
- ある物流拠点から別の物流拠点に荷物(Baggage)を配送する例。
物流拠点クラス#Transport(行く先物流拠点, 荷物)
だとぎこちない。輸送ドメインサービス#Trasnsport(出発物流拠点, 行く先物流拠点, 荷物)
だと違和感がない。- なおドメインサービスのクラスは
AbcDomain.Service
のような名前空間に配置。クラス名は「ドメインの概念」か「ドメインの概念+Service」か「ドメインの概念+DomainService」とする事が多い。
ドメインサービスに記述するかの判断基準は本書では「ぎこちない」などの抽象的な表現に留まってあり、例も簡単です。このへんはより複雑な題材で実際にやって勘所を学ばないとなあと思いました。
Chapter 5 データにまつわる処理を分離する「リポジトリ」
5.1 リポジトリとは
- リポジトリ:「データの保管庫」。データストア(RDB,NoSQLなど)への直接書き込みは行わず、必ずリポジトリで行うことが柔軟性が高まる。
- リポジトリはドメインオブジェクトではないが、ドメインオブジェクトを際立たせる。
5.2 リポジトリの責務
- ドメインオブジェクトの永続化、再構築を行うこと。
- 例:前章の
Program
クラスならメンバ変数にIUserRepository
型でリポジトリを持つ。Program#CreateUser
メソッドの中身の実処理はuserRepository.Save(user)
に任す。 - 同様にドメインサービスの
UserService
もメンバ変数にIUserRepository
型でリポジトリを持つ。重複確認のbool Exists(User user)
メソッドは、実処理はuserRepository.Find(user.Name)
に任す。 - これによりオブジェクトの永続化の処理はリポジトリに任され、それ以外のクラスではビジネスロジックに集中できる。
5.3 リポジトリのインターフェース
- 抽象型のインターフェイス
IUserRepository
で、メソッドvoid Save(User user)
,User Find(UserName name)
などのように定義していく。 - ユーザの重複確認は、ドメインサービス
UserService
にbool Exists(User user)
でまず定義、中でメンバ変数のリポジトリを呼ぶのがよい。 - 一旦インターフェースで定義することで、対象がRDBだったりNoSQLだったり、テスト用のインメモリDBだったり、実装技術によるコードの差はインターフェースを実装したクラスの中で吸収できる。呼び出し側からは気にしなくてよい。
- C#のコード例ではユーザがないときにnullで示しているが、
Option<User>
のようにOption型で定義する言語もある。
nullは人類が取り扱うには難しい概念だと書いてあります...そうや…人類には早すぎたんや…
- 対象がRDBだった場合のコード例。
IUserRepository
をインプリメンツしたUserRepository
クラスで、Save
やFind
など各メソッドを実装。
5.5 テストによる確認
- 意図通りの動作、そして後で変更した場合の検証にもテストは重要。
- テストに手間がかかると次第にテストに対して誠実でなくなってしまい、動くだろうと祈ってしまう。効率的にテストを行えるようにする。
5.6 テスト用のリポジトリを作成する
- リポジトリはインターフェースなので、
IUserRepository
をインプリメンツしたInMemoryUserRepository
クラスのようなものも作れる。ここでは内部のDictionary
型のメンバ変数でデータを保持するインメモリDBで実現。DBの準備がなくてもテストができる。 - 注意点として検索、保存メソッドでは既存データを
Clone()
メソッドでディープコピーした値を使う。
5.7 オブジェクトリレーショナルマッパーを用いたリポジトリを作成する
.NET Framework
や.NET Core
で元から使えるORMのEntity Framework
を使った実装例。IUserRepository
をインプリメンツしたEFUserRepository
クラスとして、接続用のContextクラスは内部にメンバ変数で閉じ込め、必要なメソッドはprivateで定義、外から見た場合のpublicなFind
やSave
メソッドはそのまま。- Entity Frameworkではデータストレージに利用するオブジェクト(データモデル)を「エンティティ」と呼ぶ。RDBの1レコードに相当。これはDDDのエンティティとはまた別物。
5.8 リポジトリに定義されるふるまい
- 永続化は
void Save(User user)
。Userクラスの各項目を個別に引数で渡すのはNG。 - あくまで永続化で、オブジェクトの生成はリポジトリでなくファクトリで行う。
- 破棄は
void Delete(User user)
。 - 1件の検索は
User Find(User user)
。 - 全件は
List<User> FindAll()
。条件なしの場合はパフォーマンス的に慎重に。 - あるいは
User Find(UserName name)
など別メソッドでそれぞれ定義。メソッド同名で引数だけ違うオーバーロードがサポートされていない言語ではメソッド名を変えて増やしていく。
知ってはいたんだけど改めてリポジトリを整理できた章でした。なおレイヤードアーキテクチャでこうしたデータアクセスを担当する層はMVCのModel
層とかDataAccess
層とか呼び方が各種ありますが、Repository
という言葉を使うとDDDの場合を指すのがお作法みたいですね。
Chapter 6 ユースケースを実現する「アプリケーションサービス」
6.1 アプリケーションサービスとは
- ドメインサービスの他に不自然さを解消するサービスがもうひとつ、「アプリケーションサービス」。ユーザ登録、ユーザ情報変更などのユースケースを実現するオブジェクト。ドメインの外側に位置する。
- アプリケーションはユーザの目的に応じたプログラムなので、その上に乗ったサービスになる。
6.2 ユースケースを組み立てる
- ユーザ登録の実装:アプリケーションサービスである
UserApplicationService
クラスを作成。 - この
UserApplicationService
を呼び出す更に外側のクラスにはドメインオブジェクトを公開しない方針の場合は、DTO(Data Transfer Object)を使う。エンティティであるUser
クラスの代わりにUserData
クラス。完全にデータの入れ物だけのクラス。 - 入れ替えが面倒だったら、
UserData
クラスのコンストラクタをUserData(User source)
のようにドメインオブジェクトを受け取って中で変換するようすればよい。 DTOのクラスを自動生成するようなツールを作る手もある。
アプリケーションサービスの
public void Update(string name, string mail)
...のような情報更新のメソッドの仕様が変わり引数が変わるようなら「コマンドオブジェクト」を使う手がある。public void Update(UserUpdateCommand command)
へ。仕様変更時も引数を変えなくて済む。- コマンドオブジェクトの中はメンバ変数だけで、何の更新をするのかを制御。
- 同様にユーザ退会のユースケースなら
public void Delete(UserDeleteCommand command)
。
6.3 ドメインのルールの流出
- アプリケーションサービスはドメインオブジェクトのタスク調整に徹する。ドメインの外側にいるので、ドメイン内のルールを書いてはいけない。
- たとえば
UserApplicationService
にユーザ重複確認のコードが複数箇所に散らばっているなら、メンバで持っているドメインサービスのUserService#Exists(User user)
に入れる。重複確認のルールが変わってもドメインサービス内の1箇所を変えれば良い。
6.4 アプリケーションサービスと凝集度
- モジュールの責任度がどれだけ集中しているかを図る尺度が凝縮度。堅牢性・信頼性・再利用性・可読性の観点から、高い方がよい。
- たとえば
UserApplicationService
でRegister(UserRegisterCommand cmd)
やDelete(UserDeleteCommand cmd)
ごとに使っているドメインサービスやリポジトリの数に違いがあるなら、UserRegisterService
,UserDeleteService
などクラスを分割すると凝縮度が高まる。分割したら、元はRegister
やDelete
だったメソッド名はHandle
に統一。 - クラス分割によってまとままりがわからなくなったら、同一のパッケージや名前空間、フォルダ内に配置してまとまりを示す。
- ただしユースケースごとにクラス分割が必須ではない。凝縮度はコード整理のヒントにする。
6.5 アプリケーションサービスのインターフェース
- メソッド
Handle
を持ったIUserRegisterService
のように、実クラスの前にインターフェースも定義するとよい。 - アプリケーションサービスを呼び出すさらに外側のクラスでは、メンバ変数でクラスでなくこのインターフェースで持つ。すると
UserRegisterService
でなくDummyUserRegisterService
のようなモックオブジェクト、テスト用のクラスも使え、利便性が高まる。
6.6 サービスとは何か
- サービスとはクライアントのために何かを行うモノ。自身のふるまいをもたない。活動や行動を表す事が多い。
- ドメインサービスは対象となる領域がドメインで、アプリケーションサービスは対象となる領域がその外側のアプリケーションである。両者は本質的には同じ。
- サービスは状態を持たないので、たとえば
UserApplicationService
が自身のメンバ変数でメール未送信/送信済みを持ったりする作りはおかしい。メソッドが行う各ユースケースの実行結果は常に同じ。
伝統的なMVCアーキテクチャのサーバーサイドのフレームワークだと、HTTPリクエストが飛んできたときに動くController層から呼ばれるのがアプリケーションサービス、その先にドメインの世界があるような感じでしょうか。
Chapter 7 柔軟性をもたらす依存関係のコントロール
7.1 技術要素への依存がもたらすもの
7.2 依存とは
ObjectA
というクラスのメンバ変数にObjectB
があったら、ObjectA
はObjectB
に依存している。A→B
の矢印。IUserRepository
インターフェースを実装したUserRepository
クラスがあったら、UserRepository
はIUserRepository
に依存している。実装クラス→インターフェース
の白抜き矢印。- 前章の
UserApplicationService
がメンバ変数にUserRepository
を持っていたら、UserApplicationService→UserRepository
に依存。UserRepository
が取り扱うデータストアの技術的な制約に縛られてしまう。 - ここで
UserApplicationService
はメンバ変数にIUserRepository
インターフェースを持ち、コンストラクタで実装クラスを渡すようにすると中身を変えられる。UserApplicationService→IUserRepository
で依存の矢印が抽象型に向く。IUserRepository←UserRepository
でもある。ビジネスロジックを具体的な実装から解き放てる。「依存関係逆転の原則」。
7.3 依存関係逆転の原則とは
依存関係逆転の法則 (DIP: Dependency Inversion Principle)
- 上位レベルのモジュールは下位レベルのモジュールに依存してはいけない。抽象に依存する。
抽象は実装の詳細に依存してはいけない。実装の詳細が抽象に依存すべき。
高レベルが人間に近い抽象的な処理、データストアの処理などは機械に近いので低レベルの処理として扱う。
- 先程の
UserApplicationService→IUserRepository←UserRepository
がまさにこれ。 - 昔は高レベルのモジュールが低レベルのモジュールに依存する形で開発される傾向があったがこれはおかしい。データストアの変更があってもビジネスロジックは変えなくてよいのが望ましい。
7.4 依存関係をコントロールする
- 例えば
UserApplicationService
のメンバ変数IUserRepository
に入る実装クラスをコンストラクタの中でnewして代入していたら、データストアが変わるたびに全クラスに修正が必要になってこれはよくない。この問題解決パターンがある。
Service Locatorパターン:
// 事前にインスタンスを登録 ここではテスト用の別リポジトリを設定 ServiceLocator.Register<IUserRepository, InMemoryUserRepository>(); // UserApplicationServiceクラスのコンストラクタの中のコードは変更不要! this.userRepository = ServiceLocator.Resolve<IUserRepository>();
- アプリケーション起動時にどこかで一括設定しておけば、各クラスのコードは変えずに済む。
- 知らない開発者が見たら、「
UserApplicationService
をnew する前にService Locatorの設定をする必要がある」という前提がわからないという欠点がある。 - また
UserApplicationService
に同種の新たなメンバ変数が追加されてService Locator経由でまた取得するように...と変更が積み重なっていくと、テストコードが破壊されてしまう。
IoC Container(DI Container)パターン:
Dependency Injection
=依存性の注入 を活用する。- 原始的なコンストラクタインジェクションだったら、メンバの
IUserRepository
型に代入される実装クラスをコンストラクタ引数で設定。メンバ変数が増えたらコンストラクタの引数が増えていく形で足りなければコンパイルエラーになるので判別できる。 - このコンストラクタインジェクションもめんどくさいので解決できる。C#のIoC Container機能を使うのが以下。
// 予めIoC Containerで依存解決の設定を登録 これをアプリ起動時に実行されるどこか一箇所のコードで設定。 var serviceCollection = new ServiceCollection(); serviceCollection.AddTransient<IUserRepository, InMemoryUserRepository>(); serviceCollection.AddTransient<UserApplicationService>; // サービスクラスのインスタンスをIoC Container経由で取得する // 自動でInMemoryUserRepositoryが設定されている。すっごーい! var provider = serviceCollection.BuildServiceProvider(); var userApplicationService = provider.GetService<UserApplicationService>();
Uncle Bob大先生の『Clean Code アジャイルソフトウェア達人の技』などにも出てくる依存性逆転の法則でした。本書では簡単な例を使っていてわかりやすいですね。
Chapter 8 ソフトウェアシステムを組み立てる
8.1 ソフトウェアに求められるユーザーインターフェース
- CUIやGUIが代表。どちらでもドメイン駆動開発の強力な力の恩恵は受けられる。
- 本書ではドメインの問題を解決するのをアプリケーション、そこにユーザーインターフェースが加わったものをソフトウェアとしている。
8.2 コマンドラインインターフェースに組み込んでみよう
Program
クラスに記述。StartUp()
メソッドでIoCコンテナを使い、メンバ変数のServiceProvider
にInMemoryUserRepository, UserService, UserApplicationService
を使うことを設定。Main()
メソッドでまず上記Startup()
を呼び、while(true)
の無限ループで入力を受け取り、UserApplicationService
に処理を委譲。StartUp()
の中を変えればDBを変えられたりする。- コラムで、デジアンパターンのシングルトンパターンはstaticの代わりではないという話。
- C#で現在メジャーなASP.NET Core MVCを使用。
- 提供されている
Startup
クラスのConfigureService()
でIoCコンテナ分の設定を記述。 IDependenySetup
インターフェースを継承したクラスで、インメモリDBとRDBで設定を分けて書いたりできる。appsettigs.json
という設定ファイルに記述、DependencySetupFactory
クラスに切り替えるクラスを記述、StartUp
クラスも書き換えるとこの切り替えが実現できる。- HTTPリクエストを受け取る
UserController
クラスを実装。メンバ変数でUserApplicationService
を持ち、コンストラクタで受け取るようにしておくと、IoCコンテナがうまくやってくれる。 Index(), Get(), Post()
でリクエストを受け取ったら、Command
クラスに変換して、メインの処理はすべてUserApplicationService
に移譲する。- コントローラーの責務は入力の変換。ゲーム機のコントローラーで操作を電気信号員変えてゲーム機本体に送るようなもの。ドメインの重要な知識やロジックはコントローラーに漏れ出さないようにする。
8.4 ユニットテストを書こう
UserRegisterTest
などのテストクラス中の各テストメソッドの中で、UserRepository, UserService, UserApplicationService
を作る。UserApplicationService
クラスの登録・更新などの処理を呼んで、その後UserRepository
クラスでリポジトリの中に入ったかを見てAssert
で比較すれば良い。
8.5 まとめ
- アプリケーションにとってユーザーインターフェースは交換可能で、ソフトウェアの核心部分からは分離しておく。
- ユニットテストが実施できれば交換可能な作りになっているはず。
作者のnrsさんが昔、ASP.NET Web Forms
で作られた古い15年もののシステムをASP.NET MVC
に改修した怪談話が出ていますが、これは登壇資料かなにかで見たような...
この章のサンプルコードだけは急に難しく、C#特有のところがあちこちに出てきますが仕方ないですね。
ところでコード中でUserApplicationService
以外にUserService
もいつも生成していますが、こちらを使っていないような...?
Chapter 9 複雑な生成処理を行う「ファクトリ」
9.1 ファクトリの目的
- コンピュータのように、複雑な道具はその生成過程も複雑なことがある。
- プログラムのオブジェクトも生成過程が複雑な場合がある。この生成を責務としたオブジェクトを「ファクトリ」と呼ぶ。
9.2 採番処理をファクトリに実装した例の確認
User
クラスのコンストラクタpublic User(UserName name)
の中でUserId
の採番が必要になる場合。常に一意になるGUIDを取得したり、DBのSequenceから次の値を得たり...と複雑になる場合。IUserFactory
インターフェースに同じ処理をUser Create(UserName name)
と定義、インプリメントしたUserFactory
に実処理を実装する。- これで元の
User
クラスから複雑なコンストラクタは消す。(中でUserFactory
を呼ばない) - アプリケーションサービスの
UserApplicationService
のメンバ変数にリポジトリなどと一緒にIUserFactory
を持たせコンストラクタでnew、実処理の中でuserFactory.Create(name)
する。 - インターフェースで定義しておけば、テスト用のファクトリに切り替えたりも可能。
User
クラスを常にファクトリ経由で作るルールに気づかせるには、XxxDomain.Models.Users
のような名前空間(パッケージ、フォルダ)に対象のクラスと一緒にファクトリも配置する。IUserRepository
インターフェースにUserId NextIdentity()
など、リポジトリに採番処理を持たせるパターンもある。
9.3 ファクトリとして機能するメソッド
- 例えば
User
が複数集まったCircle
クラスを作る場合。User
クラスにpublic Circle CreateCircle(CircleName name)
のようにインスタンス生成メソッドを作っても良い。ファクトリの役目をしている。 - この場合はユーザーがサークルを生成するのがドメインオブジェクトのふるまいとして正しければ、クラスに持たせて良い。
9.4 複雑な生成処理をカプセル化しよう
- 単純にインスタンス生成が複雑な場合に、コンストラクタ内の長い処理をファクトリに移すパターンもある。
- コンストラクタで他のオブジェクトも生成していたら、単にファクトリに移すのはやめた方がよい場合もある。
- なお、ファクトリはドメインを由来とするオブジェクトではない。ドメインの設計を構成する要素。
GoFデザインパターンのファクトリパターンとほぼ同様に感じました。DDDではドメインオブジェクトにこれを使うことでうまく適用しているのですね。
続くよ