kanachi-blog

notionでの公開記事をastro-notion-blogを使って公開するよ

知識を表現するより発展的なパターン『ドメイン駆動設計入門 』part3

データの整合性を保つ

データの整合性を保つために、以下の画像に示すような致命的な不具合が起こらないように設計をしなければならない

ユニークキー制約による防衛

  • 重複して欲しくないものにDBのユニークキー制約をつけることは有効
  • ただし、重複チェックの主体をDBのユニークキー制約に任せることは、ドメインロジックを特定技術分野への依存させてしまうためNG
    • あくまで主体はドメインオブジェクトなどのコード上でのチェック
    • ユニークキー制約はセーフティネットとして使う

トランザクションによる防衛

  • 整合性を保っていてほしい一連の状態操作はコミットするまで反映されない
    • コミットが成功すれば一連の処理が全て行われる、失敗すれば全て行われない

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

アプリケーションを組み立てるフロー

  1. どういった機能が求められているか
  2. 機能をなり立たせるために必要なユースケースを洗い出す
  3. ルールと制約を選び出し、ドメインオブジェクトを準備する
  4. ドメインオブジェクトを用いてユースケースを実現するアプリケーションサービスを実装する

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

集約とは

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

集約にはルートとなるオブジェクトが存在して、すべての操作はルート越しに行われる。

そのようにして集約内部の操作に制限がかけられ、集約ないの普遍条件は維持される。

デメテルの法則

オブジェクト指向プログラミングではこのように、外部から内部のオブジェクトに対して直接操作するのではなく、それを保持するオブジェクトに依頼する形を取ります。そうすることで直感的に、かつ不変条件を維持することができるのです。

オブジェクトO上のメソッドMが呼び出してもよいメソッドは以下のオブジェクトに属するメソッドのみに限定される。

  1. Oそれ自身
  2. Mの引数に渡されたオブジェクト
  3. Mの内部でインスタンス化されたオブジェクト
  4. Oを直接的に構成するオブジェクト(Oのインスタンス変数)
すべての操作をAggregate Rootに依頼する形をとる

Goでの実装コード

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

仕様はオブジェクトの評価を行うオブジェクトです。

オブジェクトの評価は時に複雑な手順が必要となります。こうした評価処理をオブジェクトのメソッドとして定義すると、オブジェクト本来の趣旨を見えづらくしてしまうことがある。

評価処理はオブジェクトの定義する以外に、評価自体をオブジェクトとして切り出すことが可能。そうして切り出された条件に合致しているかどうかを見極めるオブジェクトが「仕様」である。

評価の例

  • ユーザにはプレミアムユーザというタイプが存在する
  • サークルに所属するユーザの最大数はオーナー含めて30名まで
  • プレミアムユーザが10名以上所属しているサークルは最大数が50名まで引き上げられる

chapter6で述べたようにドメインのルールはサービスに記述してはいけない。

とは言いつつも既存のドメインオブジェクトに定義しようとすると、ドメインオブジェクト内でリポジトリを受け取る必要がある。だが、エンティティや値オブジェクト内でリポジトリを操作するのは可能な限り避けたい。

このような場合に仕様を利用する。

type CircleRecommendSpecification struct {
    ExecuteDateTime time.Time
}

// NewCircleRecommendSpecification は新しいCircleRecommendSpecificationインスタンスを生成
func NewCircleRecommendSpecification(executeDateTime time.Time) *CircleRecommendSpecification {
    return &CircleRecommendSpecification{
        ExecuteDateTime: executeDateTime,
    }
}

// IsSatisfiedBy は指定されたサークルが仕様を満たしているかどうかを判断
func (crs *CircleRecommendSpecification) IsSatisfiedBy(circle *Circle) bool {
    if circle.CountMembers() < 10 {
        return false
    }
    return circle.Created.After(crs.ExecuteDateTime.AddDate(0, 1, 0)) // AddMonths(1)の代わりにAddDateを使用
}

CQRSについて

  • DDDではデータの永続化は集約単位ごとに定義されたリポジトリを通して行う
  • しかし、画面から要求されるデータは必ずしも集約単位ではない
  • 集約単位で取り出し、画面側の要求する形に作り直すのは非効率
  • この問題を解決するのが、クエリ(読み込み)とコマンド(書き込み)のモデルを分けて考えるCQRS(Command Query Responsibility Segregation: コマンドクエリ責務分離)
  • コマンドは集約単位で行い、クエリは画面側の要求に都合がいい単位で行うイメージ

アーキテクチャ

ドメイン駆動設計で最も大事なことはドメインロジックを1つのレイヤーにまとめ、隔離することである。アーキテクチャはそれを実現するため具体的な方針であり、必ずしもアーキテクチャに従う必要はない

  • アンチパターン:利口なUI(ドメインロジックがインターフェースに染み出している状態)

DDDと同時に語られることが多いアーキテクチャ

  • レイヤードアーキテクチャ
    • プレゼンテーション層
    • アプリケーション層
    • ドメイン層
    • インフラストラクチャ層
  • ヘキサゴナルアーキテクチャ
    • コンセプトはアプリケーションとそれ以外のインターフェースや保存媒体は付け外しできるようにするということ
  • クリーンアーキテクチャ

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

ドメイン駆動設計はモデリング手法とそのモデルをコードへ落とし込むための実践的なパターン

軽量DDDとは

実践的なパターンだけを取り入れたもの。ドメイン駆動設計では、パターンを踏襲するだけでなくドメインの本質に向き合うことが重要。

ドメインエキスパートと協調する

ドメインと向き合うことよりも技術的なアプローチに傾倒するのは間違った選択である。

ドメインを知り、ドメインと向きあう姿勢が大切であり、そのためにドメインエキスパートと真に解決すべき問題を発掘する。

ユビキタス言語

認識齟齬や翻訳にコストをかけないために、共通言語を作る。

境界付けられたコンテキスト

下記のサイトを参照

コンテキストマップ

コンテキスト同士の関係を定義し、ドメイン全体を俯瞰できるようなものとしてコンテキストマップを作る必要があります。