Chapter1 ドメイン駆動設計とは
ドメイン駆動設計とは何か
利用者にとって有用なソフトウェアを開発するためには、必要な知識とそうでないものを区別する必要がある。
必要な知識を区別するには、ソフトウェアの利用者やそれを取り巻く環境・世界を知る必要がある。
それによって利用者の問題を理解し、ソフトウェアの実装とそれらの知識を結びつけるための手法。
ドメインの知識に焦点を当てた設計手法
ドメインとは
プログラムを適用する対象となる領域。システムによってドメインに含まれるものはそれぞれ異なる。
会計システムなら「金銭」「帳票」、物流なら「倉庫」「輸送」等
ソフトウェアの目的はドメインにおける何らかの問題の解決である。
=> ドメインに対する理解を深める必要がある
ドメインモデルとは何か
各ドメインの事象や概念を抽象化したものがドメインモデル。
ドメインに対する知識は利用者、ソフトウェアに対する知識はエンジニアが詳しい。つまりドメインモデルの構築には両者の協力が不可欠である。
知識をコードで表現するドメインオブジェクト
ドメインモデルを実際のコードに落とし込んだものが ドメインオブジェクト
ドメインモデルはドメインの射影である。そのため、ドメインで生じた変更はドメインモデルにも必ず反映される。
そしてドメインモデルをもとにドメインオブジェクトの修正点が浮き彫りになる。
このようにドメインの変更はドメインモデルを通じてドメインオブジェクトにも反映される。
逆にドメインオブジェクトの実装中に曖昧な仕様が見つかり、それがドメインモデルの修正、ドメインの理解の見直しにつながることもある。
ドメイン、モデル、オブジェクトは相互に影響しあい、反復的に開発が行われる。
本書解説事項と目指すゴール
ドメイン駆動設計は実践するにはある程度の環境が必要。
ドメインの理解やドメインモデルの把握には関係者とのコミュニケーションが必要になる。
Chapter2 システム固有の値を表現する「値オブジェクト」
値オブジェクトとは
システム固有の値を表現するために定義されたオブジェクトが 値オブジェクト
例として名前が挙げられている。
日本では性 + 名の順だが、世界には名 + 性で名前を表記する国が存在する。
これらをプリミティブな型、文字列型等で一緒くたに扱おうとすると性のみ・名のみを取り出そうとする時などにうまくいかないことがある。
これを以下のようなクラス(値オブジェクト)で表現するのが値オブジェクト。
class FullName:
def __init__(self, first_name: str, last_name: str):
self._first_name: str = first_name
self._last_name: str = last_name
@property
def first_name(self):
return self._first_name
@property
def last_name(self):
return self._last_name
値の性質と値オブジェクトの実装
代表的な値の性質 - 不変である - 交換が可能である - 等価性によって比較される
不変である
代入は値の変更ではない。値の変更とは以下のようなもの。
このように値そのものが変わってしまうと開発者にとっては大きな混乱をもたらす。
値そのものは 不変 であることが値を安心して利用するための条件になる。
よくあるセッターを利用した値の変更の例
値オブジェクトは 値 である。そのため値オブジェクトも不変である必要がある。
上記のような値を変更できる振る舞いは実装するべきではない。
交換が可能である
普段行っている 代入 が値の交換にあたる。
等価性によって比較される
値オブジェクトを比較する際に同じインスタンスであるかを比較するのではなく、そのオブジェクトを構成する属性(インスタンス変数)が一致することを比較する。
上記のように 0 と 0、"test" と "test" は別インスタンスだが属性は同じ場合は等価であると判定される。
値オブジェクトには上記のようにオブジェクトそのままを等価性によって比較できるように実装するほうが自然な実装になる。
=> プリミティブな値と同様に扱えるのがよい?
class FullName:
def __init__(self, first_name: str, last_name: str):
self._first_name: str = first_name
self._last_name: str = last_name
@property
def first_name(self):
return self._first_name
@property
def last_name(self):
return self._last_name
def __eq__(self, value: "FullName"):
return (
self._first_name == value._first_name
and self._last_name == value._last_name
)
t1 = FullName("tanaka", "taro")
t2 = FullName("tanaka", "taro")
t3 = FullName("kato", "aki")
print(t1 == t2) # True
print(t2 == t3) # False
print(t1 == t3) # False
値オブジェクトにする基準
ドメインモデルとして定義される概念ならば、値オブジェクトとして定義される。
値オブジェクトにするかはそのコンテキストによる。
筆者の考える基準
- 値にルールがあるか
- 値を単体として扱いたいか
実装中に値オブジェクトを定義すべき概念が見つかったならば、ドメインモデルにフィードバックを行うべきである。
振る舞いを持った値オブジェクト
プリミティブな値とは異なり、値オブジェクトには振る舞いを実装できその振る舞いにドメインのルールを実装することができる。
加算等の振る舞いを追加する場合は、値オブジェクトの性質を満たすように実装するように注意する。
class Money:
def __init__(self, amount: int, currency: str):
self._amount: str = amount
self._currency: str = currency
@property
def amount(self):
return self._amount
@property
def currency(self):
return self._currency
def __repr__(self):
return f"{self.amount} {self.currency}"
def __add__(self, value: "Money"):
# ドメインモデルからのルール。異なる通貨は加算できない。
if self._currency != value.currency:
raise Exception("通貨単位が異なります。")
# 値オブジェクトは不変であるため、返すのは新しいMoneyオブジェクト
return Money(self.amount + value.amount, self.currency)
m1 = Money(100, "yen")
m2 = Money(313, "yen")
print(m1 + m2) # 413 yen
定義されないからこそわかること
値オブジェクトの振る舞いはそのオブジェクトができることを表す。つまり定義されていない振る舞いはそのオブジェクトはできない。
例えばお金同士の乗算はできないため、その振る舞いは定義されないが、金利計算等による乗算は定義される可能性はある。
memo
上記例で金利相手の乗算のみを定義するには、金利を表す Rate クラスが必要になる。モデルに金利がなければこの段階でモデルの見直しが入ると思われる。
値オブジェクトを採用するモチベーション
- 表現力を増す
- 不正な値を存在させない
- 誤った代入を防ぐ
- ロジックの散在を防ぐ
表現力を増す
メールアドレスやシリアル番号などプリミティブな値だけでは値自体がどのようなものかを表現しきれない。
そのため処理の途中でその値がどのようなものなのかを知るためには、処理自体を追って値を確認しなければならない。
値オブジェクトならば値オブジェクトの定義を見ればそのオブジェクトがどのような値・振る舞いを持つかをすぐに見つけることができる。
値オブジェクトはその定義により自分がどういったものであるかを主張する自己文書化を推し進めます。
成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (p.79). 株式会社翔泳社. Kindle 版.
不正な値を存在させない
値オブジェクトを利用すれば、オブジェクト生成時に値のチェックを行い、不正な値が存在することがないようにできる。
結果としてシステムが不正な値を持つことにおびえる必要がなくなる。
class Money:
def __init__(self, amount: int, currency: str):
# 不正な値を防ぐガード節
if (amount < 0):
raise Exception("金額は0以上であることが必要です。")
...
誤った代入を防ぐ
代入が正しいものか誤ったものかを判断するには、仕様についての深い理解が必要になる。
より判断が簡単に、仕様の把握が容易になるのはその仕様を実装に落とし込み、自己文書化を進めることにある。
class UserName:
...
class User:
def __init__(self):
self._name: UserName = None
@property
def name(self):
self._name
@name.setter
def name(self, name: UserName):
if not isinstance(name, UserName):
raise Exception("名前はUserName型である必要があります")
self._name = name
疑問
値オブジェクトは不変であるためには、代入処理を許すべきではないのでは?
代入ではなく新しいオブジェクトを返すべきではないのか?
2025/03/17 追記
ここでの User クラスは後に登場するエンティティにあたるため可変である。
不変な値オブジェクトはここでは UserName クラスにあたるため、User の name の変更は User の中の UserName オブジェクトの属性を変更するのではなく、新しい UserName オブジェクトを渡して代入することによって変更する。
memo
例では値オブジェクトの属性に代入を行っているが、静的型付け言語ではコンパイル時に変数への代入部分も型のチェックが入る。
値オブジェクトを利用していれば、変数への代入もチェック機能が働くことになる。
これによって誤った代入を防ぐことができるようになる。
ロジックの散在を防ぐ
値オブジェクトの利用が DRY 原則を守ることにつながる。
例えば値オブジェクトを利用しない場合、値のチェック処理等はシステムのあらゆるところに散在することとなる。
この場合に値の定義に変更があった場合、修正が必要な個所を洗い出すのは困難な仕事となる。
値オブジェクトにチェック機能を実装していれば、DRY 原則を破ることもなく、また定義の変更があった際の修正箇所の 1 か所で済む。
ルールをまとめることは変更箇所をまとめることと同義です。
成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (p.88). 株式会社翔泳社. Kindle 版.
まとめ
ドメインの知識を値オブジェクトとして定義すれば、その定義自体がドキュメントとして機能する。
Chapter3 ライフサイクルのあるオブジェクト「エンティティ」
エンティティとは
ドメイン駆動におけるエンティティはドメインモデルを実装したドメインオブジェクトを指す。
値オブジェクトも同様にドメインモデルを実装したドメインオブジェクトだが、両者は別のものである。
値オブジェクトはその 属性(インスタンス変数) によって識別され、エンティティは 同一性(identity) によって区別される。
例えばシステムにおけるユーザーという概念を考えると、ユーザー情報の住所や電話番号等の属性を変更したとしてもユーザー自体が変更されたとは考えられない。そのためユーザーのドメインモデルを実装したドメインオブジェクトはエンティティとなる。
一方でお金のようなドメインオブジェクトはその金額により識別されるため値オブジェクトとなる。
エンティティの性質について
エンティティの性質 - 可変である - 同じ属性であっても区別される - 同一性によって区別される
可変である
値オブジェクトとは異なり、エンティティの属性は変更が許容されている。
class User:
def __init__(self, name: str):
self.change_name(name)
@property
def name(self):
return self._name
def change_name(self, name: str):
if not name:
raise Exception("名前がNullです。")
if len(name) < 3:
raise Exception("ユーザ-名は3文字以上必要です。")
self._name = name
すべての属性を可変にする必要はない。
同じ属性であっても区別される
エンティティは値オブジェクトとは異なり属性が同じでも同一とはみなされない。
エンティティ同士の区別は 識別子(Identity) が利用される。
同一性を持つ
ユーザー情報に変更があっても変更前後のユーザーは同一のユーザーとして識別される。
このようにエンティティはその属性によらず、識別子によって同一性が担保される。
識別子は可変にする必要はない。
class UserId: ...
class UserName: ...
class User:
def __init__(self, id: UserId, name: UserName) -> None:
self._id: UserId = id
self._name: UserName = name
def __eq__(self, user: "User") -> bool:
return self._id == user._id
if __name__ == "__main__":
user1 = User("id_1", "test_user")
user2 = User("id_1", "test_user2")
user3 = User("id_2", "test_user1")
print(user1 == user2) # True
print(user1 == user3) # False
エンティティの判断基準としてのライフサイクルと連続性
値オブジェクトとエンティティはともにドメインの概念を表現するため、その使い分けには判断基準が必要となる。
そのエンティティを利用する際の判断基準が以下の 2 つ
- ライフサイクルがある
- 連続性がある
今までの例であったユーザーはユーザー登録により作成され、利用終了に伴い削除される概念である。
また登録から削除まではその属性に変更があったとしても同一のユーザーとして扱われるため連続性も持っている。
そのためユーザーはライフサイクルがあり連続性があるオブジェクトであるため、エンティティとして実装されるべきである。
上記2つの判断基準に合致しない場合は、基本的には値オブジェクトを利用するのがよい。
可変なオブジェクトは取り扱いに注意が必要となり、システムのバグ等が潜り込みやすくなる。
値オブジェクトとエンティティのどちらにもなりうるモデル
同じ概念でも値オブジェクトとすべき場合やエンティティとすべき場合がある。
これはドメインがその概念をどのように扱うのかによる。
車にとってはタイヤはパーツであり、交換が可能なため値オブジェクトとして表現できる。
しかし、タイヤ製造工場などではロット単位や個体単位で管理を行うことが大事となるため、エンティティとして実装すべき概念となりうる。
同じものごとを題材にしても、それを取り巻く環境によってモデルに対する捉え方は変わります。値オブジェクトにも、エンティティにもなりえる概念があることを認識し、ソフトウェアにとって最適な表現方法がいずれになるのかは意識しておくとよいでしょう。
成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (p.108). 株式会社翔泳社. Kindle 版.
ドメインオブジェクトを定義するメリット
- コードのドキュメント性が高まる
- ドメインにおける変更をコードに伝えやすくなる
コードのドキュメント性が高まる
ドメイン駆動開発ではドメインの知識をドメインモデルに落とし込み、それをドメインオブジェクトとして実装する。
そのためドメインに関する知識・ルールはドメインオブジェクト内にそのまま記述(実装)されることとなる。
つまりコードを読み解くことにより、ドメインの知識やルールを理解することができる。
ドメインにおける変更をコードに伝えやすくなる
ドメインモデルをもとにドメインオブジェクトを実装することで、ドメインに関する知識の実装箇所が明白になる。
そのためドメインにおける変更の修正箇所はドメインオブジェクトのみとなり修正が容易となる。
ドメインオブジェクトを実装しない場合は、ドメインモデルにおける知識・ルールが様々なところに顔をのぞかせるととなる。
そのため修正箇所の把握だけでも困難なものになる。
Chapter4 不自然さを解決する「ドメインサービス」
サービスが指し示すもの
サービスとはクライアントのために何かを行うオブジェクトのこと。
ドメイン駆動においては 2 つに分けられる。
- ドメインのためのサービス(ドメインサービス)
- アプリケーションのためのサービス(アプリケーションサービス)
ドメインサービスとは
値オブジェクトやエンティティに実装すると不自然になる振る舞いが存在する。
この不自然さを解決するのがドメインサービス。