初めに
ドメイン駆動設計を取り入れたプロジェクトに携わり出したので、入門書を読んでのアウトプットを行う。
使用した入門書はこちら
また、普段はGoを用いて開発しているため、書籍ではC#を用いている箇所をGoに書き換えている。
ドメイン駆動設計とは
ドメイン駆動設計とは、ソフトウェアで解決したいリアルの領域の知識をソースコードに落とし込む設計思想である。
ドメインとは
ドメインとは「領域」のことで、開発者が解決したいリアル世界の領域のことである。
事業でソフトウェア・プログラミングを用いる場合には、利用者のドメインでの問題を解決するために行われることが多いだろう。
例えば飲食店で例えるのなら、「店員」「会計」「食品」「土地」「店舗」などが挙げられる。
実際に開発する際に、ドメインに焦点を向けるべくドメイン駆動設計を用いる。
モデルとは
モデルとは、現実の事象や概念を抽出したものである。
これをモデリングと呼ぶ。
例えば、先ほどの「店員」というドメインであれば、「お客様を接客し、商品を作り出す」ということを抽出して表現できる。
ドメインオブジェクト
ドメインモデルは抽象化した知識であるため、それだけでは問題を解決することはできない。
そこで、ドメインモデルをソースコードに落とし込むためにドメインオブジェクトがある。
リアルのドメインは、人々の営みによって移ろいゆくものである。
ドメインが変化した場合には、まずドメインモデルが変化するだろう。
ドメインモデルが変化するため、ドメインオブジェクトとの差異が生まれる。
その差異こそが修正するべき点になる。
ドメインやドメインモデルの理解の曖昧さは実装ミスにつながり、リアルドメインにも障害を来す可能性がある(実際に運用しているシステムのバグなど)
そのため、ドメイン駆動設計は変化に強く、長期的に運用を考えるシステムに向いている。
また、リアルドメイン、ドメインモデルの解像度を上げる必要があるが、開発者だけでリアルドメインの解像度を上げることは困難であるため周りの協力を仰いでドメインモデルを作り出す必要がある。
下図のようにリアルのドメインとドメインオブジェクトはモデルを介して双方向に影響を与える関係性になっている。
参照: 成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Kindle の位置No.512)
値オブジェクト
ドメイン駆動設計における値オブジェクトとは不変なオブジェクトであり、そのオブジェクト自体よりも値が何であるかが重要であるものを表現するためのオブジェクトである。
単純な数値や文字列などのプリミティブなデータ型では表現しきれないドメイン上の概念を表現するために使用される。
例えば、日付、金額、住所、電話番号などは、値オブジェクトとして表現されることが多い。
不変
もし値オブジェクトである日付が変更されるとどうなるだろう。
例えば、日付に紐づいている会議やタスクなどがあるとすると会議の時間が変わったり、タスクの期限が変わってしまうことになる。
そのため、値オブジェクトは不変にする必要がある。
交換可能
値オブジェクトは不変であるが、同じ値を表すオブジェクト同士であれば交換することが可能である。
例えば、以下のような住所が同じ値を表すオブジェクトである。
- 東京都港区六本木1-1-1
- 東京都港区六本木1-1-1 ミッドタウンタワー
こちらは同じ値であるが、表現が異なるため交換しても問題ない。
等価性
この等価性に関して完璧に理解することができなかったので機会があれば再度確認したいが、ざっくり以下のことが書かれていた。
値オブジェクトは、自身が持つ属性に基づいて等価性を判断する必要がある。
属性を取り出して比較するだけでは、属性の値が変更された場合に等価性が維持できなくなる可能性がある。
javaだと、以下のようにequals
を使用して等価性を判断するべきだとある。
address1 == address2 // equalsを使うべき
address1.equals(address2)
値オブジェクトをいつ使用するか
値オブジェクトをいつ使用するかは厳密には決まっていないが、ドメインの概念を表現するために不変な値を用る場合などに使用する。
逆に単純なスカラー値を表現する場合などは用いる必要はない。
値オブジェクトを用いるということはクラスの数も増えるということになるが、必要であれば使用するべきと本書では記載があった。
振る舞いをもつ
値オブジェクトは振る舞いを持つ。
例えば、金額を表現する値オブジェクトの場合は値が負の値でないことを確認する振る舞いを持つ。
日付を表現する値オブジェクトだと、日付同士の差分を求める振る舞いを持つ場合などがある。
逆にオブジェクトに定義されていない振る舞いは、そのオブジェクトができないということになる。
値オブジェクトを採用する際のモチベーション
値オブジェクトを採用する際のモチベーション・メリットとして、本書では以下の4つが挙げられている。
- 表現力を増す
- 不正な値を存在させない
- 誤った代入を防ぐ
- ロジックを散在を防ぐ
表現力を増す
値オブジェクトを採用する際に表現力が増すというメリットがある。
例えば、機械などの製品番号を例に見る。
値オブジェクトをプリミティブな値で表現した場合は以下のようになる。
var modelNumber string = "a20421-100-1"
上記だと文字列が3種類連なっていることはわかるが、modelNumberが具体的に何を表しているかが分からない。
type ModelNumber struct {
productCode string
branch string
lot string
}
値オブジェクトとして表現すると具体的に何を表している値なのかがわかりやすくなる。
不正な値を存在させない
値オブジェクトを用いることで不正な値を存在させないことを実現できる。
例えば、アプリのユーザー名を例にとる。
そのアプリのユーザー名は3文字以上である必要がある。
その場合、開発者は3文字以上であるかのバリデーションをユーザー名を用いた箇所で必ず記載しなければならないが、以下のように値オブジェクトを用いることで異常な値をチェックし、ルールに従っていいない不正な値の存在を排除することができる。
type UserName struct {
value string
}
func NewUserName(value string) (*UserName, error) {
if value == "" {
return nil, errors.New("value cannot be null")
}
if len(value) < 3 {
return nil, errors.New("user name must be at least 3 characters long")
}
return &UserName{value: value}, nil
}
func (u UserName) Value() string {
return u.value
}
誤った代入を防ぐ
プログラムをしているときに代入をすることがよくある。
その際に、正しいと思った代入が間違っていることを防止するべきとある。
例えばユーザーIDを代入するとする。
var userID string = "user1234"
では、このuserIDが本当に正しい代入なのだろうか。
ユーザーIDはシステムによってメールアドレスなのかユニークなIDなのか異なる場合がある。
例で挙げたuser1234
だと、一見正しい代入に見えるが、ユーザーIDがメールアドレスである場合は誤った代入になる。
そのため、ユーザーIDがメールアドレスで管理されていることをコードで表現できるようにする必要がある。
エンティティとは
ドメイン駆動設計のいけるエンティティとは、永続性(ID)を持ち一意のIDによって区別され、データベースに保存されたり、ネットワークを介して他のシステムとやり取りすることにも用いられる。
同じ識別子を持つエンティティは同一のものである。
また、エンティティはデータの状態や属性だけではなく振る舞い(メソッド)を持つことができる。
例として、オンラインショッピングの商品が挙げられる。
商品は一意のIDや名前や価格などを所有している。
なおかつ、在庫の変更や価格変更などのメソッドも持ち合わせている。
また、人間も値オブジェクトだと同姓同名の人は同一人物だとされてしまうのでエンティティとして定義するべきである。
値オブジェクトとの違い
値オブジェクトは不変であり、同じ属性の値を持つ場合には等価である一方、エンティティは可変であり、一意の識別子によって区別される。
しかし、値オブジェクトもエンティティも同じドメインオブジェクトである。
値オブジェクトとエンティティの切り分け
値オブジェクトとエンティティの切り分けは、不変であるかどうかとライフサイクルがあるかどうかである。
ライフサイクルとは
オブジェクトが生成されて消滅されるまでの生存期間や状態のことを表す。
例えばユーザーなどは、新規に登録して更新もされ、退会をすると削除されるというライフサイクルを持っている。
ドメインオブジェクトを定義するメリット
ドメインオブジェクトを定義するメリットは以下の2点がある
- コードがドキュメント化してドメインのマクロな部分までをコードを見て理解できる
- ドメインの変更をコードに反映しやすくなる
上記メリットは、新規のシステム開発より保守開発によって役立つ。
コードのドキュメント化
システム開発は、常に同じ人間がコーディングするとは限らない。
例えば移動で新たな人が入ってくる場合もあるだろう。
その際に、マクロな仕様は仕様書を作成して対応できるがミクロな部分までは対応することが難しい。
そこで、コードを見てミクロな仕様をまで理解できるように、「コードを饒舌」にしてドキュメント化する。
例えば以下のUser構造体だと、Userに関する情報がstring
型のName
フィールドがあること以外の情報がなくて「無口なコード」になっている。
type User struct {
Name string
}
そこで下記のように、ポインタレシーバーを使用することで、Userが3文字以上であるということがコードを見て分かるようになる。
package user
type User struct {
Name string
}
func (u *User) Validation() bool {
return len(u.Name) >= 3
}
上記のように、コードにドメインの情報を落とし込んで「コードを饒舌」にすることで、マクロな仕様をコードを見て確認できるようになる。
上記例だと、「UserNameは5文字以上に変更する」ことになれば下記のように数字を変更するのみで対応することができる。
return len(u.Name) >= 5
ドメインサービス
概要
ドメイン駆動設計におけるサービスには大きく分けてアプリケーションのためのサービスとドメインのためのサービスの2つがある。
今回の章では、ドメインのためのサービスに焦点を当てて記載する。
ドメインサービスとは
ドメインサービスは、ドメインオブジェクトに記載するべきではない処理を切り分けて担当するサービスのことである。
例えば、ユーザーの重複を許可しないシステムがあるとする。
その際に、Userのドメインモデルに、重複の有無を自分自身に問い合わせることになる。
自分自身に重複すると本当に正しい値が返ってくるかわからない。
具体的には以下のような時に使用する。
- ドメインモデルが他のドメインモデルとの相互作用を行う必要がある場合
- ドメインモデルや他のアプリケーション層から呼び出されることがあるためインターフェイスを提供する
- ドメインモデルからの疎結合性を維持する
- ドメインサービスはドメインモデルから依存を受けることがあるが、逆の依存は持たない。これにより、ドメインモデルの疎結合性を保ちつつ、ドメインサービスの再利用や交換が容易になります。
また、ドメインサービスには上記のようなケースのみ使用して基本的に、ドメインオブジェクトに記載できるところはドメインオブジェクトに記載するべきである。
ドメインサービスにロジックを持たせすぎるとロジックが混在しコードが読みづらくなる。
基本的には、開発者がドメインオブジェクトを見てコードのミクロな仕様を理解できるコーディングが望ましい。
リポジトリ
リポジトリは、ドメインオブジェクト(エンティティや値オブジェクトなど)をデータベースや永続ストレージに保存し、取得するためのインターフェースと実装を提供する。
オブジェクトとDBのやりとりをしたい場合はリポジトリを介すようにするべきである。
引用成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.145)
また、リポジトリのインターフェイスはdomain層に定義して実際のメソッドはインフラ層に書くべきである。
そして、Find(ctx)
のようなメソッドの呼び出しはアプリケーション層からするべきである。
domain層
type Repository interface {
Find() ([]*User, error)
}
インフラ層
func (m *UserRepository) Find() ([]*User, error) {
// ここに具体的なユーザー検索のロジックを実装する
return []*User{}, nil
}
アプリケーションサービス
アプリケーションサービスは、ドメインとアプリケーションの仲介役である。
具体的には以下の役割がある。
- ユーザからの入力を受け取り、適切なドメインオペレーションを呼び出す
- 複数のドメインオペレーションをまとめてトランザクション管理する
- リポジトリを介してドメインオブジェクトの作成・更新などをする
また、アプリケーションサービスにはドメインオブジェクトのタスク管理に徹するべきであり、ロジックが混在するのでドメインルールは記載するべきではない。
依存関係のコントロール
依存関係を軽くするためにはインターフェイスを用いることが有効である。
依存関係逆転の原則
依存関係逆転の原則とは以下のことである。
- 上位レベルのモジュールは、下位レベルの具体的な実装に直接的に依存するべきではない。代わりに、両方のモジュールは共通の抽象化されたインターフェースや抽象クラスに依存するべき。
- 具体的な実装の詳細に依存するのではなく、モジュール間の関係性は抽象的なインターフェースやクラスに基づいて構築されるべき。これにより、異なる具体的な実装を交換する際にも依存関係を変更せずに済む。
上記を簡単に表すと「具体的な実装にではなく、抽象化されたインターフェースやクラスに依存することでシステムを柔軟にする」という原則になる。
上位レベルと下位レベルのモジュールとは
上位レベル:アプリケーションサービスなど
下位レベル:リポジトリなど
上位レベルが下位レベルのモジュールに依存してはいけない理由
例えばデータストアの変更を理由にビジネスロジックを変更するなどが起きるため
Dependency Injection
Dependency Injectionを用いることで、コンポーネントの依存関係を外部から依存性を注入することによって解決することができる。
解決することでコンポーネントの疎結合性が向上し、テストや保守性の向上などの利点が得られる。
ユーザーインターフェイス
ユーザーインターフェイスはアプリケーションの使用者がアプリケーションと対話するための媒体である。
ユーザーが情報を閲覧できるようにデータを受け取りシステムに入力するなどの役割を持つ。
また、データの表現形式やレイアウトを制御する。例えば、同じデータを表形式で表示したり、グラフで表示したりする場合、その表示形式を決定する。
基本的にはユーザーインターフェイスの影響がビジネスロジックに影響を与えることを防ぎ、より疎結合にシステムを構成するためにある。
ファクトリ
ファクトリとは、ドメインオブジェクトの生成を担当する設計パターンである。
以下のような時に利用する。
- 生成が複雑なオブジェクトを生成する
- 生成が複雑であったり必要なパラメータが多い場合、ファクトリを使用することで生成の詳細を隠蔽でき、ドメインオブジェクトをシンプルにして利用を簡単にする。
ファクトリはcreateOrder
などの明確な名称を持つメソッドを通してオブジェクトの生成をする。
ファクトリは以下のような使用方法になる。
User
オブジェクトをUserFactory
のCreate
メソッドを使用して生成している。
type User struct {
ID string
Name string
Age int
}
type UserFactory struct {}
func (f *UserFactory) Create(id string, name string, age int) (*User, error) {
user := &User{
ID: id,
Name: name,
Age: age,
}
return user, nil
}
集約
集約とは、エンティティと値オブジェクトのグループを1つにして表現するための概念である。
例えば、ユーザーの集約だと下図のようになる。
成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.401). Kindle 版.
集約を操作するには、集約のルート(Aggregate Root)と呼ばれるオブジェクトを介す必要がある。
newUserName := "NewName"
userName := NewUserName(newUserName)
user.Name = userName // NG
user.ChangeName(userName) // OK
ChangeName
などのメソッドを使用することで、nullチェックなどのバリデーションを行うことができる。
オブジェクトの操作の基本原則
デメテルの法則
デメテルの法則とは、あるオブジェクトはその直接的な隣人のみを知る必要があり、隣人の隣人は知る必要がないという考え方で、クラスやオブジェクトがどのように依存するかを明確にして直接関係のないオブジェクトとは依存しないというものである。
この考え方は、より疎結合にシステムを開発して保守性や再利用性を向上させることができる。
デメテルの法則によると、メソッドを呼び出すオブジェクトは次の4つである。
- オブジェクト自身
- 作成したオブジェクト(Factotyを使用して作成したオブジェクトなど)
- メソッドの引数として渡されたオブジェクト
- オブジェクトが保持しているオブジェクト
車に例えると、タイヤに直接命令するのではなく、Outgoing
などの発信する命令を呼び出すと勝手にタイヤが回るイメージである。
普通に車に乗る場合にタイヤを手動で動かすことはないだろう。
下手したら壊れてしまうかもしれない。
集約の大きさ
トランザクションはデータをロックするため集約が大きすぎるとその分多くのデータをロックすることになるので、基本的には集約は最小限の範囲に留めるべきである。
仕様
ビジネスルールをエンコードしたドメインオブジェクトの1つである。
仕様は、下記のようにビジネスルールを表現し、それが満たされているかを確認する。
仕様を用いることにより、ビジネスロジックが繰り返し書かれていたりするのを防ぎ、コードの再利用性と可読性を向上させる。
type User struct {
Age int
}
type UserSpecification struct {
minAge int
}
func (spec *UserSpecification) IsSatisfiedBy(user User) bool {
return user.Age >= spec.minAge
}
alice := User{Age: 20}
userSpec := UserSpecification{minAge: 18}
fmt.Println("Is Alice old enough? ", userSpec.IsSatisfiedBy(alice))
上記だと、ユーザーの年齢の最小値があるシステムにおいてその確認をしている。
確認する場合はIsSatisfiedBy
メソッドを使い回すことができる。
また、仕様はリポジトリと一緒に使用されることが多く、リポジトリのメソッド内でビジネスルールを確認する際に用いられる。
ソリューション
実際にドメイン駆動設計のドメインレイヤーをどのように扱うかを記載する。
成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (p.541). Kindle 版.
- Domain.Modelにはドメインオブジェクトが配置される。集約を構成するエンティティや値オブジェクト、ファクトリやリポジトリ、仕様もここに属する。
- Domain.Serviceにはドメインサービスを配置する
まとめ
ドメイン駆動設計を簡単にまとめてみた。
個人的に、今まで設計思想に触れたことがなく、リアルドメインが変更された場合への対応のしやすさやコードを疎結合にすることでテストがやりやすくなったり改修が容易になるなど多くの利点があるように感じた。
ただ、ドメイン駆動を取り入れる際に思想に疎はないもの以外は受け入れないようなコードには限界があるように思うので、必要に応じて柔軟にコードを書くべきだと感じた。