「エリック・エヴァンスのドメイン駆動設計をちゃんと読んでないなー」、「読まなきゃなー」と思い、その準備として「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」を読んだので内容まとめ。
目次
「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」とは?
ドメイン駆動設計を手軽に理解したい初心者向けの書籍。この書籍のゴールは、ドメイン駆動設計を理解するために必要な前提知識や概念、用語を理解し、ドメイン駆動設計の本質を学ぶための準備を完了させること。
エリック・エヴァンスのドメイン駆動設計を読む前の準備段階として、この「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」を読むと良いと言われています。
こんな方におすすめ
- ドメイン駆動設計の基礎知識を理解したい
- エリック・エヴァンスのドメイン駆動設計を読むのがしんどかった人・準備をしたい人
1章 ドメイン駆動設計とは
ドメインの知識に焦点をあてた設計手法。
ソフトウェア開発におけるドメインは「プログラムを適用する対象となる領域」のこと。「ドメインが何かではなく、ドメインに含まれるものが何か」が重要。
ソフトウェアの目的は利用者のドメインにおける何らかの問題解決であり、そのためには、利用者の考えや視点、取り巻く環境、直面している問題を正確に理解する必要がある。
「知識に焦点をあてる」ということは、ドメインの概念や事象を理解して、その中から問題解決に役立つものを抽出して得られたものをソフトウェアに反映していくということ。
用語整理
- モデル
現実の事象や概念を抽象化した概念。抽象化した概念なので、現実をすべて忠実に再現したものではない。必要に応じて取捨選択を行う。何を取捨選択するかはドメインによる(何に重きを置くかが異なってくるため) - モデリング
モデルを作るために事象や概念を抽象化する作業 - ドメインモデル
ドメイン(プログラムを適用する領域)の概念をモデリングして得られたモデル。ドメインモデルはあくまでも概念を抽象化して知識。 - ドメインオブジェクト
ドメインモデル(概念を抽象化した知識)をソフトウェアで動作するモジュールとして表現したもの。ドメインモデルの実装表現(コード)
2章 値オブジェクト
値オブジェクトはドメインオブジェクトの一種。システムに登場する単価や商品番号、IDといったシステム固有の値を表現するために定義されたオブジェクトのこと。
システムに最適な値が必ずしもプリミティブな値とは限らない。必要とされる処理に応じて適切な値の表現があるはずと思ったら値オブジェクトを定義することを検討すると良い。
値オブジェクトを利用することで表現力が増し、ロジックの散在を防ぐことができる。
値オブジェクトのイメージ
氏名を文字列型の値で扱っていると、姓・名を分けて取得したいときに「半角スペースで文字列を分割する」、「分割した文字列の1つ目を名とする」などの処理が必要になる。
これを氏名を表現する値オブジェクト(FullNameクラス)を定義して、そこに「姓を取得するメソッド」、「名を取得するメソッド」を定義するとスッキリする。「半角スペースで姓・名を区切る」、「名が前で姓が後」といったルールがコード内に散らばるのを防げ値オブジェクト内に閉じ込めることが可能。
値の性質
値オブジェクトは「値」であるため、値の性質を持つべき。
値の性質
- 不変
値は不変。1という数は1のまま不変だし、"こんにちは"という文字列も不変。 - 交換可能
値の変更したいときは代入を使う。代入は値自体を変更しているわけではなく、変数の内容を変更しているだけ。つまり、値は代入によって交換可能といえる。 - 比較可能
値は等価性によって比較できる
特に気をつけるべきと思った点は、交換可能と不変。値オブジェクトは自身の状態を変更するふるまい(メソッド)を持つべきではない。値を変更したい場合は代入による値の交換を行う。
値オブジェクトにする基準
どこまで値オブジェクトにするかはコンテキストによる。
著者は「そこにルールが存在しているか」、「それ単体で扱いたいか」を重要な判断基準としている。
3章 エンティティ
エンティティとは、属性ではなく同一性によって識別されるドメインオブジェクト。
※ ER図のエンティティとは意味が異なるので注意。
同一性で識別?
人間の場合、年齢、身長、体重といった属性が変化したからといって別の人間になるわけではない。
同じ人間だと識別するための何かが存在しているはず。
ソフトウェアの世界では何かしらのID(識別子)が同一性を担保することが多い。
例えば、ユーザ情報が更新されたからと言って別のユーザーとなることはなく、ユーザーIDで同一性を識別する。なので、ユーザーはエンティティと言える。
エンティティの性質
エンティティは属性ではなく同一性によって識別されるドメインオブジェクト。
エンティティと値オブジェクトは共にドメインモデルの実装であるドメインオブジェクトであるが、性質は真逆にしたようなもの。
値の性質
- 可変
エンティティの属性は変化することが許容されている。年齢、身長、年収など。もちろん不変のものもある。 - 同じ属性でも区別される
例えば氏名や生年月日が同じユーザーがいても別のユーザーだと区別される。 - 同一性によって区別される
属性ではなく、識別子(ID)で同一かどうか区別される。
値オブジェクトは不変であり、値の交換は代入を使うべきだったため、値を変更するふるまいを持つべきではなかった。一方、エンティティの属性は可変であるため、ふるまい(メソッド)を通じて属性を変更するべき。
エンティティにする判断基準
値オブジェクトもエンティティもドメインの概念を表現するドメインオブジェクトである。値オブジェクトとエンティティのどちらにするかを判断する基準の1つに「ライフサイクルが存在し、そこに連続性が存在するか」がある。
例えばユーザーには、ユーザー登録 → ユーザー情報の変更 → 退会というライフサイクルがあり、連続性のある概念と言える。
もし「ライフサイクルを持たない」 or 「システム上ライフサイクルを表現する意味が無い」場合には値オブジェクトとして扱うと良い。
注意ポイント
- 同じ概念でもシステムによっては値オブジェクトにすべきときもあればエンティティにすべきときもある。
物事をどの側面からみるかによって扱い方が変わってくる。 - エンティティを実装しようとして曖昧さ・不自然さ・違和感を感じたら、ドメインの捉え方を見直すきっかけ
4章 サービス
ソフトウェアにおけるサービスとは「クライアントのために何かを行うオブジェクト」
ドメインサービスは自身の振る舞いを変更するようなインスタンス特有の状態を持たないオブジェクトで、値オブジェクトやエンティティに記述すると不自然になってしまうふるまいを定義する。(複数のドメインにまたがる処理などはドメインサービスが使える可能性大)
ドメインサービスもドメインモデルのコード上の表現であり、括りとしては値オブジェクトやエンティティと同一。そのため、ドメインサービスは入出力(DB操作)を伴う処理を取り扱わないようにすべき。
著者は可能な限り入出力を伴う操作を持たせないのには賛成だが、それを考慮した上で必要であれば入出力操作をドメインサービスに持たせることも厭わないという考え。
注意ポイント
- ドメインサービスは自身の振る舞いを変更するようなインスタンス特有の状態を持たないオブジェクト
- ドメインサービスを使うのは、値オブジェクトやエンティティに定義すると「不自然なふるまい」に限定すること。濫用禁止
- 何でもかんでもドメインサービスに定義してしまうと、エンティティや値オブジェクトにどんな「ふるまい」、「ルール」があるのか不透明になる
- 入出力操作(DB操作)を持たせない
5章 リポジトリ
リポジトリはデータストアにまつわる、永続化(保存)や再構築(復元)を担う。
オブジェクトのインスタンスを保存・取得したいときは、直接データストアに読み書きするのではなく、リポジトリに依頼する。複雑になりがちなデータストア操作をリポジトリに任せることで、ドメインオブジェクトの見通しを良くすることができる。
リポジトリが扱うデータストアはRDBでもNoSQLでもファイルでも何でも良い。むしろデータストアの差し替えを容易にしてくれるもの。
type IUserRepository interface {
Find(id UserID) (*User, error)
Save(u *User) error
}
リポジトリのインターフェイスを定義しておき、MySQLを使うリポジトリを実装する。もしMongoDBを使うことになれば、新たにMongoDBを使うリポジトリを実装すれば差し替えが容易。メモリ上に保存するリポジトリを実装しておいて、テストのときにはこのリポジトリを使うとしておけば、面倒なテスト用DB構築処理も不要になる。
リポジトリに定義されるふるまい
リポジトリの責務はドメインオブジェクトの永続化・再構築であり、ドメインのルールをリポジトリに実装するべきではない。
リポジトリの永続化のふるまいは、永続化を行うオブジェクトを引数に取る。
type IUserRepository interface {
Find(id UserID) (*User, error)
Save(u *User) error
UpdateName(id UserID, name string) error // <- NG
}
値を更新したい場合は、エンティティに属性を変更するように依頼して、更新後の状態をリポジトリで永続化する。
↓イメージ
user.ChangeName(name)
repository.Save(user)
再構築(データ検索)のふるまいは基本的には識別子による検索メソッド。(例: FindByID
)
ただし、データ検索系のふるまいにはパフォーマンス問題もあるため、識別子以外で検索するメソッドを状況に応じて定義する。(例: ユーザー名で検索したいときにはFindByName
を定義する)
6章 アプリケーションサービス
ドメインオブジェクトを組み合わせてユースケースを実現するものがアプリケーションサービス。ドメインオブジェクトが行うタスクの進行を管理し、ソフトウェア利用者の問題を解決に導くもの。
アプリケーション固有の機能・ふるまいを実現するのがアプリケーションサービス。
ドメインオブジェクトを公開する?しない?
例えば、ユーザー取得処理を行うアプリケーションサービスで取得したユーザーを返却するとき、「ユーザーのドメインオブジェクトをそのまま返すかどうか」という話。
ドメインオブジェクトを返却する(公開する)場合、コードはシンプルになるが、アプリケーションサービス以外からドメインオブジェクトのふるまい(メソッド)を呼び出せ、ドメインオブジェクトに対する多くの依存が生まれやすくなる。(本来、ドメインオブジェクトのふるまいを呼び出す役目はアプリケーションサービスの役目)
解決策は、ドメインオブジェクトを返却ドメインオブジェクトの代わりに、データ転送用オブジェクト(DTO, Data Transfer Object)にデータを移し替えて返却する。
// Userのデータを公開するためのオブジェクト(DTO)イメージ
type UserData struct {
id UserID
Name string
Age int
}
func NewUserData(u User) UserData {
return UserData{...}
}
Userのデータを公開するためのオブジェクトなので、Userに依存していても問題なし。
ドメインオブジェクトを公開したところで即座に問題が起きるわけではない。非公開にしたときコード量が量が増える煩わしさもある。どちらを採用するかはプロジェクトのポリシーによる。
ドメインのルールを記述しない
アプリケーションサービスはドメインオブジェクトのタスク調整に徹するべきで、ドメインのルールを記述すべきではない。同じようなコードを点在させることにつながる。
ドメインにふるまいをもたせたり、ドメインサービスを用意して、アプリケーションサービス内にドメインルールを記述しないようにする。
サービスは状態をもたない
サービスは自身のふるまいを変化させる目的で状態を保持しない。このような状態をもってしまうと、サービス利用時に今どんな状態なのかを気にする必要が出てしまう。
自身のふるまいを変化させる目的の状態の例
type UserRegisterService struct {
repo UserRepository // ふるまいを変更しない状態はOK
sendMail bool
}
func (s *UserRegisterService) Execute() {
// 色々やる
if sendMail { // 状態によって処理が変わる
}
// 色々やる
}
後編につづく....
こちらもCHECK
-
-
「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」まとめ後編
ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 のまとめ後編。前編はこちら リンク 目次7章 柔軟性をもたらす依存関係のコントロール依存関係逆転の原則8章 ソフトウェアシステムを組 ...
続きを見る
今回紹介している書籍
こんな方におすすめ
- ドメイン駆動設計の基礎知識を理解したい
- エリック・エヴァンスのドメイン駆動設計を読むのがしんどかった人・準備をしたい人