ドメイン駆動設計をはじめよう―ソフトウェアの実装と事業戦略を結びつける実践技法

1章 事業活動を分析する
業務領域は以下の3つに分類される
- 中核の業務領域 : 競争優位になる。いわゆるコアコンピタンス
- 一般的な業務領域 : 他社も同じく有する類の業務で競争優位とはならない
- 補完的な業務領域 : 競争優位とはならない。あくまで中核の業務領域のサポート。
中間の業務領域は他者との差別化要素は大きいが、業務ロジックが複雑
ロジックも頻繁に変わりやすい
競争優位を保つ以上、持続的に発展させる必要があるからです
一般や補完の領域においては他社の外部サービスを利用することもある
業務領域とは関連するユースケースの集まり

2章 業務知識を発見する
同じ言葉で意図を適切に表し伝達する
同じ言葉をモデリングやプログラム上でも使っていく、認識を強固なものにしていく。曖昧さを減らす。
モデルとは、現実のものや出来事を簡略化した表現。特定の側面を意図的に強調し、一方で、それ以外の側面を意図的に除外します。モデルは用途を限定した抽象化です。
地図がわかりやすい。
世界地図に地下鉄の駅は不要。地図によって目的が異なり、目的に沿わない情報は捨象されてモデリングされている。

3章 : 事業活動の複雑さに立ち向かう
モデルで重要なのは解決しようとしている課題が何か。モデルには必ず境界がある。モデルの境界を広げすぎるとそのモデルは役に立たなくなる。
したがって、モデルを作るためには適切に区切られた境界が必要。
区切られた文脈によって同じ言葉の定義が完成する。同じ言葉は区切られた文脈の内側に限定される。
この章の例で挙げた、販売促進部門と営業部門で見込み客の対象の捉え方が異なる話があるが、適切に区切られた境界では異なる意味で使い分けられる

3.3 区切られた文脈と業務領域の関係
業務領域は相互に関連したユースケースの集まり、つまり『発見』であり、区切られた文脈はソフトウェア技術者の手によって分割される『設計』である
一つの業務領域に一つのモデルが対応するということではない。業務領域ごとに課題が複数あって、それぞれ区切られた文脈として分割できるケースもある。
業務の捉え方が異なる不整合に直面した場合、同じ言葉を複数の限られた文脈に分解してみる。
所感
事実と解釈に近いか

ルース・マランは、システム設計の本質は境界の決定と説く。
アーキテクチャの選択はシステムの設計です。システムの設計は文脈の設計です。文脈の本質は境界です。境界の内側に何があり、境界の外側に何があるか。境界を越えて、どうつながり、何が移動するか。何を選択し、何をあきらめるか。境界は、何が外部であり何が内部であるかを明らかにします
区切られた文脈がそのままモデルの境界を定義する

4章: 区切られた文脈どうしの連携
モデルの構築には、目的の特定、つまり境界が必要。境界は同じ言葉が通用する範囲を分割します。
区切られた文脈がモデルの境界を分ける。とはいえ、モデル自体が完全に独立するわけではなく、システムとして機能するには、コンポーネント同士の連携が必要
結果として、区切られた文脈の間には、必ず接合部分が存在しており、この接合部分を区切られた文脈間の契約と呼ぶ
契約が必要な理由は、異なる区切られた文脈同士では、モデルと言葉が異なるから。契約とは、利害が異なる複数の当事者間の取り決め。取り決めを明文化し、お互いの利益を調整することが必要。

利用する側が自分のモデルに合うように変換することをモデル変換装置
提供する側が利用側のニーズに最適化した公開された言葉を実装するのが共用サービス
言葉の定義としてはこのように分けられるけど、関心の対象が違うということだけ抑えておけば良さそう => 区切られた文脈同士の連携

5章 : 単純な業務ロジックを実装する
5.1 トランザクションスクリプト
トランザクションスクリプトは、順次処理していくスクリプト
名前にもあるように、トランザクション管理が目的なので、スクリプトの実行結果として、成功または失敗で終了する => 業務ロジックが置かれることがある
トランザクションスクリプトは、単純な問題領域の記述、つまり、補完的な業務領域に向いてる
業務ロジックが複雑になると、ロジックの重複、コード量の増加により、不具合の原因となりやすい
だから、中核の業務領域には使ってはならない

