Open43

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

dak2dak2

1章 事業活動を分析する

業務領域は以下の3つに分類される

  1. 中核の業務領域 : 競争優位になる。いわゆるコアコンピタンス
  2. 一般的な業務領域 : 他社も同じく有する類の業務で競争優位とはならない
  3. 補完的な業務領域 : 競争優位とはならない。あくまで中核の業務領域のサポート。

中間の業務領域は他者との差別化要素は大きいが、業務ロジックが複雑
ロジックも頻繁に変わりやすい
競争優位を保つ以上、持続的に発展させる必要があるからです

一般や補完の領域においては他社の外部サービスを利用することもある

業務領域とは関連するユースケースの集まり

dak2dak2

2章 業務知識を発見する

同じ言葉で意図を適切に表し伝達する
同じ言葉をモデリングやプログラム上でも使っていく、認識を強固なものにしていく。曖昧さを減らす。

モデルとは、現実のものや出来事を簡略化した表現。特定の側面を意図的に強調し、一方で、それ以外の側面を意図的に除外します。モデルは用途を限定した抽象化です。

地図がわかりやすい。
世界地図に地下鉄の駅は不要。地図によって目的が異なり、目的に沿わない情報は捨象されてモデリングされている。

dak2dak2

3章 : 事業活動の複雑さに立ち向かう

モデルで重要なのは解決しようとしている課題が何か。モデルには必ず境界がある。モデルの境界を広げすぎるとそのモデルは役に立たなくなる。
したがって、モデルを作るためには適切に区切られた境界が必要。

区切られた文脈によって同じ言葉の定義が完成する。同じ言葉は区切られた文脈の内側に限定される。

この章の例で挙げた、販売促進部門と営業部門で見込み客の対象の捉え方が異なる話があるが、適切に区切られた境界では異なる意味で使い分けられる

dak2dak2

3.3 区切られた文脈と業務領域の関係

業務領域は相互に関連したユースケースの集まり、つまり『発見』であり、区切られた文脈はソフトウェア技術者の手によって分割される『設計』である

一つの業務領域に一つのモデルが対応するということではない。業務領域ごとに課題が複数あって、それぞれ区切られた文脈として分割できるケースもある。

業務の捉え方が異なる不整合に直面した場合、同じ言葉を複数の限られた文脈に分解してみる。

所感

事実と解釈に近いか

dak2dak2

ルース・マランは、システム設計の本質は境界の決定と説く。

アーキテクチャの選択はシステムの設計です。システムの設計は文脈の設計です。文脈の本質は境界です。境界の内側に何があり、境界の外側に何があるか。境界を越えて、どうつながり、何が移動するか。何を選択し、何をあきらめるか。境界は、何が外部であり何が内部であるかを明らかにします

区切られた文脈がそのままモデルの境界を定義する

dak2dak2

4章: 区切られた文脈どうしの連携

モデルの構築には、目的の特定、つまり境界が必要。境界は同じ言葉が通用する範囲を分割します。

区切られた文脈がモデルの境界を分ける。とはいえ、モデル自体が完全に独立するわけではなく、システムとして機能するには、コンポーネント同士の連携が必要

結果として、区切られた文脈の間には、必ず接合部分が存在しており、この接合部分を区切られた文脈間の契約と呼ぶ

契約が必要な理由は、異なる区切られた文脈同士では、モデルと言葉が異なるから。契約とは、利害が異なる複数の当事者間の取り決め。取り決めを明文化し、お互いの利益を調整することが必要。

dak2dak2

利用する側が自分のモデルに合うように変換することをモデル変換装置

提供する側が利用側のニーズに最適化した公開された言葉を実装するのが共用サービス

言葉の定義としてはこのように分けられるけど、関心の対象が違うということだけ抑えておけば良さそう => 区切られた文脈同士の連携

dak2dak2

5章 : 単純な業務ロジックを実装する

5.1 トランザクションスクリプト

トランザクションスクリプトは、順次処理していくスクリプト
名前にもあるように、トランザクション管理が目的なので、スクリプトの実行結果として、成功または失敗で終了する => 業務ロジックが置かれることがある

トランザクションスクリプトは、単純な問題領域の記述、つまり、補完的な業務領域に向いてる

