ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本
- ドメイン駆動設計とは
- システム固有の値を表現する「値オブジェクト」
- ライフサイクルのあるオブジェクト「エンティティ」
- 値オブジェクトとエンティティのまとめ
- 不自然さを解決する「ドメインサービス」
- データにまつわる処理を分離する「リポジトリ」
- ユースケースを実現する「アプリケーションサービス」
- 柔軟性をもたらす依存関係のコントロール
- ソフトウェアシステムを組み立てる
- 複雑な生成処理を行う「ファクトリ」
- ドメインのルールを守る「集約」
- 複雑な条件を表現する「仕様」
- アーキテクチャ
ドメイン駆動設計とは
ドメインモデルとは何か
ペンを考えたとき、小説家にとっては商売道具であり、文字が書けること、長く使えること、書きやすいこと、などが関心事として挙げられる。
一方、文房具店にとっては、売価や原価などが関心事として重要になる。
一見して対象が同じものであっても、見るべきアスペクトが異なるという場合が非常に多く存在する。
例えば物流システムにおいては、トラックは荷運びをするものとして表現されるのであって、エンジンキーを回すとエンジンがかかる、といったことまで表現する必要はない。
システム毎に必要なアスペクトで設計したモデルのことをドメインモデルと呼ぶ。
パターン
- 知識を表現するパターン
- 値オブジェクト
- エンティティ
- ドメインサービス
- アプリケーションを実現するためのパターン
- リポジトリ
- アプリケーションサービス
- ファクトリ
- 知識を表現する、より発展的なパターン
- 集約
- 仕様
システム固有の値を表現する「値オブジェクト」
値オブジェクトとは何か
金銭、製造番号、氏名、などといった値を表現するオブジェクト。特徴として
- 不変である
- 交換可能である
- 等価性によって比較できる
といったものが挙げられる。不変はそのままの意味。
交換可能である、とは、例えばある人物エンティティの登録名の名字だけ変えたくなったとき、値オブジェクトは不変であるから、氏名オブジェクトの名字だけ変えるとかはできない。 その代わり、「氏名オブジェクトごと」取り換える、というような操作をすることになる。要するに、上書きの最小単位と言ってもいいかもしれない。
等価性に関しては言語にもよるが、例えば Java なら Object#equals
をちゃんと override しようね、という話。
何を値オブジェクトとして定義するか?
例えば先の氏名の例では、
FullName
という型のみ作成し、内部的にはstring
で持つ。FullName
という型に加え、FirstName
LastName
という型も作成し、これらをFullName
の field に持つ。
といった実装が考えられる。どちらがいいかは context による。
……とは言っても、判断基準がないと困る。
- そこにルールが存在しているか?
- それ単体で取り扱いたいか?
を考えるとよい。
定義されないからこそわかること
例えば、金銭オブジェクト Money
を考えてみる。金額同士を足し引きすることは極めて自然でシステム上でももちろん行われ得ることだが、一方で金銭同士の乗算が意味を持つ場面はそうない。
つまり、100 円 + 100 円 = 200 円 という演算には意味があるが、100 円 * 100 円 = 10000 円2 という演算には意味がない(円2 って何よ?って話)。
したがって、Money
オブジェクトには Add
が method として生えるとしても、Multiply
のような method を生やす必要はないし、生やしてはいけない。
このように、Multiply
を定義しないことによって、この型の使用者(つまり、他の開発者)にとって、「そういう操作はこのシステム上で行われえない操作なのだ」ということがすぐわかるようになる。
(逆に、これを primitive な整数型とかで持っちゃうと、かけ算できちゃうからそういう操作をやっちゃう開発者が出てくる可能性を否定できない)
値オブジェクトを採用するモチベーション
- 表現力を増す
- 前述の
Money
オブジェクトみたいな話。何ができるか、何はできないのか、が表現できるようになる。
- 前述の
- 不正な値を存在させない
- オブジェクトに対して不正な操作が行われたら例外を投げることができる。
- 誤った代入を防ぐ
- 静的型付けにおいて、例えば金額と製造シリアル番号、両者を別の型として定義しておけば、これらを取り違えて代入しちゃった、なんてことがなくなる。
- ロジックの散在を防ぐ
- 前述の
FullName
の等価性判定の話。等価性判定のロジックが色々なところに書かれてたら嫌なので。
- 前述の
ライフサイクルのあるオブジェクト「エンティティ」
エンティティとは何か
ユーザなどを表現するオブジェクト。特徴として
- 可変である
- すべての属性が同一の値を持っていても区別される
- 同一性により区別される
が挙げられる。
値オブジェクトが不変である一方、エンティティはその属性の一部分だけを書き換えるということがありうるため、可変であると定義される。もちろん、不変であるに越したことはないので、本当に可変である必要があるものに関しては可変にしてもよい、というぐらいの理解がちょうどいい感じ。
すべての属性が同一でも区別されるのは、例えばユーザがその登録名で一意に識別できるとしてしまうとすると、同姓同名を同一ユーザとして扱うことになってしまう。しかし実際には、同姓同名であっても別の人物であるということは普通にあることなので、属性が一致していても identity は別ということが一般にありうる。
逆に、一部の属性を変更したからといって、別のユーザになってしまうわけでもない。例えばユーザがユーザ名を変更したからといって、まったく別のユーザになってしまうわけではない。EC サイトで、ユーザ名を変更しただけで、ユーザ自体が別物になったとみなされて自分の注文履歴が見られなくなったら、いやだろ(それはそう)。
区別するためには区別するための属性を持たせるわけだが、これが ID などになる。登録名を変更したとしても、ID を新たに払い出すわけではないので、同一性は失われない。
※ ここで、同一性の話としてテセウスの船やどこでもドアの思考実験とかの議論を知っているとわかりやすいかも。
値オブジェクトとエンティティ
値オブジェクトとエンティティは、前述の特徴からすると真逆のようにも思えるが、ドメイン知識を表現するためのオブジェクトという点で共通しており、重なる部分がかなり大きい。では、どのような場合に値オブジェクトで設計し、またどのような場合にはエンティティで設計すべきなのか。
連続性のあるライフサイクルを持っている場合はエンティティにしたほうがよい。例えば、ユーザは登録によって生まれ、運用の中で属性の変更等が発生し、最終的にそのユーザが退会することになれば削除されて死ぬ。
前述のとおり、同じものであっても、システム特性によって値オブジェクトにすべきかエンティティにすべきかが変わる場合もある。
例えば、車のタイヤを考えてみる。自動車のディーラーにとってタイヤは単に車の一部を形成するものであって、同じメーカーの同じ品番のタイヤは同じものとして交換可能ととらえるのが自然。したがってこの場合は値オブジェクトで設計するのが妥当になる。
一方で、タイヤの製造工場ではどうか。それぞれのタイヤには品番のみではなく製造番号やロット番号があり、製造年月日があり、それぞれのタイヤは一意に識別されるべきものであると言えそう。この場合はエンティティとして設計するのが妥当になる。
値オブジェクトとエンティティのまとめ
エンティティ (Entity)
エンティティは、一意の識別子(ID)を持つオブジェクトであり、その識別子を通じてシステム内の他のオブジェクトと区別されます。エンティティは、ライフサイクル全体を通じて一意であり、状態が変わっても同じエンティティとして認識されます。
- 特性:
- 一意の識別子(ID)を持つ。
- 識別子に基づいてオブジェクトを比較する。
- 状態が変わることがある(可変)。
- ライフサイクルがある。
- 例:
- ユーザー (User): ユーザーは一意のID(例えば、ユーザーID)を持ち、そのIDを通じてシステム内で認識されます。ユーザーの名前やメールアドレスが変わっても、同じユーザーとして扱われます。
- 注文 (Order): 注文は一意の注文番号を持ち、その番号で識別されます。注文の内容やステータスが変わっても、同じ注文として扱われます。
値オブジェクト
値オブジェクトは、一意の識別子を持たず、その属性によって完全に定義されるオブジェクトです。値オブジェクトは不変であり、同じ属性を持つ値オブジェクトは同一とみなされます。主に属性の集合を表現するために使用されます。
- 特性:
- 一意の識別子を持たない。
- 属性によってオブジェクトを比較する。
- 不変である(状態が変わらない)。
- 他のオブジェクトに埋め込まれることが多い。
- 例:
- お金 (Money): 通貨と金額の組み合わせで定義されます。例えば、10ドルは10ドルであり、同じ通貨と金額であれば同一とみなされます。
- 住所 (Address): 住所は、国、都市、郵便番号、通りなどの属性で定義されます。同じ属性を持つ住所は同一とみなされます。
エンティティと値オブジェクトの比較
特性 | エンティティ (Entity) | 値オブジェクト (Value Object) |
---|---|---|
識別方法 | 一意の識別子(ID) | 属性の値 |
同一性の判断基準 | IDによる | 属性の値による |
状態の変化 | 可変 | 不変 |
使用目的 | ライフサイクルを持つ概念を表現 | 属性の集合を表現 |
主な使用例 | ユーザ、注文など | お金、住所、期間など |
不自然さを解決する「ドメインサービス」
ドメインサービスとは
基本的に、値オブジェクトやエンティティのふるまいは、それぞれのクラス定義の中に method を生やすのが普通。
一方で、そうすると逆に不自然なつくりになってしまう場合もある。例えば、ユーザ名の重複を許さないシステムを作る場合の、ユーザ名重複チェックロジックが instance method として User
の中に生えてたらかなりおかしそう。
ただし、ドメインサービスは濫用すべきではない。ドメインサービスを肥大化させすぎると値オブジェクトやエンティティは簡単に貧血ドメインモデルになってしまう。
データにまつわる処理を分離する「リポジトリ」
ドメインモデルやドメインサービスに直接データストアを参照させると単一責任の原則違反になる。具体的には、ドメイン知識とデータの入出力が密結合になり、変更容易性が失われる。
なので、データ入出力に関してはドメイン知識からは引きはがして、リポジトリというドメインとはまったく異なるものとして設計するのがよい。
ユースケースを実現する「アプリケーションサービス」
アプリケーションサービスとは
端的に言えばユースケースを実現するオブジェクト。ユースケース図を描いて提供する機能を決定する。
あくまで「機能」「ふるまい」「活動」「行動」であって、状態を持たないものであることに注意。
ドメインオブジェクトの公開・非公開
アプリケーションサービスはデータの検索機能を含むことがあるが、この際に、ドメインオブジェクトをそのまま返却するか、DTO に詰め替えたり wrapper をかぶせたりして、データのみを見せるか、を選択することになる。これは非常に重要な決定である。
なぜならば、
- ドメインオブジェクトを公開する場合
- メリット: DTO や wrapper などのクラスを書く必要がない
- デメリット: ドメインオブジェクトのもつふるまい method が不正に client 側から呼び出される可能性がある
- 詰め替えたり wrapper をかましたりする場合
- メリット: 呼ばれるべきでないものは呼べないようにしておくことができる
- デメリット: DTO や wrapper などを書くのでコード量が増える
という風に一長一短であり、どちらのメリット・デメリットもそれなりに大きいものだからである。
例えば、ドメインオブジェクトを公開して、ふるまい method を不正に呼ばないというコーディング規約を「紳士協定」として作っておけばいいように思えるが、この強制力は極めて弱く、破ろうと思えば簡単に破れてしまう。
一方で、無駄にコーディング量が増えることを極端に嫌う開発者も一定数いることは事実であり、これらに対応するには例えばコードの自動生成ツールを提供するなど、一定の配慮が必要になる。
いずれを選択するにしても、当該システムのその後の命運を左右するレベルの決定なので、これは慎重に行わなければならない。
アプリケーションサービスと凝集度
凝集度の測定方法として LCOM (Lack of Cohesion in Methos) という計算式がある。インスタンス変数とそれが利用されている method の数から計算を行う。インスタンス変数は、すべての method で利用されているべきという思想に基づく。逆に、使用していない method があるのであればその method は本当にそこにあるべきだろうか?ということを疑うべきであるということ。
柔軟性をもたらす依存関係のコントロール
DIP の話。抽象は抽象にのみ依存しようね、といういつものやつ。
Service Locator Pattern で interface を返すようにしておいて、 instance の登録は別でやる、ってやると実装の詳細と interface の分離がうまくいく。
ソフトウェアシステムを組み立てる
この辺りはあまり興味ないので割愛……
まあ、要するに、アプリケーションにとって UI は可換であるべきで、つまりアプリケーションが UI に依存してちゃダメだよね、という話。UI を一新することなんてほぼないと言うかもしれないけど、UI を一新できるようにしておくと testability とかが上がってうれしいよね、というのがある。単体テストによってソフトウェア品質がめちゃくちゃ上がる、というわけでは必ずしもないけど、単体テストもできないようなシステムの品質は知れてるよね、という話で、それはそうという感じ。
複雑な生成処理を行う「ファクトリ」
リポジトリと同様、ドメイン知識が不要な箇所に関する処理を記述するもの。
リポジトリとの比較
リポジトリ: データの永続化と再構築 ファクトリ: データストアから独立したロジック。ただし、RDB のシーケンスを利用する採番処理のような、データストアに依存するものの場合もある(が、それは in-memory の採番処理に置き換えることもあるわけで、そういったものはファクトリだよね、という整理)
ドメインのルールを守る「集約」
SNS におけるユーザと、ユーザの集まりであるサークル機能のようなものを考えてみる。
classDiagram
namespace circle {
class Circle {
<<Aggregate Root>>
}
class CircleId
class CircleName
}
Circle --> CircleId
Circle --> CircleName
namespace user {
class User {
<<Aggregate Root>>
}
class UserId
class UserName
}
User --> UserId
User --> UserName
Circle "0..n" --> "0..n" User
たとえば user.UserName = newUserName
みたいに直接代入されちゃうと困る。
なぜかというと、ユーザ名の変更の責務がそこら中にばらまかれちゃうから。
ユーザ名の変更は、その property の所有者である User
モデルに任せましょう、という考え。コードとしては user.ChangeName(newName)
のようなイメージ。
また、 circle.Members.Add(joiningUser)
みたいなのも禁止する。理由は同じ。サークルメンバの管理はサークルに一任しましょうという考え。コードとしては circle.Join(joiningUser)
のようなイメージ。
つまるところ、OOP の隠蔽の延長線上にある話であって、デメテルの法則そのものと言ってよい。
デメテルの法則において、method を呼び出す object は次の 4 つに限定される。
- object 自身
- 引数として渡された object
- instance 変数
- 直接 instance 化した object
この範囲内でうまいことやるために、ダブルディスパッチ とかがある。
Scala だとアクセス限定子とかがあって便利。
集約の境界をどう決めるか
変更の単位で区切るのがよい。逆に、変更の単位で区切らないと、リポジトリに記述する処理が重複してしまう。
例えば、先の例、ユーザとサークルの例で考えると、サークルにユーザの変更を許せば、ユーザリポジトリ内にはもちろん、サークルリポジトリ内にもユーザの update 処理が書かれてしまい、DRY 違反となる。
ID の getter の是非
確かに、getter はなるべく少なくして、データの公開は最小限にするほうがよい。
ただし、ID のみを公開する getter に関してはちょっと事情が異なる。例えば、今回の例でいうところだと、Circle
が UserId
の collection を持っていて、それを getter で公開していたとする。
User
にはビジネスロジックが満載だが、UserId
自体にはロジックは記載されず、不正な操作というのはほとんどできないようになっている。こういった場合に関しては、Collection<UserId>
は getter で外部に公開したとしても、実害がほぼない。こういった場合には、公開することによるデメリットよりも、公開することによる自然さ・実装のしやすさといったメリットのほうが勝る場合がある。
複雑な条件を表現する「仕様」
基本的には domain model 内に domain business logic を書いていくべきだが、限界もある。例えば、自身の集約の範囲にない object の repository を扱わなければならない場合、repository を引数に取る method を生やすのか?という問題。
先の例だと、Circle#IsFull
が、プレミアム会員が 10 人以上所属するならば上限人数を 30 ではなく 50 として扱う、などとしたい場合、Circle
が UserRepository
を利用しなければならなくなる。
これを許してコーディングしていくと、結合度がどんどん上がっていく。
こういった場合に、CircleSpecification
のような object に、その domain の仕様を切り出して処理の記述をしてしまおう、というアイデア。この Specification
も一つの立派な domain model の一部である。
ただし、このような実装方針にはパフォーマンスの問題を引き起こす可能性がある。遅延実行などで対策を打つべき。
アーキテクチャ
ヘキサゴナルアーキテクチャ
ゲーム機にとって、コントローラが純正だろうがサードパーティ製だろうが、なんでもいい。とにかく、要求している仕様を満たしているなら動作する。記憶媒体も同様で、保存先がメモリーカードでも、クラウドでも、規格を満たしてさえいれば動作する。 -> ポートアンドアダプタ とも呼ばれる仕組み。