Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』:挫折せずDDD入門できる本【前編】

ドメイン駆動設計(DDD)に入門してみよう

 さてエンジニアリング界隈でも設計の話になるとクリーンアーキテクチャと並んでよく話題に上がるドメイン駆動設計。『エリック・エヴァンスのドメイン駆動設計』という原典があるのですが難しすぎる、挫折したという声をよく聞きます。何を隠そう僕もむかーし電子書籍のセールか何かで買ったままKindleの奥に眠っていました。
 2020年に出た本書がボトムアップでわかりやすく説明してくれている!と評判も高いのでこちらで改めてDDD入門してみることにしました。

 合計392Pのがっつりした本ですが中は細分化されていて読みやすいです。『ITエンジニア本大賞2021』でも技術書部門ベスト10にランクインしています。
 なお以下、C#の用語でのクラスの「フィールド」はメンバ変数と記述しちゃってます。まあ大体同じなので...

f:id:iwasiman:20210731144132p:plain
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

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 値オブジェクトにする基準

  • 基準は様々。本書ではドメインモデルになっていなかった概念を値オブジェクトにするかは、「そこにルールがあるか」「単体で取り扱いたいか」で判断することを推奨。
  • 例えばfirstNamelastName は単体で取り扱いたいならそれぞれ値オブジェクトで定義、一緒に扱うならそれぞれは文字列型で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 リポジトリとは

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) などのように定義していく。
  • ユーザの重複確認は、ドメインサービス UserServicebool Exists(User user)でまず定義、中でメンバ変数のリポジトリを呼ぶのがよい。
  • 一旦インターフェースで定義することで、対象がRDBだったりNoSQLだったり、テスト用のインメモリDBだったり、実装技術によるコードの差はインターフェースを実装したクラスの中で吸収できる。呼び出し側からは気にしなくてよい。
  • C#のコード例ではユーザがないときにnullで示しているが、Option<User> のようにOption型で定義する言語もある。

nullは人類が取り扱うには難しい概念だと書いてあります...そうや…人類には早すぎたんや…

5.4 SQLを利用したリポジトリを作成する

  • 対象がRDBだった場合のコード例。IUserRepository をインプリメンツした UserRepository クラスで、SaveFindなど各メソッドを実装。

5.5 テストによる確認

  • 意図通りの動作、そして後で変更した場合の検証にもテストは重要。
  • テストに手間がかかると次第にテストに対して誠実でなくなってしまい、動くだろうと祈ってしまう。効率的にテストを行えるようにする。

5.6 テスト用のリポジトリを作成する

  • リポジトリはインターフェースなので、IUserRepository をインプリメンツした InMemoryUserRepository クラスのようなものも作れる。ここでは内部のDictionary型のメンバ変数でデータを保持するインメモリDBで実現。DBの準備がなくてもテストができる。
  • 注意点として検索、保存メソッドでは既存データをClone()メソッドでディープコピーした値を使う。

5.7 オブジェクトリレーショナルマッパーを用いたリポジトリを作成する

  • .NET Framework.NET Coreで元から使えるORMのEntity Frameworkを使った実装例。
  • IUserRepositoryをインプリメンツしたEFUserRepositoryクラスとして、接続用のContextクラスは内部にメンバ変数で閉じ込め、必要なメソッドはprivateで定義、外から見た場合のpublicなFindSaveメソッドはそのまま。
  • 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) など別メソッドでそれぞれ定義。メソッド同名で引数だけ違うオーバーロードがサポートされていない言語ではメソッド名を変えて増やしていく。

 知ってはいたんだけど改めてリポジトリを整理できた章でした。なおレイヤードアーキテクチャでこうしたデータアクセスを担当する層はMVCModel層とかDataAccess層とか呼び方が各種ありますが、Repositoryという言葉を使うとDDDの場合を指すのがお作法みたいですね。

Chapter 6 ユースケースを実現する「アプリケーションサービス」

6.1 アプリケーションサービスとは

  • ドメインサービスの他に不自然さを解消するサービスがもうひとつ、「アプリケーションサービス」。ユーザ登録、ユーザ情報変更などのユースケースを実現するオブジェクト。ドメインの外側に位置する。
  • アプリケーションはユーザの目的に応じたプログラムなので、その上に乗ったサービスになる。