業務ロジックが複雑になると、ロジックの重複、コード量の増加により、不具合の原因となりやすい

だから、中核の業務領域には使ってはならない

dak2dak2

5.2 アクティブレコード

データベースのテーブルまたは1行を表すオブジェクト。データベース操作をカプセル化し、データに関連する業務ロジックを持つ

テーブル間のリレーションを扱うとなると、トランザクションスクリプトだと似たようなコードの繰り返しになる

アクティブレコードはレコードの読み取りや作成など、いわゆるCRUD操作のメソッドと入力値の検証のような比較的単純な業務ロジックに用途が限定される

なので、補完的な業務領域、一般的な業務領域用の外部サービスとの連携などに用途が限定される

dak2dak2

アクティブレコードの典型的な使い方は、データ構造と振る舞い(業務ロジック)の分離
たいてい、アクティブレコードが getter/setter を公開し、外部の手続きがアクティブレコードのフィールドを変更する

だからこそ、貧血ドメインモデルと呼ばれるアンチパターンとして知られている

*Rails の ActiveRecord じゃなく、あくまでパターンの話

dak2dak2

6章 複雑な業務ロジックに立ち向かう

業務ロジックの実装方法はドメインモデル

ドメインモデルとは

複雑な業務ロジックを扱うための設計手法で、ロジックとデータの両方を一体化させた、事業活動を表現するオブジェクトモデル

業務ロジックは本質的に複雑なので、ドメインモデルにそれ以上の複雑さを持ち込むべきではない。データベース通信や外部との通信など技術的な関心事からドメインモデルを完全に切り離す必要がある

そのために古くからある単純なオブジェクト(Plain Old Object)だけで組み立てる。実行基盤に関係するライブラリやFWに依存させてはいけない

技術的関心事から切り離すことで、区切られた文脈の同じ言葉の用語と対応しやすくなる

dak2dak2

値オブジェクト

値オブジェクトを使うことでソースコードが同じ言葉を話す
なるほど

事業活動の観点からは、値オブジェクトは事業活動を表現する基本部品と考えると良い

通貨とか金銭的な価値の表現は値オブジェクトで表現する用途として重要
Int だけだと意図が表現できないし、丸めや端数処理も散らばる

dak2dak2

エンティティ

id のようか識別子を持つオブジェクトで、イミュータブルな値をもとに識別する値オブジェクトとは対照的

値オブジェクトはエンティティの状態を表現する手段

エンティティはドメインモデルの基本となる部品であるが、エンティティは単独で実装することはなく、必ず集約の実装の一部

dak2dak2

集約

集約はエンティティである
つまり、一意に識別できるフィールドが必要
集約のインスタンスはライフサイクルの途中で状態が変化する

集約の目標はデータの一貫性の保証

ただ、集約はエンティティなのでミュータブルであるから、データの一貫性を保証するためにはさまざまな課題ある

dak2dak2

データの一貫性を矯正する

集約はミュータブルなので一貫性を保つためには、内部と外部の間に明確な境界を定義する。つまり、集約とはデータの一貫性を矯正する境界

集約に記述するロジックは状態を変更しようとする外部からのすべての操作の妥当性を検証し、業務ルールに違反した状態になることを防ぐ必要ある

実装的には、集約内部の業務ロジックだけが状態を変更できるようにすることでデータの一貫性を矯正

状態変更メソッドは実現方法二つある

1つは公開メソッドとしてその差の中で変更ロジックを定義
もう一つはコマンドの実行に必要な情報をカプセル化したオブジェクトであるパラメータオブジェクトを渡す方法 ref https://qiita.com/yoron0122/items/5f96535091d336e3fa09

dak2dak2

集約に業務ロジックを集めると、集約を使う側のアプリケーション層は単純になる

  1. 最新の状態を反映した集約オブジェクト作る
  2. 生成した集約に対して必要な操作を実行
  3. 変更された状態を永続化
  4. 操作の結果を呼び出し元に返す

p102 アプリケーション層はトランザクションスクリプトの集まり => なるほど確かに。業務ロジックは記述されず、集約に対して適切な操作をし、成功失敗に関わらず結果を返す。そのエラーハンドリング含め。

