プログラミング学習 書籍

「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本」まとめ後編

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 のまとめ後編。前編はこちら

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

モジュール間の依存関係のコントロールについて書かれている。

依存関係逆転の原則

依存関係逆転の原則

  • 上位レベルのモジュールは下位レベルのモジュールに依存してはならない。どちらのモジュールも抽象に依存すべき
  • 抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべき

レベルは入出力からの距離を表す。低レベルは機械に近い具体的な処理を指し、高レベルといえば人間に近い抽象的な処理を指す。依存関係逆転の原則で言われている上位レベル・下位レベルの考え方も同じ。

例えば、データストアを操作するRepositoryは下位レベル、ユースケースを実現するアプリケーションサービスは上位レベル。

「上位レベルのモジュールは下位レベルのモジュールに依存してはならない。どちらのモジュールも抽象に依存すべき」ってのは↓感じ。

依存関係

Goだとこんな感じ。

type User struct {
	ID UserID
	Name string
}
// IUserRepository インターフェース
type IUserRepository interface {
	Find(id UserID) (*User, error)
	Save(u *User) error
}
// UserRepository はIUserRepositoryの実装
type UserRepository struct {
	// RDBMSを扱うRepository
}
func (r *UserRepository) Find(id UserID) (*User, error) {
	// 取得処理
}
func (r *UserRepository) Save(id *User) error {
	// 保存処理
}
// UserApplicationService はアプリケーションサービス
type UserApplicationService struct {
	Repository IUserRepository // インターフェースに依存
}
// NewUserApplicationService はアプリケーションサービスのコンストラクタ
func NewUserApplicationService(repo IUserRepository) *UserApplicationService {
	return &UserApplicationService{
		Repository: repo,
	}
}

「抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべき」ってのはどういう意味かというと、下の引用の通り。

一般的に抽象型はそれを利用するクライアントが要求する定義。IUserRepositoryはUserApplicationServiceのために存在していると言っても過言ではありません。このIUserRepositoryという抽象に合わせてUserRepositoryの実装を行うということは、方針の主導権をUserApplicationServiceに握らせていることと同義です。。「抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべき」という原則はこのようにして守られます。

「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 p167」より引用

なんで抽象に依存するようにしたら良いのかというと

抽象が詳細に依存すると、低レベルのモジュールの変更が高レベルのモジュールに波及してしまう。重要なドメインルールが含まれているのは高レベルなモジュールなので、低レベルなモジュールの変更が波及するのはおかしい。(例えば、データストアの変更がビジネスロジックの変更に波及するのはおかしい)
インターフェースはそれを利用するクライアントが宣言するもので、主導権はクライアントにある。インターフェースを宣言して低レベルのモジュールはそれに合わせて実装することで、高レベルなモジュールに主導権を握らせることが可能。

依存関係が増えてくるとコンストラクタの引数増えたりして見通しが悪くなるのでIoC Containerを使うと良い。
(ライブラリとかもあるよねー。ここらへんはあとで勉強....)

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

1〜7章の内容をふまえてソフトウェアを組み立てる方法・流れをサンプルコード多めで解説している。
コントローラーにこんな処理きて、こんなの返すよねーみたいな話。
この章はまとめづらいので割愛(1〜7章のまとめみたいなものだし)

9章 複雑な生成処理を行うファクトリ

ファクトリはオブジェクト生成に関わる知識がまとめられたオブジェクト。オブジェクト生成に複雑な手順を必要とする場合、モデルを表現するオブジェクトに無理やり実装するよりも、ファクトリを使った方がコードの意図が明確となる。

本来、オブジェクトの生成はコンストラクタの役目だが、コンストラクタは単純である必要がある。もし複雑になってしまうならファクトリを定義する。「コンストラクタ内で他のオブジェクトを生成するか」はファクトリを作る判断の1つとして使える。

もし、ドメインオブジェクトに定義するのが適切であれあば、ファクトリとして機能するメソッドを作っても良い。

10章 データの整合性を保つ

データの整合性を保つ方法の紹介。

  1. ユニーク制約をつける
  2. トランザクションを使う
    • トランザクションスコープを使う
    • UnitOfWorkを使う

11章 アプリケーションを1から組み立てる