5.2 アクティブレコード
データベースのテーブルまたは1行を表すオブジェクト。データベース操作をカプセル化し、データに関連する業務ロジックを持つ
テーブル間のリレーションを扱うとなると、トランザクションスクリプトだと似たようなコードの繰り返しになる
アクティブレコードはレコードの読み取りや作成など、いわゆるCRUD操作のメソッドと入力値の検証のような比較的単純な業務ロジックに用途が限定される
なので、補完的な業務領域、一般的な業務領域用の外部サービスとの連携などに用途が限定される

アクティブレコードの典型的な使い方は、データ構造と振る舞い(業務ロジック)の分離
たいてい、アクティブレコードが getter/setter を公開し、外部の手続きがアクティブレコードのフィールドを変更する
だからこそ、貧血ドメインモデルと呼ばれるアンチパターンとして知られている
*Rails の ActiveRecord じゃなく、あくまでパターンの話

6章 複雑な業務ロジックに立ち向かう
業務ロジックの実装方法はドメインモデル
ドメインモデルとは
複雑な業務ロジックを扱うための設計手法で、ロジックとデータの両方を一体化させた、事業活動を表現するオブジェクトモデル
業務ロジックは本質的に複雑なので、ドメインモデルにそれ以上の複雑さを持ち込むべきではない。データベース通信や外部との通信など技術的な関心事からドメインモデルを完全に切り離す必要がある
そのために古くからある単純なオブジェクト(Plain Old Object)だけで組み立てる。実行基盤に関係するライブラリやFWに依存させてはいけない
技術的関心事から切り離すことで、区切られた文脈の同じ言葉の用語と対応しやすくなる

値オブジェクト
値オブジェクトを使うことでソースコードが同じ言葉を話す
なるほど
事業活動の観点からは、値オブジェクトは事業活動を表現する基本部品と考えると良い
通貨とか金銭的な価値の表現は値オブジェクトで表現する用途として重要
Int だけだと意図が表現できないし、丸めや端数処理も散らばる

エンティティ
id のようか識別子を持つオブジェクトで、イミュータブルな値をもとに識別する値オブジェクトとは対照的
値オブジェクトはエンティティの状態を表現する手段
エンティティはドメインモデルの基本となる部品であるが、エンティティは単独で実装することはなく、必ず集約の実装の一部

集約
集約はエンティティである
つまり、一意に識別できるフィールドが必要
集約のインスタンスはライフサイクルの途中で状態が変化する
集約の目標はデータの一貫性の保証
ただ、集約はエンティティなのでミュータブルであるから、データの一貫性を保証するためにはさまざまな課題ある

データの一貫性を矯正する
集約はミュータブルなので一貫性を保つためには、内部と外部の間に明確な境界を定義する。つまり、集約とはデータの一貫性を矯正する境界
集約に記述するロジックは状態を変更しようとする外部からのすべての操作の妥当性を検証し、業務ルールに違反した状態になることを防ぐ必要ある
実装的には、集約内部の業務ロジックだけが状態を変更できるようにすることでデータの一貫性を矯正
状態変更メソッドは実現方法二つある
1つは公開メソッドとしてその差の中で変更ロジックを定義
もう一つはコマンドの実行に必要な情報をカプセル化したオブジェクトであるパラメータオブジェクトを渡す方法 ref https://qiita.com/yoron0122/items/5f96535091d336e3fa09

集約に業務ロジックを集めると、集約を使う側のアプリケーション層は単純になる
- 最新の状態を反映した集約オブジェクト作る
- 生成した集約に対して必要な操作を実行
- 変更された状態を永続化
- 操作の結果を呼び出し元に返す
p102 アプリケーション層はトランザクションスクリプトの集まり => なるほど確かに。業務ロジックは記述されず、集約に対して適切な操作をし、成功失敗に関わらず結果を返す。そのエラーハンドリング含め。