dak2dak2

集約はトランザクションの境界でもある

集約の状態を変更できるのは集約内部に記述した業務ロジックだけ => 集約はトランザクションの教会としても機能

集約の状態を変える処理は、一つのトランザクションとしてコミットする必要がある

=> なぜなら、集約の目標はデータの一貫性の保証だから
たとえば、注文という集約があったときに、注文ヘッダーと複数の注文明細が含まれる場合、注文明細を追加する処理は、注文ヘッダーの合計金額を更新する処理と同時に行わなければならない。これらが別々のトランザクションだと、注文したのに合計金額が更新されない状態が生じてしまい、データの一貫性が失われてしまう

複数の集約の変更を単一のトランザクションでコミットしようとしている場合、集約の境界が間違っている

dak2dak2

集約は何も一つのエンティティだけでなく、複数のエンティティも扱う
ある業務ロジックが複数のエンティティを必要とするなら、それをまとめたものが集約となる

結果的に整合していれば良いエンティティや操作は別の集約として表せる

dak2dak2

集約のルート

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

dak2dak2

業務イベント

業務イベントは事業活動の中で起きた重要な出来事を表現するメッセージ
例えば、チケットを割り当てたとかメッセージを受け取ったとか
実際に起きた出来事なので必ず過去形になる

業務イベントは集約の公開I/F
集約は業務イベントを発行する
そして他の外部サービスがそのイベントを購読して、業務イベントに応じた業務ロジックを実行

dak2dak2

集約を実装するときは同じ言葉を厳密に反映せよ

集約の名前、フィールドに持つデータ名、メソッド名、発信する業務イベントの名前、そういう全ての名前を、区切られた文脈の同じ言葉によって命名

エヴァンスが言うように、ソースコードは、開発者同士の会話や業務エキスパートとの会話で使う、同じ言葉に基づいて書かなければならない

dak2dak2

業務サービス

集約や値オブジェクトでは表現しにくい業務ロジックや、複数の集約にまたがる業務ロジックが見つかった場合、このような業務ロジックを記述する方法として業務サービスがある

業務サービスは業務ロジックだけを記述する自分自身の状態は持たないステートレスなオブジェクト。
ほとんどの場合、業務サービスは様々なコンポーネントの呼び出し統合して、何らかの計算や分析を行う

集約のインスタンスの変更は単一のデータベーストランザクションであることは引き続き必要。
業務サービスで複数の集約インスタンスを扱うからと言って、この原則は変わらない

dak2dak2

複雑さの扱い方

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

dak2dak2
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 は不変条件を追加することで、複雑さが小さくなっている。不変条件をカプセル化することで複雑さを小さくしている。これは集約と値オブジェクトの背景にある考え

集約も値オブジェクトもその境界の内部に業務ロジックをカプセル化し、業務ルールを確実に適用することで複雑さを小さくする。集約の内部の状態を変えられるのは集約自身のメソッドだけ

dak2dak2

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

dak2dak2

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

この販売管理システムの見込み客テーブルがあったとして、現在の状態は分かってもどのような経緯で現在の状態に至ったのか分からない。

この見込み客との商談は終わりにして、新たな見込み客との商談に取り組んだ方が良いのか。こういう問いに答えられない。

商談プロセスを最適化したいなどの事業活動の本質的な関心事は生まれうる。このような時間軸を取り入れたデータモデルをイベントソーシングという。

イベントソーシングでは、集約の現在の状態を記録する代わりに、集約のライフサイクルで発生する全ての変化を一連のイベントとして永続化する。

dak2dak2
{
  "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"
}

のように一連のイベント記録を登録
最新状態を簡単に反映できるし、情報のトレース、分析も可能

dak2dak2

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

dak2dak2

所感

イベント履歴式のドメインモデルは便利な反面、ある程度のコストを払う必要があるよなと

履歴を追いやすいし、分析にも使えるし
ただ、その性質が必要な業務領域なのかは見極める必要がある
出入金を扱う集約なら必要ではあるだろうけど、そのような性質を扱う集約なのか

ただ、本書でも書いてある通り、一つの集約で発生するイベントの数が100を超えてくることはほとんどないだろうというのはそうかもしれないなと思う
仮に、イベントの数が多すぎると別の集約に切り出せるサインなのだろうなと