今までの復習の章。ドメインオブジェクトを作って、ドメインサービス作って、リポジトリ、ユースケース作ってって話をサンプルコード交えて解説。

復習なので割愛。

12章 ドメインのルールを守る「集約」

データを変更するための単位として扱われるオブジェクトの集まりを「集約」という。

複数のオブジェクトがまとめられ、ひとつの意味をもつオブジェクトが構築されることがある。このオブジェクトグループには維持されるべき不変条件が存在する。この不変条件を維持するのに「集約」が役立つ。

集約の定義

  1. 集約は不変条件を維持する単位として切り出され、オブジェクトの操作に秩序をもたらす
  2. 集約には境界とルートが存在する。境界は集約に何が含まれるのかを定義するための境界。ルートは集約に含まれる特定のオブジェクト。
  3. 外部からの集約に対する操作はすべてルートを経由して行われる。集約内の境界内に存在するオブジェクトを外部に出さないことで、集約内の不変条件を維持する。

集約のイメージ図。(図では値オブジェクトをまとめているけど、値オブジェクトに限った話ではない)

ポイント

ポイントは定義の3番「外部からの集約に対する操作はすべてルートを経由して行われる。

図の例で言うと、ユーザー名を変更するメソッド(ふるまい)はUserNameではなく集約ルートのUserに定義して外部に公開するべき。

外部から内部のオブジェクトに対して直接操作するのではなく、それを保持するオブジェクトに依頼する形をとることで直感的になり、かつ不変条件を維持することができる。「デメテルの法則」としても知られている。

集約をどのように区切るか

集約をどのように区切るかの方針としてメジャーなのは「変更の単位」。集約に対する変更はあくまでその集約自身に実施させ、永続化の依頼も集約ごとに行われる必要がある。そのため、リポジトリも変更の単位である集約ごとに用意する。

集約はなるべく小さく保つべき。巨大な集約ができてしまったら、集約の境界線を見つめ直した方が良い。

また、複数の集約を同一トランザクションで操作することも可能な限り避ける。複数の集約にまたがるトランザクションは巨大な集約と同様に広範囲なデータロックを引き起こす可能性を高める。

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

オブジェクトを評価するドメインオブジェクト

仕様はオブジェクトの評価を行うドメインオブジェクト

オブジェクトの評価は単純なものであればメソッドで十分(IsDeleted とか)だが、複雑だったり、オブジェクトのメソッドとして定義されるには似つかわしくないもの存在する。

そういった複雑な評価はアプリケーションサービスに書かれがちだが、オブジェクトの評価はドメインの重要なルールであるため、サービスに書かれるべきではない。また、サービスに書かれると似たような評価処理が各所に散らばりやすくなる。

オブジェクトの評価方法はメソッドに限った話ではなく、オブジェクトの評価を外に切り出す(仕様オブジェクトを作る)ことで扱いやすくなることもある。

type User struct {
	ID     UserID
	Name   string
	Status string // 登録ステータス(仮登録・本登録・退会)
}

type UserSpecification struct {
	user *User
}

func NewUserSpecification(u *User) *UserSpecification {
	return &UserSpecification{user: u}
}

// IsSatisfyRegistered 本登録済みユーザーかどうか
func (s *UserSpecification) IsSatisfyRegistered() bool {
	// これくらいならドメインオブジェクトのメソッドで十分。
	// もっと複雑な評価処理を書く
	return s.user.Status == "Registered"
}

func (s *UserSpecification) IsSatisfyXxx() bool {
	// 複雑な評価処理
}

サンプルコードはシンプルな評価処理なのでドメインオブジェクトにメソッドを用意すべきだが、複数のドメインオブジェクトが絡むような複雑な評価処理の場合に仕様パターンが役立つ場合がある。

type UserXxxSpecification struct {
	user *User
	plan *Plan
	team *Team
}

func (s *UserXxxSpecification) IsSatisfyXxxx() bool {

}


仕様の検索用法

仕様には評価だけでなく「検索(選択)」としての用法もある。

リポジトリに検索を行うメソッドを定義するが、検索処理の中に重要なドメインルールを含む場合がある。こうした場合リポジトリに重要なドメインルールが記述されてしまう。重要なドメインルールを仕様オブジェクトとして定義してリポジトリに引き渡すことで、リポジトリにドメインルールが記述される状況を避けることができる。

