Skip to the content.

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

ドメイン駆動設計とは

ドメインモデルとは何か

ペンを考えたとき、小説家にとっては商売道具であり、文字が書けること、長く使えること、書きやすいこと、などが関心事として挙げられる。

一方、文房具店にとっては、売価や原価などが関心事として重要になる。

一見して対象が同じものであっても、見るべきアスペクトが異なるという場合が非常に多く存在する。

例えば物流システムにおいては、トラックは荷運びをするものとして表現されるのであって、エンジンキーを回すとエンジンがかかる、といったことまで表現する必要はない。

システム毎に必要なアスペクトで設計したモデルのことをドメインモデルと呼ぶ。

パターン

システム固有の値を表現する「値オブジェクト」

値オブジェクトとは何か

金銭、製造番号、氏名、などといった値を表現するオブジェクト。特徴として

といったものが挙げられる。不変はそのままの意味。

交換可能である、とは、例えばある人物エンティティの登録名の名字だけ変えたくなったとき、値オブジェクトは不変であるから、氏名オブジェクトの名字だけ変えるとかはできない。 その代わり、「氏名オブジェクトごと」取り換える、というような操作をすることになる。要するに、上書きの最小単位と言ってもいいかもしれない。

等価性に関しては言語にもよるが、例えば Java なら Object#equals をちゃんと override しようね、という話。

何を値オブジェクトとして定義するか?

例えば先の氏名の例では、

といった実装が考えられる。どちらがいいかは context による。

……とは言っても、判断基準がないと困る。

を考えるとよい。

定義されないからこそわかること

例えば、金銭オブジェクト Money を考えてみる。金額同士を足し引きすることは極めて自然でシステム上でももちろん行われ得ることだが、一方で金銭同士の乗算が意味を持つ場面はそうない。

つまり、100 円 + 100 円 = 200 円 という演算には意味があるが、100 円 * 100 円 = 10000 円2 という演算には意味がない(円2 って何よ?って話)。

したがって、Money オブジェクトには Add が method として生えるとしても、Multiply のような method を生やす必要はないし、生やしてはいけない。

このように、Multiply を定義しないことによって、この型の使用者(つまり、他の開発者)にとって、「そういう操作はこのシステム上で行われえない操作なのだ」ということがすぐわかるようになる。 (逆に、これを primitive な整数型とかで持っちゃうと、かけ算できちゃうからそういう操作をやっちゃう開発者が出てくる可能性を否定できない)

値オブジェクトを採用するモチベーション

ライフサイクルのあるオブジェクト「エンティティ」

エンティティとは何か

ユーザなどを表現するオブジェクト。特徴として

が挙げられる。

値オブジェクトが不変である一方、エンティティはその属性の一部分だけを書き換えるということがありうるため、可変であると定義される。もちろん、不変であるに越したことはないので、本当に可変である必要があるものに関しては可変にしてもよい、というぐらいの理解がちょうどいい感じ。

すべての属性が同一でも区別されるのは、例えばユーザがその登録名で一意に識別できるとしてしまうとすると、同姓同名を同一ユーザとして扱うことになってしまう。しかし実際には、同姓同名であっても別の人物であるということは普通にあることなので、属性が一致していても identity は別ということが一般にありうる。

逆に、一部の属性を変更したからといって、別のユーザになってしまうわけでもない。例えばユーザがユーザ名を変更したからといって、まったく別のユーザになってしまうわけではない。EC サイトで、ユーザ名を変更しただけで、ユーザ自体が別物になったとみなされて自分の注文履歴が見られなくなったら、いやだろ(それはそう)。

区別するためには区別するための属性を持たせるわけだが、これが ID などになる。登録名を変更したとしても、ID を新たに払い出すわけではないので、同一性は失われない。

※ ここで、同一性の話としてテセウスの船やどこでもドアの思考実験とかの議論を知っているとわかりやすいかも。

値オブジェクトとエンティティ

値オブジェクトとエンティティは、前述の特徴からすると真逆のようにも思えるが、ドメイン知識を表現するためのオブジェクトという点で共通しており、重なる部分がかなり大きい。では、どのような場合に値オブジェクトで設計し、またどのような場合にはエンティティで設計すべきなのか。

連続性のあるライフサイクルを持っている場合はエンティティにしたほうがよい。例えば、ユーザは登録によって生まれ、運用の中で属性の変更等が発生し、最終的にそのユーザが退会することになれば削除されて死ぬ。

前述のとおり、同じものであっても、システム特性によって値オブジェクトにすべきかエンティティにすべきかが変わる場合もある。

例えば、車のタイヤを考えてみる。自動車のディーラーにとってタイヤは単に車の一部を形成するものであって、同じメーカーの同じ品番のタイヤは同じものとして交換可能ととらえるのが自然。したがってこの場合は値オブジェクトで設計するのが妥当になる。