イベント数が多すぎる場合、最新状態だけスナップショットのキャッシュで扱うのは一つの手段としてありそう。最新状態だけのイベントを格納したテーブルとか?

状態を記録するテーブルにトリガーを追加して、すべての項目のスナップショットを履歴テーブルに複製するやり方はどうか?という質問に対して、『履歴テーブルへの書き忘れは起きないが、項目の変化内容が記録されるだけで、業務の文脈、つまり項目が変更された意図が記録できない』というのはその通り過ぎるなと思った

dak2dak2

8章 技術方式

レイヤードアーキテクチャは、技術的な関心事にもとづいて、ソースコードを分解します。この方式は、業務ロジックとデータアクセスの実装を結びつけるため、アクティブレコードパターンのシステムに適しています。
=> Rails はこれ

一方、ポートとアダプターパターンは依存関係を逆転させます。つまり、業務ロジックを中心に置き、業務ロジックをすべての基盤コンポーネントから独立させます。このやり方は、ドメインモデルで実装した業務ロジックに適しています。
=> ポートアンドアダプターはドメインモデルを中心に置きつつ、ドメインモデルと接続するアダプターが交換可能なファサードとして表現する
https://qiita.com/cocoa-maemae/items/b08c4cf95d47e314e2dc の記事わかりやすい

CQRSは、同じデータを複数のモデルで表現します。イベントソーシングにもとづくシステムでは必須です。それ以外にも、複数の永続化モデルを用いる必要があるシステムに利用できる技術方式です。
=> コマンドを実行するモデルと複数の読み取りモデル(RDBと検索用のElasticSearchなど)で便利。ただ、コマンド実行モデルで行われた変更を同期的に反映するか、非同期的に反映するかで難しさはある

区切られた文脈ごとに適用するアーキテクチャを検討した方が良い
=> とはいえコストはかかるし認知負荷も高いので現実解がどこにあるかは見極め大事

dak2dak2

9章 通信

集約同士の連携

業務イベントの発行
ある集約でDBへのコミット後にイベントを発行するとする
仮にコミット後にサーバーが停止したとすれば、業務イベントを発行できない => 状態の不整合を引き起こす

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

送信箱パターンを使うと、信頼性高くメッセージを送信できる

dak2dak2

サーガ

トランザクションの範囲を一つの集約に限定することは、集約の重要か設計原則
ただ、この原則を超えて複数の集約にまたがる業務プロセスを実装しないといけないケースもある
このケースではサーガとして実装できる
多数のトランザクションで構成される業務プロセスはサーガとして表現できる
関連するコンポーネントが発信する業務イベントを検知し、それに続くコマンドを別のコンポーネントに向けて発信する
一連の処理が失敗した場合、システムの一貫性を保つために、失敗に対応する補償アクションを実行

本書では、広告配信の例が示されてる
広告キャンペーンが開始
広告用の素材を広告事業者に送る必要ある
広告事業者が広告配信を開始したら配信開始の通知受け取る
通知受け取ったらキャンペーンの状態をPublishedに
広告事業者が広告の配信を拒否したら、キャンペーンの状態をRejectedに変更する

キャンペーンと広告事業者は別の文脈なので、一つの集約に同居させるのは違うが、相互に関連したアクションが必要 => サーガ

dak2dak2

サーガを使った一貫性

サーガは複数のコンポーネントにまたがるトランザクションの実行を調整し、全てのコンポーネントの状態は結果的に整合する

サーガは最終的には全てのコマンドを実行するが、個々のトランザクションの実行は分離されている

これは集約のもう一つの設計原則に関係

データが強く整合していると想定できるのは、集約の教会の内側だけ
集約の境界の外側にあるデータは結果的に整合する

サーガを適切に使うにはこの原則を使おう

集約境界の設計が不適切なことを補うために、サーガを使ってはならない
=> ありそう

dak2dak2

プロセスマネージャー

サーガは単純で直線的な業務フローを扱う
逆にプロセスマネージャーは業務ロジックが中心となる複雑な業務プロセスを実装する方法