// 仕様(specification)
// リポジトリのメソッドにわたすインターフェース
type IUserSpecification interface {
	IsSatisfy(user *User) bool
}

type XxxUserSpecification struct {
	user *User
}

func (u *XxxUserSpecification) IsSatisfy(u *User) bool {
}

type YyyUserSpecification struct {
	user *User
}

func (u *YyyUserSpecification) IsSatisfy(u *User) bool {
}

type ZzzUserSpecification struct {
	user *User
}

func (u *ZzzUserSpecification) IsSatisfy(u *User) bool {
}

// リポジトリ
func (repo *UserRepository) GetList(spec IUserSpecification) ([]User, error) {
	users := repo.db.getAll() // 全件取得
	var result []User
	// DBから取得したデータをspecで評価してフィルター
	for _, u := range users {
		if spec.IsSatisfy(u) {
			result = append(result, u)
		}
	}
	return result, nil
}

// リポジトリの呼び出し元
spec := NewXxxSpecification()
users := userRepo.GetList(spec)

確かにこれならリポジトリがドメインルールだらけになるのは防げるが、取得するデータ数が多いときのパフォーマンスの懸念がある。

パフォーマンスに懸念のある読み取り(クエリ)の場合はドメインオブジェクト外にルールが流出してしまうのをある程度許容するという考えもある。
(CQSやCQRS)
クエリはリポジトリに記述するのではなく、アプリケーションレイヤーのサービスからクエリビルダーを介して直接クエリを発行する。

14章 アーキテクチャ

DDDと一緒に語られるアーキテクチャの紹介をしている。アーキテクチャと簡単な概念の紹介。

  • レイヤードアーキテクチャ
  • ヘキサゴナルアーキテクチャ
  • クリーンアーキテクチャ

ここはアーキテクチャの概念紹介程度なので割愛。気になるアーキテクチャがあれば別途その書籍を読んだ方が良いと思う。

15章 ドメイン駆動設計のとびらを開こう

今後の学習の手引とドメイン駆動設計で重要な概念のさわりを紹介している。

ポイント

  • ドメイン駆動設計に登場するパターンだけを取り入れる軽量DDDに陥らないようにする
    • どう表現するか、どのパターンを使うか」ばかりを考えないこと。
    • 重要なのはドメインの本質に向き合うことで、技術的なパターンはコードで表現するときのサポート役。
  • ドメインエキスパートとモデリングする
    • ドメインエキスパートと対話するときにシステマティックな難しい言葉を使わないこと
    • ドメインエキスパートの言葉をすべて鵜呑みにするのではなく、その中から本当に解決したいこと、重要な知識を見つけ出す。
  • ユビキタス言語を使う
    • 認識を合わせて意思の疎通を図るのに有用
    • ユビキタス言語はコードを書くときの表現(命名)にも反映する
  • 境界づけられたコンテキスト
    • ドメインにも境が存在し、その境を越えるとユビキタス言語も変わる
    • 同じ言葉であっても意味が少し異なる場合や言葉が異なっても同じものを指しているという状況もある。ユビキタス言語の定義が揺れているのでなければ、複数のコンテキストの境界にいる可能性がある。
    • 例えばSNSで投稿やグループを作る「ユーザー」と認証を行うシステム上の「ユーザー」では言葉が同じでも背景・目的が異なる。こういった場合は、ドメインオブジェクトを分けて定義すると良い。(パッケージを分ける)
    • システムが大規模になるにつれて、統一したモデルを作ることは困難。無理に統一すると巨大で複雑なモデルになってしまうので、それぞれのコンテキストの事情にあった複数のモデルにすると変化に強くなる。

参考図書

ドメイン駆動設計の概要、前提知識・用語を理解しやすい良書でした。

次は実践ドメイン駆動設計エリック・エヴァンスのドメイン駆動設計に再挑戦してみようと思います!!

エヴァンスのドメイン駆動設計に挫折した人やドメイン駆動設計の土台となる基礎知識を固めたい人はぜひ読んでみてください。

-プログラミング学習, 書籍