集約はトランザクションの境界でもある
集約の状態を変更できるのは集約内部に記述した業務ロジックだけ => 集約はトランザクションの教会としても機能
集約の状態を変える処理は、一つのトランザクションとしてコミットする必要がある
=> なぜなら、集約の目標はデータの一貫性の保証だから
たとえば、注文という集約があったときに、注文ヘッダーと複数の注文明細が含まれる場合、注文明細を追加する処理は、注文ヘッダーの合計金額を更新する処理と同時に行わなければならない。これらが別々のトランザクションだと、注文したのに合計金額が更新されない状態が生じてしまい、データの一貫性が失われてしまう
複数の集約の変更を単一のトランザクションでコミットしようとしている場合、集約の境界が間違っている

集約は何も一つのエンティティだけでなく、複数のエンティティも扱う
ある業務ロジックが複数のエンティティを必要とするなら、それをまとめたものが集約となる
結果的に整合していれば良いエンティティや操作は別の集約として表せる

集約のルート
集約には複数のエンティティが含まれ得るが、外部に公開するインターフェース役のエンティティは一つにすべき
このインターフェース役のエンティティを集約のルートと呼ぶ
集約内部の他のエンティティには、集約のルートからしかアクセス、操作できないということ

業務イベント
業務イベントは事業活動の中で起きた重要な出来事を表現するメッセージ
例えば、チケットを割り当てたとかメッセージを受け取ったとか
実際に起きた出来事なので必ず過去形になる
業務イベントは集約の公開I/F
集約は業務イベントを発行する
そして他の外部サービスがそのイベントを購読して、業務イベントに応じた業務ロジックを実行

集約を実装するときは同じ言葉を厳密に反映せよ
集約の名前、フィールドに持つデータ名、メソッド名、発信する業務イベントの名前、そういう全ての名前を、区切られた文脈の同じ言葉によって命名
エヴァンスが言うように、ソースコードは、開発者同士の会話や業務エキスパートとの会話で使う、同じ言葉に基づいて書かなければならない

業務サービス
集約や値オブジェクトでは表現しにくい業務ロジックや、複数の集約にまたがる業務ロジックが見つかった場合、このような業務ロジックを記述する方法として業務サービスがある
業務サービスは業務ロジックだけを記述する自分自身の状態は持たないステートレスなオブジェクト。
ほとんどの場合、業務サービスは様々なコンポーネントの呼び出し統合して、何らかの計算や分析を行う
集約のインスタンスの変更は単一のデータベーストランザクションであることは引き続き必要。
業務サービスで複数の集約インスタンスを扱うからと言って、この原則は変わらない

複雑さの扱い方
エリヤフ・ゴールドラットは、書籍『ザ・チョイス:複雑さにまどわされるな』で、システムの複雑さを議論するときは、システムの振る舞いを制御し予測する難易度で判断するという。この難易度はシステムの自由度、要するに状態を表現するデータの個数

public class ClassA
{
public int A { get; set; }
public int B { get; set; }
public int C { get; set; }
public int D { get; set; }
public int E { get; set; }
}
public class ClassB
{
private int _a, _d;
public int A
{
get => _a;
set
{
_a = value;
B = value / 2;
C = value / 3;
}
}
public int B { get; private set; }
public int C { get; private set; }
public int D
{
get => _d;
set
{
_d = value;
E = value * 2;
}
}
public int E { get; private set; }
}
ClassA と ClassB では後者の方が複雑に見えるが、自由度で言うと後者の方が小さい
前者は int A から E の5つが状態を表現するデータの個数に対し、後者は A と D が決まれば他は自動的に決まる、つまり自由度は2
ClassB は不変条件を追加することで、複雑さが小さくなっている。不変条件をカプセル化することで複雑さを小さくしている。これは集約と値オブジェクトの背景にある考え
集約も値オブジェクトもその境界の内部に業務ロジックをカプセル化し、業務ルールを確実に適用することで複雑さを小さくする。集約の内部の状態を変えられるのは集約自身のメソッドだけ