一方で、タイヤの製造工場ではどうか。それぞれのタイヤには品番のみではなく製造番号やロット番号があり、製造年月日があり、それぞれのタイヤは一意に識別されるべきものであると言えそう。この場合はエンティティとして設計するのが妥当になる。

値オブジェクトとエンティティのまとめ

エンティティ (Entity)

エンティティは、一意の識別子(ID)を持つオブジェクトであり、その識別子を通じてシステム内の他のオブジェクトと区別されます。エンティティは、ライフサイクル全体を通じて一意であり、状態が変わっても同じエンティティとして認識されます。

値オブジェクト

値オブジェクトは、一意の識別子を持たず、その属性によって完全に定義されるオブジェクトです。値オブジェクトは不変であり、同じ属性を持つ値オブジェクトは同一とみなされます。主に属性の集合を表現するために使用されます。

エンティティと値オブジェクトの比較

特性 エンティティ (Entity) 値オブジェクト (Value Object)
識別方法 一意の識別子(ID) 属性の値
同一性の判断基準 IDによる 属性の値による
状態の変化 可変 不変
使用目的 ライフサイクルを持つ概念を表現 属性の集合を表現
主な使用例 ユーザ、注文など お金、住所、期間など

不自然さを解決する「ドメインサービス」

ドメインサービスとは

基本的に、値オブジェクトやエンティティのふるまいは、それぞれのクラス定義の中に method を生やすのが普通。

一方で、そうすると逆に不自然なつくりになってしまう場合もある。例えば、ユーザ名の重複を許さないシステムを作る場合の、ユーザ名重複チェックロジックが instance method として User の中に生えてたらかなりおかしそう。

ただし、ドメインサービスは濫用すべきではない。ドメインサービスを肥大化させすぎると値オブジェクトやエンティティは簡単に貧血ドメインモデルになってしまう。

データにまつわる処理を分離する「リポジトリ」

ドメインモデルやドメインサービスに直接データストアを参照させると単一責任の原則違反になる。具体的には、ドメイン知識とデータの入出力が密結合になり、変更容易性が失われる。

なので、データ入出力に関してはドメイン知識からは引きはがして、リポジトリというドメインとはまったく異なるものとして設計するのがよい。

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

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

端的に言えばユースケースを実現するオブジェクト。ユースケース図を描いて提供する機能を決定する。

あくまで「機能」「ふるまい」「活動」「行動」であって、状態を持たないものであることに注意。

ドメインオブジェクトの公開・非公開

アプリケーションサービスはデータの検索機能を含むことがあるが、この際に、ドメインオブジェクトをそのまま返却するか、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 つに限定される。

この範囲内でうまいことやるために、ダブルディスパッチ とかがある。

Scala だとアクセス限定子とかがあって便利。

集約の境界をどう決めるか

変更の単位で区切るのがよい。逆に、変更の単位で区切らないと、リポジトリに記述する処理が重複してしまう。

例えば、先の例、ユーザとサークルの例で考えると、サークルにユーザの変更を許せば、ユーザリポジトリ内にはもちろん、サークルリポジトリ内にもユーザの update 処理が書かれてしまい、DRY 違反となる。

ID の getter の是非

確かに、getter はなるべく少なくして、データの公開は最小限にするほうがよい。

ただし、ID のみを公開する getter に関してはちょっと事情が異なる。例えば、今回の例でいうところだと、CircleUserId の collection を持っていて、それを getter で公開していたとする。

User にはビジネスロジックが満載だが、UserId 自体にはロジックは記載されず、不正な操作というのはほとんどできないようになっている。こういった場合に関しては、Collection<UserId> は getter で外部に公開したとしても、実害がほぼない。こういった場合には、公開することによるデメリットよりも、公開することによる自然さ・実装のしやすさといったメリットのほうが勝る場合がある。

複雑な条件を表現する「仕様」

基本的には domain model 内に domain business logic を書いていくべきだが、限界もある。例えば、自身の集約の範囲にない object の repository を扱わなければならない場合、repository を引数に取る method を生やすのか?という問題。

先の例だと、Circle#IsFull が、プレミアム会員が 10 人以上所属するならば上限人数を 30 ではなく 50 として扱う、などとしたい場合、CircleUserRepository を利用しなければならなくなる。

これを許してコーディングしていくと、結合度がどんどん上がっていく。

こういった場合に、CircleSpecification のような object に、その domain の仕様を切り出して処理の記述をしてしまおう、というアイデア。この Specification も一つの立派な domain model の一部である。

ただし、このような実装方針にはパフォーマンスの問題を引き起こす可能性がある。遅延実行などで対策を打つべき。

アーキテクチャ

ヘキサゴナルアーキテクチャ

ゲーム機にとって、コントローラが純正だろうがサードパーティ製だろうが、なんでもいい。とにかく、要求している仕様を満たしているなら動作する。記憶媒体も同様で、保存先がメモリーカードでも、クラウドでも、規格を満たしてさえいれば動作する。 -> ポートアンドアダプタ とも呼ばれる仕組み。

ホームへ戻る