分かりやすい経験則として、もしサーガの実装でif-else文を使った処理の分岐が必要なら、それはおそらくプロセスマネージャーとのこと

プロセスマネージャーは複数の処理ステップで構成される、開始から終了までが一つにつながった業務プロセス。明示的に起動する必要あり。
逆にサーガは特定イベントを検知した際に暗黙的に起動
本書の例では、広告キャンペーンイベントを検知するとサーガが起動して、広告の提出コマンドを実行するみたいな感じ

プロセスマネージャーは複数の処理を包含しているので、本書の例で言うと、出張のための航空券とホテルを手配する業務を考えたとき、割安なフライトを選定し、そのフライトを確認し、問題があれば別のフライトを選定。航空券手配できたら次はホテル、、と繰り返す。
この例では、特定の業務要素(社員、フライト、ホテルなど)が出張手配のプロセスを開始するわけではない。手配業務という処理全体をプロセスマネージャーとして実装することが必要

dak2dak2

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

dak2dak2

10章 設計の経験則

区切られた文脈の引き方

区切られた文脈の大きさを決めてからモデルを作ってはダメ。モデルを作ってから区切られた文脈の大きさを決定しよう

前提として区切られた文脈の変更はかなりコストのかかる作業。特に境界ごとにチームが分かれていたりすると、調整にかなりの時間を要する。

なので、きっちりと境界を分けるということはせず、割と広い範囲で文脈を区切る、あるいは複数の文脈を取り込んだ境界で区切る。
こうすれば、仮に境界が間違っていたとしても比較的安全に変更できる
なぜなら、物理的に切り離してしまった境界を後から変更し直すより、論理的な境界を引き直す方がはるかに簡単

dak2dak2

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

dak2dak2

11章 設計を進化させる

競争上の優位を維持するためには、事業活動を常に進化させていく必要がある。その過程でソフトウェアも変化していくので、区切られた文脈がぼやけてきたり、再度考え直さないといけない。それは成長している証

  • 機能拡張が必要になったら、適切な設計判断をするために、業務領域を小さい単位で識別することを検討
  • 何にでも手を出してどれもちゃんとできてない区切られた文脈を作らない。区切られた文脈で表現するモデルは、特定の課題を解決することだけに集中して
  • 集約はできるだけ小さく設計する。強い一貫性が必要なデータの範囲を特定するという経験則を使って、業務ロジックを新しい集約に抽出する可能性を探って

成長に起因する複雑さの増大を検知するために、さまざまな境界を常に監視しよう

区切られた文脈の境界の変化を嗅ぎ取るために、継続的な業務知識の習得は不可欠
組織体制の変化も境界を見直すタイミングだったりする

dak2dak2

トランザクションスクリプトからアクティブレコードへの変換

どちらも業務ロジックを手続的に扱う点では同じ
アクティブレコードの方がデータ構造を永続化するための複雑な仕組みをカプセル化している

変換するタイミングとしては、トランザクションスクリプトで扱うデータが複雑になってきた場合、アクティブレコードに変換

=> 逆にいうと、集約が切り出されてアクティブレコードで扱うデータが簡素になってきたらトランザクションスクリプトに戻すというのはありそうだが、あまりそういう力学は働かなそうな感覚もある

dak2dak2

アクティブレコードからドメインモデルへの変換

アクティブレコードで扱う業務ロジックが複雑になり、データの不整合やロジックの重複が増えていると気づいたら、業務ロジックの実装をドメインモデルにリファクタリングする機会

まず値オブジェクトを見つけることから始める

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

ドメインモデルからイベント履歴式ドメインモデルへ変換

集約のデータを直接変更する代わりに、集約のライフサイクルを表現する、一連の業務イベントを特定します

現在の集約の履歴はトレースできないからどうする?
=> スナップショットのログ取っていたらトレースはできそう

移行イベントとして表現して記録する方法もある
過去の記録が失われたことを明示する

dak2dak2

12章 イベントストーミング

同じ言葉を構築しモデリングする手法

=> ちゃんとやったことないのでやりたいな

まずイベントを洗い出して、時系列で整理して、イベントの転換点を見つけ、コマンドを見つけ、アクターや外部システム、ポリシーを整理し、集約を見つける