業務ロジックの複雑さに立ち向かうために、その複雑さを値オブジェクトと集約の境界内部にカプセル化し、それらを部品としてドメインモデルを組み立てる。

7章 : 時間軸でモデルを作る
lead-id | last-name | first-name | status | phone-number | followup-on | created-on | updated-on |
---|---|---|---|---|---|---|---|
1 | 中島 | 瑞希 | 成約済 | 555-1246 | 2019-01-31T10:02:40.32Z | 2019-01-31T10:02:40.32Z | |
2 | 神田 | 謙二 | 終了 | 555-4395 | 2019-03-29T22:01:41.44Z | 2019-03-29T22:01:41.44Z | |
3 | 中村 | 喜彦 | 終了 | 555-1176 | 2019-04-15T23:08:45.59Z | 2019-04-15T23:08:45.59Z |
この販売管理システムの見込み客テーブルがあったとして、現在の状態は分かってもどのような経緯で現在の状態に至ったのか分からない。
この見込み客との商談は終わりにして、新たな見込み客との商談に取り組んだ方が良いのか。こういう問いに答えられない。
商談プロセスを最適化したいなどの事業活動の本質的な関心事は生まれうる。このような時間軸を取り入れたデータモデルをイベントソーシングという。
イベントソーシングでは、集約の現在の状態を記録する代わりに、集約のライフサイクルで発生する全ての変化を一連のイベントとして永続化する。

{
"lead-id": 12,
"event-id": 0,
"event-type": "新規登録",
"last-name": "小林",
"first-name": "浩美",
"phone-number": "555-2951",
"timestamp": "2020-05-20T09:52:55.952Z"
},
{
"lead-id": 12,
"event-id": 1,
"event-type": "架電",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 2,
"event-type": "商談予定設定",
"followup-on": "2020-05-27T12:00:00.00Z",
"timestamp": "2020-05-20T12:32:08.24Z"
}
のように一連のイベント記録を登録
最新状態を簡単に反映できるし、情報のトレース、分析も可能

イベントソーシングはオブジェクトのすべての状態変更をイベントとして表現し、永続化することが必要
永続化されたイベントが真実を語る情報源(source of truth) となる
つまり、event を sourcing すること

所感
イベント履歴式のドメインモデルは便利な反面、ある程度のコストを払う必要があるよなと
履歴を追いやすいし、分析にも使えるし
ただ、その性質が必要な業務領域なのかは見極める必要がある
出入金を扱う集約なら必要ではあるだろうけど、そのような性質を扱う集約なのか
ただ、本書でも書いてある通り、一つの集約で発生するイベントの数が100を超えてくることはほとんどないだろうというのはそうかもしれないなと思う
仮に、イベントの数が多すぎると別の集約に切り出せるサインなのだろうなと
イベント数が多すぎる場合、最新状態だけスナップショットのキャッシュで扱うのは一つの手段としてありそう。最新状態だけのイベントを格納したテーブルとか?
状態を記録するテーブルにトリガーを追加して、すべての項目のスナップショットを履歴テーブルに複製するやり方はどうか?という質問に対して、『履歴テーブルへの書き忘れは起きないが、項目の変化内容が記録されるだけで、業務の文脈、つまり項目が変更された意図が記録できない』というのはその通り過ぎるなと思った

8章 技術方式
レイヤードアーキテクチャは、技術的な関心事にもとづいて、ソースコードを分解します。この方式は、業務ロジックとデータアクセスの実装を結びつけるため、アクティブレコードパターンのシステムに適しています。
=> Rails はこれ
一方、ポートとアダプターパターンは依存関係を逆転させます。つまり、業務ロジックを中心に置き、業務ロジックをすべての基盤コンポーネントから独立させます。このやり方は、ドメインモデルで実装した業務ロジックに適しています。
=> ポートアンドアダプターはドメインモデルを中心に置きつつ、ドメインモデルと接続するアダプターが交換可能なファサードとして表現する
https://qiita.com/cocoa-maemae/items/b08c4cf95d47e314e2dc の記事わかりやすい
CQRSは、同じデータを複数のモデルで表現します。イベントソーシングにもとづくシステムでは必須です。それ以外にも、複数の永続化モデルを用いる必要があるシステムに利用できる技術方式です。
=> コマンドを実行するモデルと複数の読み取りモデル(RDBと検索用のElasticSearchなど)で便利。ただ、コマンド実行モデルで行われた変更を同期的に反映するか、非同期的に反映するかで難しさはある
区切られた文脈ごとに適用するアーキテクチャを検討した方が良い
=> とはいえコストはかかるし認知負荷も高いので現実解がどこにあるかは見極め大事