6.2 ユースケースを組み立てる

  • ユーザ登録の実装:アプリケーションサービスである UserApplicationService クラスを作成。
    • メンバでIUserRepository型のリポジトリを持つ。コンストラクタでセット。
    • メンバでUserService型のドメインサービスを持つ。コンストラクタでセット。
    • Register(string name)Get(string userId)のようなpublicなメソッドを持ち、中でドメインサービスやリポジトリを呼んで処理。
  • この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 アプリケーションサービスと凝集度

  • モジュールの責任度がどれだけ集中しているかを図る尺度が凝縮度。堅牢性・信頼性・再利用性・可読性の観点から、高い方がよい。
  • たとえば UserApplicationServiceRegister(UserRegisterCommand cmd)Delete(UserDeleteCommand cmd) ごとに使っているドメインサービスやリポジトリの数に違いがあるなら、UserRegisterService, UserDeleteService などクラスを分割すると凝縮度が高まる。分割したら、元はRegisterDeleteだったメソッド名は Handle に統一。
  • クラス分割によってまとままりがわからなくなったら、同一のパッケージや名前空間、フォルダ内に配置してまとまりを示す。
  • ただしユースケースごとにクラス分割が必須ではない。凝縮度はコード整理のヒントにする。

6.5 アプリケーションサービスのインターフェース

  • メソッドHandleを持った IUserRegisterServiceのように、実クラスの前にインターフェースも定義するとよい。
  • アプリケーションサービスを呼び出すさらに外側のクラスでは、メンバ変数でクラスでなくこのインターフェースで持つ。するとUserRegisterServiceでなく DummyUserRegisterService のようなモックオブジェクト、テスト用のクラスも使え、利便性が高まる。

6.6 サービスとは何か

  • サービスとはクライアントのために何かを行うモノ。自身のふるまいをもたない。活動や行動を表す事が多い。
  • ドメインサービスは対象となる領域がドメインで、アプリケーションサービスは対象となる領域がその外側のアプリケーションである。両者は本質的には同じ。
  • サービスは状態を持たないので、たとえば UserApplicationServiceが自身のメンバ変数でメール未送信/送信済みを持ったりする作りはおかしい。メソッドが行う各ユースケースの実行結果は常に同じ。

 伝統的なMVCアーキテクチャのサーバーサイドのフレームワークだと、HTTPリクエストが飛んできたときに動くController層から呼ばれるのがアプリケーションサービス、その先にドメインの世界があるような感じでしょうか。

Chapter 7 柔軟性をもたらす依存関係のコントロール

7.1 技術要素への依存がもたらすもの

  • プログラムの中の多くのオブジェクトには依存関係がある。依存は避けられないので、コントロールすることが重要。
  • ドメインのロジックを技術的な制約から開放し、ソフトウェアに柔軟性を与えていく。

7.2 依存とは

  • ObjectAというクラスのメンバ変数にObjectBがあったら、ObjectAObjectBに依存している。A→Bの矢印。
  • IUserRepositoryインターフェースを実装したUserRepositoryクラスがあったら、UserRepositoryIUserRepositoryに依存している。実装クラス→インターフェースの白抜き矢印。
  • 前章の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 アジャイルソフトウェア達人の技』などにも出てくる依存性逆転の法則でした。本書では簡単な例を使っていてわかりやすいですね。

iwasiman.hatenablog.com

Chapter 8 ソフトウェアシステムを組み立てる

8.1 ソフトウェアに求められるユーザーインターフェース

8.2 コマンドラインインターフェースに組み込んでみよう

  • Program クラスに記述。
  • StartUp() メソッドでIoCコンテナを使い、メンバ変数のServiceProviderInMemoryUserRepository, UserService, UserApplicationServiceを使うことを設定。
  • Main() メソッドでまず上記Startup()を呼び、while(true)の無限ループで入力を受け取り、UserApplicationServiceに処理を委譲。
  • StartUp() の中を変えればDBを変えられたりする。
  • コラムで、デジアンパターンのシングルトンパターンはstaticの代わりではないという話。

8.3 MVCフレームワークに組み込んでみよう

  • 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ではドメインオブジェクトにこれを使うことでうまく適用しているのですね。

f:id:iwasiman:20210731144132p:plain
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

続くよ