9章 通信
集約同士の連携
業務イベントの発行
ある集約でDBへのコミット後にイベントを発行するとする
仮にコミット後にサーバーが停止したとすれば、業務イベントを発行できない => 状態の不整合を引き起こす
送信箱のパターンを使おう
つまり、集約の状態変更と業務イベントの両方をRDBなどのデータストアに一つのトランザクションとしてコミット
メッセージ中継サービスがRDBを読み取って、送信していなければメッセージ通信基盤に送信するパターン
=> データは用意しておくから読み取ってよしなにやってくれみたいな。仮にメッセージ中継サービスが落ちていても、再起動時にRDB読み取りに行けば、メッセージは送れる
中継サービスがプル型としてRDB読み取るパターンと、プッシュ型として中継サービスを呼び出すパターンもある
送信箱パターンを使うと、信頼性高くメッセージを送信できる

サーガ
トランザクションの範囲を一つの集約に限定することは、集約の重要か設計原則
ただ、この原則を超えて複数の集約にまたがる業務プロセスを実装しないといけないケースもある
このケースではサーガとして実装できる
多数のトランザクションで構成される業務プロセスはサーガとして表現できる
関連するコンポーネントが発信する業務イベントを検知し、それに続くコマンドを別のコンポーネントに向けて発信する
一連の処理が失敗した場合、システムの一貫性を保つために、失敗に対応する補償アクションを実行
本書では、広告配信の例が示されてる
広告キャンペーンが開始
広告用の素材を広告事業者に送る必要ある
広告事業者が広告配信を開始したら配信開始の通知受け取る
通知受け取ったらキャンペーンの状態をPublishedに
広告事業者が広告の配信を拒否したら、キャンペーンの状態をRejectedに変更する
キャンペーンと広告事業者は別の文脈なので、一つの集約に同居させるのは違うが、相互に関連したアクションが必要 => サーガ

サーガを使った一貫性
サーガは複数のコンポーネントにまたがるトランザクションの実行を調整し、全てのコンポーネントの状態は結果的に整合する
サーガは最終的には全てのコマンドを実行するが、個々のトランザクションの実行は分離されている
これは集約のもう一つの設計原則に関係
データが強く整合していると想定できるのは、集約の教会の内側だけ
集約の境界の外側にあるデータは結果的に整合する
サーガを適切に使うにはこの原則を使おう
集約境界の設計が不適切なことを補うために、サーガを使ってはならない
=> ありそう

プロセスマネージャー
サーガは単純で直線的な業務フローを扱う
逆にプロセスマネージャーは業務ロジックが中心となる複雑な業務プロセスを実装する方法
分かりやすい経験則として、もしサーガの実装でif-else文を使った処理の分岐が必要なら、それはおそらくプロセスマネージャーとのこと
プロセスマネージャーは複数の処理ステップで構成される、開始から終了までが一つにつながった業務プロセス。明示的に起動する必要あり。
逆にサーガは特定イベントを検知した際に暗黙的に起動
本書の例では、広告キャンペーンイベントを検知するとサーガが起動して、広告の提出コマンドを実行するみたいな感じ
プロセスマネージャーは複数の処理を包含しているので、本書の例で言うと、出張のための航空券とホテルを手配する業務を考えたとき、割安なフライトを選定し、そのフライトを確認し、問題があれば別のフライトを選定。航空券手配できたら次はホテル、、と繰り返す。
この例では、特定の業務要素(社員、フライト、ホテルなど)が出張手配のプロセスを開始するわけではない。手配業務という処理全体をプロセスマネージャーとして実装することが必要

プロセスマネージャーは、多くの場合、複数の集約の組み合わせ

10章 設計の経験則
区切られた文脈の引き方
区切られた文脈の大きさを決めてからモデルを作ってはダメ。モデルを作ってから区切られた文脈の大きさを決定しよう
前提として区切られた文脈の変更はかなりコストのかかる作業。特に境界ごとにチームが分かれていたりすると、調整にかなりの時間を要する。
なので、きっちりと境界を分けるということはせず、割と広い範囲で文脈を区切る、あるいは複数の文脈を取り込んだ境界で区切る。
こうすれば、仮に境界が間違っていたとしても比較的安全に変更できる
なぜなら、物理的に切り離してしまった境界を後から変更し直すより、論理的な境界を引き直す方がはるかに簡単

実装方法とテスト方針の判定方法 p196

11章 設計を進化させる
競争上の優位を維持するためには、事業活動を常に進化させていく必要がある。その過程でソフトウェアも変化していくので、区切られた文脈がぼやけてきたり、再度考え直さないといけない。それは成長している証
- 機能拡張が必要になったら、適切な設計判断をするために、業務領域を小さい単位で識別することを検討
- 何にでも手を出してどれもちゃんとできてない区切られた文脈を作らない。区切られた文脈で表現するモデルは、特定の課題を解決することだけに集中して
- 集約はできるだけ小さく設計する。強い一貫性が必要なデータの範囲を特定するという経験則を使って、業務ロジックを新しい集約に抽出する可能性を探って
成長に起因する複雑さの増大を検知するために、さまざまな境界を常に監視しよう
区切られた文脈の境界の変化を嗅ぎ取るために、継続的な業務知識の習得は不可欠
組織体制の変化も境界を見直すタイミングだったりする

トランザクションスクリプトからアクティブレコードへの変換
どちらも業務ロジックを手続的に扱う点では同じ
アクティブレコードの方がデータ構造を永続化するための複雑な仕組みをカプセル化している
変換するタイミングとしては、トランザクションスクリプトで扱うデータが複雑になってきた場合、アクティブレコードに変換
=> 逆にいうと、集約が切り出されてアクティブレコードで扱うデータが簡素になってきたらトランザクションスクリプトに戻すというのはありそうだが、あまりそういう力学は働かなそうな感覚もある

アクティブレコードからドメインモデルへの変換
アクティブレコードで扱う業務ロジックが複雑になり、データの不整合やロジックの重複が増えていると気づいたら、業務ロジックの実装をドメインモデルにリファクタリングする機会
まず値オブジェクトを見つけることから始める
- 不変なオブジェクトとして表現できそうなデータ構造を特定
- そのデータ構造に関連した業務ロジックを見つけて値オブジェクトのメソッドとして記述
- データ構造を分析し、トランザクションの境界を探す
- 状態を変更する業務ロジックを明確にするために、アクティブレコードの全てのsetterメソッドをプライベートに変更し、アクティブレコードの外部から状態を変更できなくなる
- これによってコンパイルエラーになるので、エラーメッセージから状態変更の箇所を特定して、その操作をアクティブレコードの内側に移動するリファクタリングをする
- 業務ルールと不変条件の一貫性を保証するために必要な階層構造を見出す。それが集約の候補。一貫性を保証するための必要なデータの最小限の集まりを特定
- 集約ごとにルートとなるオブジェクト、いわゆるエントリポイントを特定し、公開I/Fとする

ドメインモデルからイベント履歴式ドメインモデルへ変換
集約のデータを直接変更する代わりに、集約のライフサイクルを表現する、一連の業務イベントを特定します
現在の集約の履歴はトレースできないからどうする?
=> スナップショットのログ取っていたらトレースはできそう
移行イベントとして表現して記録する方法もある
過去の記録が失われたことを明示する

12章 イベントストーミング
同じ言葉を構築しモデリングする手法
=> ちゃんとやったことないのでやりたいな
まずイベントを洗い出して、時系列で整理して、イベントの転換点を見つけ、コマンドを見つけ、アクターや外部システム、ポリシーを整理し、集約を見つける