Open12

読書メモ『ドメイン駆動設計 モデリング/実装ガイド』

ose20ose20

概要

以下の本の読書メモ
https://little-hands.booth.pm/items/1835632

読んでる人の事前知識とか

  • DDDについては、『関数型ドメインモデリング』を読んだ程度
  • ソフトウェアエンジニアとしての経験は2~3年
  • 好きな言語: Rust, Haskell, OCaml
  • モチベーション
    • ソフトウェアを書くときの指針となるアーキテクチャを学びたい
    • 開発だけでなく、チームとしての保守も楽になる書き方が知りたい
    • 可能な限り不変条件をにエンコードしたいので、代数的な型とその上の操作をプリミティブとして採用している言語を使ってDDDを実践したい
ose20ose20

1. DDD概要

1.1. 言葉の定義

  • Eric Evansも名前を連ねているDomain-Language.comのDDD Referenceに従うことにする

1.2. DDDとは

  • 前提として、ソフトウェアは、ある領域の特定の問題を解決するためにつくられる
    • その対象領域をドメインと呼ぶ
  • そしてDDDとは、このドメインのモデリングによってソフトウェアの価値を高めることに注力した開発手法

1.3. モデルとは

1.3.1. 定義

  • 問題解決のために、特定の側面を抽象化したもの

  • モデルの種類

    • ドメインモデル
      • ドメインの問題を解決するためのモデル
    • データモデル
      • データの永続化方法を決める(それを効率化する)ためのモデル
  • エイリアス

    • モデル = ドメインモデル

1.3.2. モデルの例

  • 履歴書のモデルを作りたいとする
    • 問題解決のための抽象
      • 必要なものだけ定義する
        • いるものの例
          • 名前
          • 経歴
          • 志望理由
        • 多分いらないものの例
          • 筆圧、筆跡
          • 履歴書のメーカー
        • 実際の履歴書から問題解決に必要なものだけ抽出する

1.4. 良いモデル、よくないモデル

1.4.1. 良いモデルとは

  • モデルの定義を見ると導かれる
    • 問題を解決できるモデルがいいモデル

1.4.2. 良くないモデルの例

  • 定義より明らかで、問題を解決できないモデル
    • 間違ったモデルでどれだけ優れたアプリを作っても意味がない

1.5. 良いモデルを作るには

  • 以下の2つのサブセクションが答え

1.5.1. ドメインエキスパートと会話する

  • そのドメインに詳しい人に話を聞く
    • 開発する前にモデリングとヒアリングをちゃんとやれ

1.5.2. 運用して得られた発見をモデルに還元する

  • モデルは徐々に改善していくものという前提がDDDで重要
    • これ工数とれるかなぁ...
    • 企業の評価システムの中でやるインセンティブがあるかなぁ...
    • 情熱のある人がチームに複数人いないと多分無理
      • 少なくともできるだけ定時であがるぞという働き方だと余裕がない
        • 自分は興味があるからやれるけど、構造として解決するのが難しそう

1.6. DDDの問題解決のアプローチ

  • ドメインについて理解を深めて、継続的に改善する
  • モデルを継続的にソフトウェアに反映する

1.6.1. モデルの継続的な改善

ユビキタス言語

  • 発見したモデルの言葉をあらゆる場所で使う
    • 会話でも、ドキュメントでも、コードでも
    • これを定義するWikiがあってもいい

1.6.2. モデルからソフトウェアへの継続的な反映

  • モデルを直接表現するコード

    • モデルとコードに乖離がない方がいい
    • これをオブジェクト指向の手法で実装する
  • モデル表現を隔離するアーキテクチャ

    • モデルとそれ以外を分離して保守しやすくする
      • レイヤード、オニオン、クリーン...
  • 戦術的設計パターン

    • 軽量DDDという概念もある
    • ただモデルが良くないとダメなのでその先を目指したい
    • モデリングと直行しているので並列に着手できる
      • 両方少しずつ実践

1.7. 取り組む上で重要な考え方

  • 著者の私見

1.7.1. 課題ドリブン

  • 解決したい課題を明確にする
  • ルールだからやる、はやめよう

1.7.2. 小さく初めて、小さく失敗する

  • 試行錯誤が大事なので、失敗しても損失が小さいように小さくやる

1.8. Q&A

1.8.1. DDDの向き・不向き

  • 向いている
    • 解決したいドメインが複雑な場合
  • 向いていない
    • 非常にシンプルな場合
    • コアコンピタンスが技術的な関心ごと(超高速処理が大事)である場合
ose20ose20

2. モデリングから実装まで

2.1. ドメインモデリング

  • 迷いどころ
    • モデリングの方法やアウトプットの形式
    • エヴァンズ本や実践DDDでは具体的な記載がない
  • 比較的シンプルで効果を出しやすい2つを取り上げる
    • ユースケース図
    • ドメインモデル図

2.1.1. ユースケース図

  • UMLで定義される「ユーザーの要求に対するシステムの振る舞いを定義する図」

  • 記載されるもの

    • アクター(ユーザーの種類を定義)
    • ユースケース
      • ooをxxするという形式で、丸い吹き出しで記載する
  • とっかかりとして便利

    • 何に対処すべきなのかがわかりやすい
  • その上で、今回モデル化するスコープを定める

2.1.2. ドメインモデル図

  • 簡易化したクラス図のようなもの
  • 以下のようなものが記載される
    • オブジェクトの代表的な属性
    • ルール/制約(ドメイン知識)を吹き出しに
    • オブジェクト同士の関連
    • 多重度
    • 集約の範囲
    • 具体例
  • この例を見ると、関数型ドメインモデルとの違いが見える
    • 未完了/完了の2種類があるというのをエンコードできていない
      • 別々の型で表していない
        • もしくはここからわけるのかな?
    • 他もそう、とにかく不変条件が型にエンコードされていないので保守がむずそう

ルール/制約の記述方法

  • 他にいい書き方があれば使っていい
    • 状態遷移図とか

集約の表現方法

  • オブジェクト同士の関連は集約の内外を区別した記法を用いる

2.2. ドメインモデルの実装

  • 改善点のあるコードからだんだん改善していく
    • ドメインモデル貧血症
      • ドメインモデルを実装するためのオブジェクトでありながら、ドメイン知識をほとんど持たない

2.2.1. アーキテクチャ

  • アーキテクチャはオニオンアーキテクチャを採用
  • オニオンのレイヤー
    • プレゼンテーション層
      • Http Requestでわたされた値とユースケース層に渡す値のマッピング
      • 入力のvalidation(一部)
    • ユースケース層
      • ドメインオブジェクトの生成、使用、永続化
      • ドメインオブジェクトからプレゼンテーション層に渡す値への変換
    • インフラ層
      • リポジトリ(実装クラス)
    • ドメイン層
      • ドメインオブジェクト(エンティティ、値オブジェクト)
      • ドメインの知識の表現
      • ドメインサービス
      • リポジトリ(インターフェース)
      • リポジトリの仕様定義

2.2.2. ドメインモデル貧血症のコード

@Setter
@Getter
public class Task {
  private Long id;
  private TaskStatus taskStatus;
  private String name;
  private LocalDate dueDate;
  private int postponeCount;
}
  • ドメイン知識(ルール/制約)を持っていない

    • タスクは未完了状態から始まる
    • 3回まで延期できる
    • 延期は1日ずつ
  • なのでユースケースでそれを(制約を破らないように)気をつけて実装しないといけない

    • ドメインの制約条件をユースケースで気にしないといけない
    • 情報漏洩が起きてソフトウェアの複雑性がましてるね
public class TaskCreateUseCase {
  @Autowired
  private TaskRepository taskRepository;
  public void createTask(String name, LocalDate dueDate) {
    if (name == null || dueDate == null) {
      throw new IllegalArgumentException("必須項目が設定されていません");
    }
    Task task = new Task();
    task.setTaskStatus(TaskStatus.UNDONE); // ①
    task.setName(name);
    task.setDueDate(dueDate);
    task.setPostponeCount(0); // ②
    taskRepository.save(task);
  }
}
public class TaskPostponeUseCase {
  @Autowired
  private TaskRepository taskRepository;
  private static int POSTPONE_MAX_COUNT = 3; // ③
  public void postponeTask(Long taskId) {
    Task task = taskRepository.findById(taskId);
    if (task.getPostponeCount() >= POSTPONE_MAX_COUNT) { // ④
      throw new IllegalArgumentException("最大延期回数を超過しています");
    }
    task.setDueDate(task.getDueDate().plusDays(1L)); // ⑥
    task.setPostponeCount(task.getPostponeCount() + 1); // ⑤
    taskRepository.save(task);
  }
}

2.2.3. ドメインモデル貧血症のコードの問題点

  • 問題点1: 不整合なデータをいくらでも作れる
    • 全ての属性のセッターがpublicになってる
      • 不用意にこんなことはしてはいけない
  • 問題点2: 仕様を追いかけるのに、多くのクラスをコード参照から追う必要がある
    • 不変条件がエンコーディングされていない
      • さらにいうと型にエンコーディングされていない
      • 型でエンコーディングしていると違反しているとそもそもコンパイルできなくなって嬉しい
    • 制約が満たされているかコードの隅々までチェックしないといけない
    • そもそも制約はなんなのかがコードからわかるとも限らない

2.2.4. ドメインモデルの知識を表現した実装

  • ドメイン知識をドメイン層に抑えるとユースケースがシンプルになる
public class TaskCreateUseCase {
  @Autowired
  private TaskRepository taskRepository;
  @Transactional
  public void createTask(String name, LocalDate dueDate) {
    // 生成時のルール/制約に関する実装がなくなっている
    Task task = new Task(name, dueDate);
    taskRepository.save(task);
  }
}
public class TaskPostponeUseCase {
  @Autowired
  private TaskRepository taskRepository;
  @Transactional
  public void postpone(Long taskId) {
    // 更新時のルール/制約に関する実装がなくなっている
    Task task = taskRepository.findById(taskId);
    task.postpone();
    taskRepository.save(task);
  }
}
  • 以上のようにドメインに関する制約を一切気にしなくてよくなる
    • ドメインに対して、HowがなくなりWhat(何をしたいか)しか書いてない
    • ユースケースは、ユースケースが関心を寄せるところに関してだけHowをかくべし
  • じゃあドメインの実装はどう変わる?
@Getter
// @Setter // ←Setterは無くす ④
public class Task {
  private Long id;
  private TaskStatus taskStatus;
  private String name;
  private LocalDate dueDate;
  private int postponeCount;

  // コンストラクタ: 新規登録時の処理
  public Task(String name, LocalDate dueDate) {
    if (name == null || dueDate == null) {
    throw new IllegalArgumentException("必須項目が設定されていません");
    }
    this.name = name;
    this.dueDate = dueDate;
    this.taskStatus = TaskStatus.UNDONE; // ①
    this.postponeCount = 0;
  }
  private static final int POSTPONE_MAX_COUNT = 3; // ②

  // 延期時の処理
  public void postpone() { // ③
    if (postponeCount >= POSTPONE_MAX_COUNT) {
      throw new IllegalArgumentException("最大延期回数を超過しています");
    }
    dueDate.plusDays(1L);
    postponeCount++;
  }
}
  • ドメインのことを理解したければ、そのドメインのクラだけを見ればよくなった

2.3. ドメイン層オブジェクト設計の基本方針

  • ドメイン層のオブジェクトを設計する際に重要な方針2つ
    • ドメインモデルの知識を対応するオブジェクトに書く
    • 常に正しいインスタンスしか存在させない

2.3.1. ドメインモデルの知識を対応するオブジェクトに書く

  • ドメインの知識はドメイン層にかく
    • コーディング規約にしてもいい
      • 俺の考えた最強設計バトルを避ける

2.3.2. 常に正しいインスタンスしか存在させない

  • そもそも規約に違反したインスタンスは作れないようにする

    • そのために
      • 生成条件の強制
      • ミューテーション条件の強制
  • あとこれに加えて代数的データ型を駆使してそもそも制約を満たさないような値はその型にしょぞくできないという条件を作った方がいい

  • 生成条件の強制

    • コンストラクタや、ファクトリーメソッドを通して生成する
  • ミューテーション条件の強制

    • 正しいミューテーションメソッドのみ公開する
    • セッターはやめよう
      • つかってないならなおさら可読性を著しく下げる
    • あとまぁ、デフォルトイミュータブルの言語を使おう...
      • 可変はよくない

2.4. 実際の開発の進め方

  • 少しずつ改善していくにはテストが必要

2.5. Q&A

2.5.1. ユビキタス言語の管理方法

  • ドメインモデル図を電子化するのが一案
    • PlantUMLというツールが便利
    • リポジトリで管理するとなお良い

2.5.2. モデリングにかける時間

  • スコープの広さによるけど、1,2時間ぐらいと決める

2.5.3. ドメインモデル図の更新頻度

  • 特に決めない。頻繁に行われる
    • 実装とドメインモデル図を乖離させない

2.5.4. モデリング時に作成するその他の成果物

  • 状態遷移図
  • シーケンス図
    • ただこれはユースケースの処理設計にあたると捉えられる
      • なるほど、そういうフェーズもあるんだ
      • ユースケース層の設計
    • ドメインの制約というよりは処理順序の話なので

2.5.5. ユースケース図を使う理由

  • 何が具体例があった方がいいから
  • アアプリケーションとし何を行うかという点に注目したときに向いてると思ったから

2.5.6. 他のモデリング手法の違い

  • 例えばRDA,ICONNIXはソフトウェア全体の要求分析から入る
    • スコープがでかい
ose20ose20

3. DDD固有のモデリング手法

  • DDDの用語説明

3.1. 集約

3.1.1. 集約とは

  • 定義
    • 必ず守りたい強い整合性を持ったオブジェクトのまとまり

3.1.2. 設計/実装時のルール

  • 2つのルール
    • 強い整合性確保が必要なものを1つの集約にする
    • トランザクションを必ず1つにする

強い整合性確保が必要なものを1つの集約にする

  • 部と部員のモデルを考える
    • 最初は未承認、部員が5以上で承認、4以下になると未承認に戻る
    • このとき、部(の承認状態)と部員数には強い整合性が必要
  • ドメインモデル図は下のような感じ
    • 部(部集約)
      • <集約ルート>
      • 部ID
      • 名前
      • 承認状態
    • 部員(部集約)
      • 部ID
      • 生徒ID
    • 生徒(生徒集約)
      • <集約ルート>
      • 部ID
  • すべてのオブジェクトはいずれかの集約にはいる
    • これ関数型ドメインモデリングで言っていたことと違う
      • IDがいらないものは集約じゃなくて値オブジェクトじゃないの?
      • この本では値オブジェクトはどうあつかわれているんだろう

トランザクションを必ず1つにする

  • 集約単位でリポジトリから取得し、リポジトリを更新する
    • 整合性を保つため
    • それがコードからわかるように、それ以外では型エラーになるようにできるといい

3.1.3. 集約の境界の決め方

  • 1つの正解があるわけじゃない
    • 2つの観点
      • 整合性を確保する必要性の強さ
      • トランザクションの範囲の適切さ

整合性を確保する必要性の強さ

  • 実装コストや難易度と相談

トランザクションの範囲の適切さ

  • DBなどつかっているデータストアへの影響と相談

3.2. 境界づけられたコンテキスト

3.2.1. 境界づけられたコンテキストの概念

  • 定義

    • 特定のモデルを定義・適用する境界を明示的に示したもの(典型例はサブシステムやチーム)
  • 具体例を考える

    • ECサイトを作る場合
    • 「商品」というモデル
      • それを扱う人によって、イメージするもの、期待する操作、制約、必要な情報が違う
        • 販売部門
        • 配送部門
        • 請求部門
        • 経理部門
      • これを全部まとめるんじゃなくて、別のコンテキストとして切り出し、
        それぞれのコンテキストに商品をモデル化する

3.2.2. 境界づけられたコンテキストの実装

  • アーキテクチャの話をしてからの方がいいので5.7でやる

3.3. Q&A

3.3.1. DBへの意識

Q. ドメインモデル図を考えるときにどうしても無意識にDB構成を意識してしまうので呪縛から逃れたい
A. NoSQLを想定するのは1つの案。つまりRDBじゃなくて純粋なオブジェクトしてどう持てばいいか考える

3.3.2. 集約とトランザクション

Q. 集約を考えるときにDBトランザクションについて考えてしまう。意識すべきことはあるか?
A. それはしょうがない。本当はモデリング時には実装のことを考えない方がいいが、集約とトランザクションはなかなか切り離せない

ose20ose20

4. 設計の基本原則

  • 基本原則がある
    • 高凝集・低結合
    • DDD固有の話ではなく汎用的な話

4.1. 凝集度・結合度

  • 1970年代に提唱されてソフトウェア工学の標準標語となった

  • 高凝集・低結合で達成すること

    • Understandability: コードを理解しやすくなる
    • Flexibility: コードを修正、拡張しやすくなる
    • Reliability: 修正時にバグを埋め込みにくくなる
    • Reusability: 同じコードを別の場所で使いやすくなる
    • Testability: テストがしやすくなる
  • 凝集度、結合度は、「モジュール」ごとに考える

    • クラス、アーキテクチャーのレイヤー、アプリケーション

4.2. 凝集度

4.2.1. 定義

  • 1つのモジュールについて、責務、データ、振る舞いの関連の強さの尺度
    • このモジュールは何をするモジュールなのか?という問いへの答え安さでも近似できる
      • これは開発時常に念頭におくべき

4.2.2. 低凝集な実装

public class OperationUtil {
  private int count = 0; 
  // 責務が曖昧なクラス名
  public void increment() {
    count ++;
  }

  public static void greet() {
    System.out.println("hello.");
  }
}
  • 何をしているクラスなの?に答えにくい
    • 便利屋とかして関係ないものがどんどん追加され、だんだんメンテが難しくなる

4.2.3. 高凝集な実装

public class Counter {
  private int count = 0;
  public void increment() {
    count ++;
  }
  public int getCurrentCount() {
    return count;
  }
}
  • 何をしているクラスなの?に答えられる

4.3. 結合度

4.3.1. 定義

  • 複数のモジュール同士が依存している度合いの尺度

  • 低い方がいい

    • モジュール間の依存をできるだけ少なくする

4.3.2. 高結合な実装

public class Printer {
  public static void print() {
    System.out.println(Counter.number); // 処理結果がCounterの内部の値に依存している
  }
}

public class Counter {
  public static int number = 0;
  public static void increment(){
    number ++;
  }
}
Printer.print(); // 0 が出力される
Counter.increment();
Printer.print(); // 1 が出力される → この振る舞いは外側から想像が難しい
  • PrinterのprintメソッドがCounterの内部状態に依存していて、なおかつそれがシグネチャ(型)からもわからない
    • 実装を追わないといけない
    • 内密結合といって最も結合度が高い状態
    • Printer、Counterどちらを編集する場合でもその使われ方を把握しないといけない

4.3.3. 低結合な実装

public class Printer{
  public static void print(int number){
    System.out.println(number);
  }
}

public class Counter {
  private static int number = 0;
  public static int getNumber(){
    return number;
  }
}
// 使い方
Printer.print(Counter.getNumber())
  • Counterへの内部状態への依存が消えた

4.4. 今後の章との関連

  • モジュールの責務について常に考えることが大事
ose20ose20

5. アーキテクチャ

  • アプリケーション全体にまたがる設計をアーキテクチャと呼ぶ

  • 本章の流れ

    • 3層アーキテクチャの紹介とその問題点
    • それを解決するためのDDDのアーキテクチャ

5.1. 3層アーキテクチャ

  • 一般的なWebアプリケーションで使うアーキテクチャ

    • プレゼンテーション層
      • クライアントとの入出力
    • ビジネスロジック層
      • ユースケースの実装
      • ドメイン知識の表現
    • データアクセス層
      • データベースとの入出力
      • ドメイン知識の表現
    • 依存関係
      • 上の層から下の層のみ許可する
  • 問題点

    • ビジネスロジック層が低凝集になって可読性、保守性が低下する
      • XxxService、XxxLogicクラスが数百行、数千行になってとてもメンテナンスできない...

5.1.1. タスク管理アプリケーションの例

  • 3層アーキテクチャの問題点を例で考える

  • 仕様として以下を考える

    • ユーザー登録、非活性化できる
    • メールアドレスは重複できない
    • タスク登録、更新、完了、未完了に戻す、延期、ユーザーへのアサインができる
    • タスクの延期は3回まで
    • 活性状態のユーザーにのみアサインができる
    • タスク期日をカレンダー登録できる
  • 3層アーキテクチャだと大体全部ビジネスロジックとしてビジネスロジック層に書かれることになる

    • でもDDDの観点から言うと、ユースケースとドメイン知識(ルール/制約)がごちゃごちゃになっている
      • ユースケース
        • タスクを登録する
        • タスクを延期する
        • タスクを完了する
        • タスクの担当者を設定する
        • タスクからカレンダーを登録する
      • ドメイン知識(ルール/制約)
        • ユーザー
          • メールアドレスは重複登録できない
          • 最初は活性状態で作成され、非活性化できる
          • ユーザーは姓名、メアドをもつ
        • タスク
          • 期日は1日ずつ、3回まで延期できる
          • 活性ステータスのユーザーにのみアサインできる
          • タスクステータスは未完了/完了の2種類で、未完了から始まる
  • さらに、3層アーキテクチャはドメイン知識がビジネスロジック層とデータアクセス層に分散して結合度も高まる

    • 低凝縮・高結合状態でよくない

5.2. レイヤードアーキテクチャ

  • 直近で見た、ユースケースとドメイン知識がごちゃまぜになっているという問題にアプローチをかけたアーキテクチャ

    • 注意
      • これはエヴァンズ本の命名に従っている
      • このあと出てくるものも階層化されているのでレイヤードではあるけど、
        レイヤードアーキテクチャの子分類というわけではない
  • レイヤードアーキテクチャ

    • 階層
      • プレゼンテーション層
        • クライアントとの入出力
      • アプリケーション層
        • ユースケースの実現
      • ドメイン層
        • ドメイン知識の表現
      • インフラ層
        • DBとの入出力
    • 依存関係
      • 上の層から下の層のみ許可
  • これの問題点

    • ドメイン層がインフラ層に依存している
      • 特定のDBやORマッパーに依存しているので、これを変えたければドメインもいじらないといけない
    • モデルを継続的に改善し、それをソフトウェアに反映していく上で、ドメインがインフラ技術に依存せず独立していることが重要

5.3. オニオンアーキテクチャ

5.3.1. レイヤーごとの責務

  • レイヤードアーキテクチャに、依存性逆転の原則を適用してドメイン層とインフラ層の依存関係を逆転させたのがオニオンアーキテクチャ

  • 階層

    • プレゼンテーション層
      • Http Requestで渡された値とユースケース層に渡す値のマッピング
      • 入力値の一部validation
    • ユースケース(アプリケーション)層
      • ドメインオブジェクトの生成、使用、永続化依頼
      • ドメインオブジェクトからプレゼンテーション層に渡す値への変換
      • ドメイン層のAPIを組み合わせてユースケースを実現する
    • ドメイン層
      • ドメインオブジェクト(エンティティ、値オブジェクト)
      • ドメイン知識(ルール/制約)の表現
      • ドメインサービス
      • リポジトリ(インターフェース)
      • リポジトリの仕様定義
      • 整合性が保証できるAPIのみを他の層に公開する
    • インフラ層
      • リポジトリオブジェクトの検索/永続化の実装
  • 依存関係

    • リポジトリ
      • ドメイン層にI/Fがあり、インフラ層にそれの実装がある
    • それ以外はレイヤードと同じ
  • ユースケース層 vs アプリケーション層

    • オリジナルは後者の命名だけど、わかりにくいので前者で呼ぶ

5.3.2. 丸型の表記

  • アーキテクチャが玉ねぎみたいに丸型で図解されることがある
    • 中心から外側に向かって
      • 1層
        • ドメイン
      • 2層
        • ユースケース
      • 3層
        • プレゼンテーション
        • インフラ
          となっている
    • 依存関係
      • 外側から内側の向きにしかapi callができない
    • さらにその外側から最外層へのI/Fはアダプターが提供して、アプリケーション内の言語に変換をする

5.4. ヘキサゴナルアーキテクチャ

  • 実践DDD本で紹介されているアーキテクチャ
  • アプリケーション層というコアを中心に、外界との通信は専用のポートとアダプターを使えという思想
    • オニオンと似ているが、こっちの方が荒い
      • オニオンはさらにレイヤーを詳細化している

5.5. クリーンアーキテクチャ

  • 上述のアーキテクチャを受けて統一概念として生み出された
    • 基本的にはヘキサゴナルアーキテクチャの思想を引き継いでいる

5.6. オニオンアーキテクチャ、クリーンアーキテクチャの比較

  • 著者はオニオンアーキテクチャをおすすめしている
    • サブセクションが理由

5.6.1. シンプルさ

  • クリーンアーキテクチャは登場する要素が多いが、一般的なWebアプリだと考慮不要なものもある

5.6.2. レイヤーの責務の思想の違い

  • 命名が違うので読み替えが必要
  • 境界づけられたコンテキストが考慮されていない

5.7. 境界づけられたコンテキストの実装

  • オニオンアーキテクチャを踏まえて、どう実装する?
    • シンプルさをとるなら、1コンテキスト1アプリケーションのマイクロサービスイメージ
    • すでに組織が細分化されている場合、大体1コンテキストでどうにかなりそうかな

5.7.1. 1コンテキスト1アプリケーションの場合

  • コンテキスト間の通信
    • 同期
      • 同期REST API
    • 非同期
      • 非同期REST API
      • メッセージキューなど

5.7.2. 1コンテキスト1プリケーション以外の場合

  • 実装言語のパッケージシステム、モジュールシステムなどを使うといい
src
└── main
    └── java
        └── dddsample
            ├── delivery
            │   ├── app
            │   ├── domain
            │   ├── infrastructure
            │   └── presentation
            └── sales
                ├── app
                ├── domain
                ├── infrastructure
                └── presentation
ose20ose20

6. ドメイン層の実装

  • 以下をキーワードに実装を進める
    • ドメインモデルを表現するもの(ドメインオブジェクト)
      • エンティティ
      • 値オブジェクト
      • ドメインイベント
    • ドメインオブジェクトを使用するもの
      • リポジトリ
      • ファクトリー
      • ドメインサービス

6.1. エンティティ

  • IDで同一性を判定する
    • 一部のフィールドが変わっても同じだと判定したいようなものはたくさんある
      • 社員
        • 部署が変わっても、体重が変わっても、山田さんは山田さん
  • 可変になる可能性があるものとしてモデリング(必須じゃない)
    • 別に不変でいいと思う
    • デフォルトイミュータブルの言語でも全然扱えるし、そっちの方が個人的には好き

6.2. 値オブジェクト

  • 保持している値で同一性を判定する
  • 不変なものとしてモデリング

6.3. ドメインサービス

  • モデルをオブジェクトとして表現すると無理があるものの表現に使う
    • e.g. 集合に対する操作とか
  • ただし、極力エンティティと値オブジェクトで実装するようにする
    • そうしないと結局従来のビジネスロジック層の感覚で書いてしまって肥大化する
    • 意識しないと慣れた方法でやりがちなので、最初は特に気をつける

6.4. リポジトリ

  • 定義
    • 集約単位で永続化へのアクセスを提供するもの
  • 注意点
    • 集約の親となるドメインオブジェクトを決める、これが集約ルートである
    • リポジトリは集約単位でつくる
      • リポジトリからの返却は集約ルート
        • 紐づくオブジェクトはの集約ルートからの参照として常に扱う
        • その子オブジェクト用のリポジトリを用意しない
    • リポジトリはListのように扱う
      • ListのようなシンプルなI/Fに止めるべき
      • ドメイン知識を埋め込むのは責務過剰
        • モックが作りにくくなるのでテストもしにくくなるよ

6.5. ファクトリー

  • 生成ロジックが複雑な場合はそれようにオブジェクトを切り出してもいい

6.6. ドメイン層のそれ以外のオブジェクト

  • 上記は一例でしかない、DDDがやりたい目的が一番大切
    • それができるなら他の方法でもいい
    • enumつかったりrecordつかったり型をもっと丁寧に使ったり

6.7. 複数集約間の整合性確保

  • 二つのアプローチ
    • ユースケース層のメソッドで確保
      • 実装は簡単だけど、違反する実装が書きやすい
    • ドメインイベントを用いて結果整合性を担保する
      • ドメイン層のオブジェクトの操作に対してイベントを発火させ、別集約の操作をトリガーする
      • 複雑、本書では割愛
        • 実践DDD本のドメインイベントの章に解説がある
          • 読書案内ありがたい

6.8. Q&A(ドメインオブジェクト)

6.8.1. ドメイン層にロジックを寄せる目的

  • Q. 安定依存の原則に反していないか、大丈夫か
  • A. 反しているが大丈夫
  • 補足
    • 安定依存の原則(SDP)
      • モジュールはより安定した(変更頻度が少ない)モジュールに依存するべきだ
      • これが、継続的に修正を前提とするドメインに多くのモジュールが依存していて違反している
    • DDDはとにかくドメインを大事することでソフトウェアの複雑度を下げようとしている
      • なので、ドメインが何かに依存していて、その依存先の事情でドメインが歪められるのを避けたい
      • だからこの構成を取る必要がある
        • したがってDDDでは問題ない
      • 影響はテストしよう
        • あるいは不変条件を型にエンコードして、そもそもドメインのルールに違反していたらコンパイルを通らないようにしよう

6.8.2. プリミティブ型の使用可否

  • なんでもかんでもプリミティブ型をつかうなということではない
    • しかし、それがドメインであるならばプリミティブ型であることはまずないだろう

6.8.3. DBの値からインスタンスを再構成する方法

  • DBからとってきた値から整合性の取れたオブジェクトを表現する型に変換する処理を定義する

6.8.4. 再構成メソッドを書くべきレイヤー

  • Javaだとパッケージの可視性に関する機能が不十分で
    依存関係をきれいにつくれない(RustにおけるFromトレイトの実装ができない)場合があるので妥協する

6.9. Q&A(リポジトリ)

6.9.1. ドメインオブジェクトとDBの対応

  • フィールドを一対一で対応させるとは限らない
  • この自由度が生まれるのは、Active Record型ライブラリを使用してテーブルとオブジェクトが1対1に制限される時には無い面白さ

6.9.2. ソートの実装方法

  • Q. order byをどうするか
  • A. I/Fではkeyをenumとかで指定できるようにしておけばいい

6.9.3. キャッシュの実装方法

  • Q. キャッシュを検討したい。どこで実装する?
  • A. 後述するCQRSを適用すると自然に解決する

6.9.4. 外部APIとリポジトリの関係

  • Q. Twitterのような外部APIを利用する時もリポジトリパターンを使うのか
  • A. やりとりする値がドメインモデルとして意味を持つならそう
    • そうで無い場合の例
      • ユースケース層にhogeAdapterといったインターフェースを定義して
        インフラ層で実装する
        • ユースケースにI/Fを置くいい例となっている

6.9.5. 外部APIから取得した値の詰め替え方法

  • 外部からドメインオブジェクト相当のものを取得したい場合
    • どう取得するかはインフラ層に隠蔽するべき
      • 取得および変換までインフラ層でやる

6.9.6. エンティティ同士の紐付け

  • 同じ集約ならインスタンス参照
  • そうでないならID参照

6.9.7. リポジトリのインターフェースを定義するレイヤー

  • Q. なぜユースケースじゃなくてドメインなのか
  • A. 集約というドメインモデルにおいて非常に重要な概念と結びついているから
    • 凝集度を高めることにもつながる

6.9.8. 一部カラムを更新するときの扱い

  • 一部だけ更新でもオブジェクト全体を渡す
    • 集約単位という原則を思い出そう
    • 整合性のかんりをするため

6.9.9. ドメインオブジェクトからリポジトリ操作の可否

  • Q. ユースケースからじゃなくてエンティティからリポジトリのapiをつかっていいか
  • A. 責務が複数になるので非推奨

6.10. Q&A(その他)

6.10.1. ドメインサービスの命名

  • Q. hogehogeService じゃなくてもいい?
  • A. はい

6.10.2. リポジトリを通じた削除方法

  • エンティティを削除するときはIDだけでいい
ose20ose20

6. ドメイン層の実装

  • 以下をキーワードに実装を進める
    • ドメインモデルを表現するもの(ドメインオブジェクト)
      • エンティティ
      • 値オブジェクト
      • ドメインイベント
    • ドメインオブジェクトを使用するもの
      • リポジトリ
      • ファクトリー
      • ドメインサービス

6.1. エンティティ

  • IDで同一性を判定する
    • 一部のフィールドが変わっても同じだと判定したいようなものはたくさんある
      • 社員
        • 部署が変わっても、体重が変わっても、山田さんは山田さん
  • 可変になる可能性があるものとしてモデリング(必須じゃない)
    • 別に不変でいいと思う
    • デフォルトイミュータブルの言語でも全然扱えるし、そっちの方が個人的には好き

6.2. 値オブジェクト

  • 保持している値で同一性を判定する
  • 不変なものとしてモデリング

6.3. ドメインサービス

  • モデルをオブジェクトとして表現すると無理があるものの表現に使う
    • e.g. 集合に対する操作とか
  • ただし、極力エンティティと値オブジェクトで実装するようにする
    • そうしないと結局従来のビジネスロジック層の感覚で書いてしまって肥大化する
    • 意識しないと慣れた方法でやりがちなので、最初は特に気をつける

6.4. リポジトリ

  • 定義
    • 集約単位で永続化へのアクセスを提供するもの
  • 注意点
    • 集約の親となるドメインオブジェクトを決める、これが集約ルートである
    • リポジトリは集約単位でつくる
      • リポジトリからの返却は集約ルート
        • 紐づくオブジェクトはの集約ルートからの参照として常に扱う
        • その子オブジェクト用のリポジトリを用意しない
    • リポジトリはListのように扱う
      • ListのようなシンプルなI/Fに止めるべき
      • ドメイン知識を埋め込むのは責務過剰
        • モックが作りにくくなるのでテストもしにくくなるよ

6.5. ファクトリー

  • 生成ロジックが複雑な場合はそれようにオブジェクトを切り出してもいい

6.6. ドメイン層のそれ以外のオブジェクト

  • 上記は一例でしかない、DDDがやりたい目的が一番大切
    • それができるなら他の方法でもいい
    • enumつかったりrecordつかったり型をもっと丁寧に使ったり

6.7. 複数集約間の整合性確保

  • 二つのアプローチ
    • ユースケース層のメソッドで確保
      • 実装は簡単だけど、違反する実装が書きやすい
    • ドメインイベントを用いて結果整合性を担保する
      • ドメイン層のオブジェクトの操作に対してイベントを発火させ、別集約の操作をトリガーする
      • 複雑、本書では割愛
        • 実践DDD本のドメインイベントの章に解説がある
          • 読書案内ありがたい

6.8. Q&A(ドメインオブジェクト)

6.8.1. ドメイン層にロジックを寄せる目的

  • Q. 安定依存の原則に反していないか、大丈夫か
  • A. 反しているが大丈夫
  • 補足
    • 安定依存の原則(SDP)
      • モジュールはより安定した(変更頻度が少ない)モジュールに依存するべきだ
      • これが、継続的に修正を前提とするドメインに多くのモジュールが依存していて違反している
    • DDDはとにかくドメインを大事することでソフトウェアの複雑度を下げようとしている
      • なので、ドメインが何かに依存していて、その依存先の事情でドメインが歪められるのを避けたい
      • だからこの構成を取る必要がある
        • したがってDDDでは問題ない
      • 影響はテストしよう
        • あるいは不変条件を型にエンコードして、そもそもドメインのルールに違反していたらコンパイルを通らないようにしよう

6.8.2. プリミティブ型の使用可否

  • なんでもかんでもプリミティブ型をつかうなということではない
    • しかし、それがドメインであるならばプリミティブ型であることはまずないだろう

6.8.3. DBの値からインスタンスを再構成する方法

  • DBからとってきた値から整合性の取れたオブジェクトを表現する型に変換する処理を定義する

6.8.4. 再構成メソッドを書くべきレイヤー

  • Javaだとパッケージの可視性に関する機能が不十分で
    依存関係をきれいにつくれない(RustにおけるFromトレイトの実装ができない)場合があるので妥協する

6.9. Q&A(リポジトリ)

6.9.1. ドメインオブジェクトとDBの対応

  • フィールドを一対一で対応させるとは限らない
  • この自由度が生まれるのは、Active Record型ライブラリを使用してテーブルとオブジェクトが1対1に制限される時には無い面白さ

6.9.2. ソートの実装方法

  • Q. order byをどうするか
  • A. I/Fではkeyをenumとかで指定できるようにしておけばいい

6.9.3. キャッシュの実装方法

  • Q. キャッシュを検討したい。どこで実装する?
  • A. 後述するCQRSを適用すると自然に解決する

6.9.4. 外部APIとリポジトリの関係

  • Q. Twitterのような外部APIを利用する時もリポジトリパターンを使うのか
  • A. やりとりする値がドメインモデルとして意味を持つならそう
    • そうで無い場合の例
      • ユースケース層にhogeAdapterといったインターフェースを定義して
        インフラ層で実装する
        • ユースケースにI/Fを置くいい例となっている

6.9.5. 外部APIから取得した値の詰め替え方法

  • 外部からドメインオブジェクト相当のものを取得したい場合
    • どう取得するかはインフラ層に隠蔽するべき
      • 取得および変換までインフラ層でやる

6.9.6. エンティティ同士の紐付け

  • 同じ集約ならインスタンス参照
  • そうでないならID参照

6.9.7. リポジトリのインターフェースを定義するレイヤー

  • Q. なぜユースケースじゃなくてドメインなのか
  • A. 集約というドメインモデルにおいて非常に重要な概念と結びついているから
    • 凝集度を高めることにもつながる

6.9.8. 一部カラムを更新するときの扱い

  • 一部だけ更新でもオブジェクト全体を渡す
    • 集約単位という原則を思い出そう
    • 整合性のかんりをするため

6.9.9. ドメインオブジェクトからリポジトリ操作の可否

  • Q. ユースケースからじゃなくてエンティティからリポジトリのapiをつかっていいか
  • A. 責務が複数になるので非推奨

6.10. Q&A(その他)

6.10.1. ドメインサービスの命名

  • Q. hogehogeService じゃなくてもいい?
  • A. はい

6.10.2. リポジトリを通じた削除方法

  • エンティティを削除するときはIDだけでいい
ose20ose20

7. ユースケース(アプリケーション)の実装

7.1. ユースケース

  • ユースケースの実現
    • 具体的にすること
      • オブジェクトの生成、変換、リポジトリへの永続化
public class TaskPostponeUseCase {
  @Autowired
  private TaskRepository taskRepository;
  @Transactional
  public void postpone(Long taskId) {
    Task task = taskRepository.findById(taskId);
    task.postpone();
    taskRepository.save(task);
  }
}
  • ドメイン知識をドメイン層に隠蔽できると抽象度の高い操作ができるようになる
  • ここでドメインの整合性について気にする必要はなくなる

7.2. ユースケースからの戻り値クラス

  • ユースケース層からプレゼンテーション層に返す型に関する2つの方針

      1. 専用の戻り値クラスに詰め替える(新しく用意する)
      • メリット
        • ドメインオブジェクトにプレゼンテーションに関連する処理が混入するのを防げる(責務分割)
        • ドメイン層の修正の影響をプレゼンテーション層が直接受けなくなる
      • デメリット
        • 詰め替えコストが発生する
      1. ドメイン層のクラスをそのまま返す
      • メリット/デメリットは裏返し
  • おすすめは1

    • 高凝集を目指すべし
  • 本書ではここで返す値をDTOとする

    • 一般にはレイヤ間の受け渡しに広く使われるが、ユースケースからの戻り値のみに使う

7.3. Q&A

7.3.1. クラスの分割単位

  • 1クラスに1パブリックapiがよい
    • 複数入れると凝集度が下がる
    • たとえばcliアプリとかを作ってるときは、まさにサブコマンドが1つのユースケースに相当しそう

7.3.2. 命名規則

  • 特に決まりはない
    • 一例
      • TaskUseCase, CreateTaskUseCase
ose20ose20

8. CQRS

  • Command Query Responsibility Segregation
    • コマンドクエリ責務分離
    • ユースケース層に定義

8.1. DDDの参照計処理で発生する問題

  • 前提 タスク、ユーザー、ラベルという3つの集約があり、それぞれにリポジトリがある
  • ここで、「タスクID、タスク、担当者名、ラベル」の一覧テーブルを表示する画面を作りたいとする
  • このままやると、3つのリポジトリからデータをかき集めることになる
    • 問題点
      • かき集めて詰め替えるのが見にくいコードになる
      • 画面に返す必要のない値を取得するのでパフォーマンスが最適化されていない
      • 複数集約の条件で絞り込んでのページングができない
  • このような問題は大変一般的

8.2. 解決策

  • CQRSを導入する
    • 更新系モデルはドメインオブジェクトをそのまま
    • 参照系モデルは特定のユースケースに特化した値の型を定義する
      • ユースケースの話で、その返り値はプレゼンテーションへなので、dto
public class TaskDto {
  private String taskId;
  private String taskName;
  private String userName;
  private String labelName;
}
public interface TaskQueryService {
  public List<TaskDto> fetchByUserId(UserId userId);
}

8.2.1. 参照用モデルの型を定義するレイヤー

  • DTOやクエリサービスのI/Fはユースケース層
  • その実装はインフラ層
    • 更新系と同じように、ユースケースには抽象化された部分しか露出していないので、インフラ層の実装は自由
    • 著者がJavaを使うときはjOOQというタイプセーフクエリビルダーを使っている
      • 気になる

8.3. メリット、デメリット

  • 以下を判断して採用を決めるべし
    • メリット
      • 複数集約にまたがるデータを取得する際のコードがシンプルになり、保守性が高まる
      • クエリパフォーマンスが上がる、チューニングもやりやすい
      • 複数集約の条件で絞り込みとページングができる
    • デメリット
      • ドメインオブジェクトのデータが参照されている場所が追いにくくなる
      • アーキテクチャ自体が複雑になる

8.4. 実装時の注意

8.4.1. 部分的導入の可否

  • 完全に分けず、参照の中でも必要なところだけ導入してもいい

8.4.2. 型を定義するレイヤーがユースケース層である理由

  • 最適な参照用モデルはユースケースに依存しているから
    • ドメイン知識ではない
    • また、戻り値の型は完全に一致しない限り使い回してはいけない
      • 結局どこでなにをつかってるかわからなくなる
      • これは他のところでも言えそう。ちゃんと型をつかって区別するべき

8.4.3. 更新系との整合性を確保する方法

  • テストを書こう
    • 更新処理と合わせた結合テストが有用

8.5. よくある誤解

8.5.1. データソース分離の必要性

  • データソースを分離するのは別の話
    • こっちは主にパフォーマンス改善が主眼になる
  • クラスタ化されてるときに、更新系と参照系でエンドポイントが違うのとかはこれかな

8.5.2. イベントソーシングとの関係

  • 相性がいいけど別物
  • イベントソーシングはユーザーが登録された、タスクが完了したみたいなイベントを永続化する手法

8.6. Q&A

8.6.1. クエリサービスの分割判断

  • Q. ユースケースの中で複数のクエリサービスを呼ぶか、1つにまとめるような実装をするか
  • A. そのクエリサービスの複雑性などに応じて変える

8.6.2. DTOとして返した値の扱い

  • dtoの用途イメージは、テンプレートエンジン用のViewModelだったり、APIが返すjson用クラス

8.6.3. 参照用モデルの使い所

  • あくまで必要なら。部分的導入は可能

8.6.4. 実装の都合にドメイン層が影響を受ける場合

  • N+1問題という参照時の、しかも利用ライブラリに依存するような問題に起因してドメインモデルが歪められるのは良くない
    • インフラ層で隠蔽できないか、CQRSで解決できないか考えるべし
  • トランザクションの問題はしょうがない。集約と密接に関係しているため

8.6.5. 集約内の一部の値だけ取得したい場合の対処方法

  • 基本的にはドメインオブジェクトをとってユースケースの処理で必要な値だけ抽出すればいい
    • 必要なCQRS導入しよう
ose20ose20

9. プレゼンテーション層の実装

9.1. プレゼンテーション層の処理概要

  • クライアントとアプリケーションの入出力の実現

9.2. プレゼンテーション層のクラス、ファイル

  • 一般にコントローラと呼ばれるものが置かれる
    • クライアントとの入出力に必要なもの全て
      • HTMLテンプレート
      • webアプリフレームワークの設定ファイル

9.3. リクエスト仕様の定義

  • 通信プロトコル、エンドポイント、パラメーターなど

9.4. レスポンス仕様の定義

  • UIとして露出させるために必要なものはここが責務

    • ドメイン層やユースケース層に漏洩させない
  • Ruby on Railsのこんとろーらーとは違う

    • Model, View, Controllerの3つのモジュールに分ける
    • よくない
      • 低凝集・高結合になりがち

9.5. Q&A

9.5.1. コントローラーの処理内容

  • コントローラーもUIもプレゼンテーション層でやる

9.5.2. プレゼンテーション層における値オブジェクト生成

  • それはユースケース層の責務として方がいい
ose20ose20

10. アーキテクチャ全般・ライブラリなど

10.1. 例外処理

10.1.1. 例外の種類

  • 以下の2つに分類する
    • 想定外の例外
      • Web APIで500系のエラーを返すような場合
      • ユーザーは通常ユースケースとしての回避策がないから共通のレスポンスを返す
        • 非検査例外を投げてプレゼンテーション層の共通クラスで一律キャッチ
    • 通常のユースケースで発生しうる例外
      • 400系のエラー
      • 内容に応じてそのレイヤーを投げるレイヤーが異なる
        • それぞれのレイヤーにおけるvaidationを行い、そのレイヤーで定義したエラーを投げる
          • その例外をどう扱うかは、それを使うレイヤー側で決める
          • そのまま持ち上げるのか、リカバリーを行うのか

10.1.2. バリデーション内部の重複

  • ドメイン層とプレゼンテーション層などでvalidationしたい項目が一部被ることはある
    • まず第一に、ドメイン層でのvalidationはサボってはいけない
      • ドメイン駆動ではないですね
        • いい加減なドメインを扱うことになる
    • かぶってもいい

10.1.3. 処理結果を例外で表現しない場合

  • いわゆるResult型のようなものを使う場合
    • RustとかScalaとかKotlnも?
    • できるなら使いたい

10.2. パッケージ

  • パッケージ構成の例
src
└── main
    └── java
        └── org.littlehands.dddsample
            ├── application
            │   ├── shared
            │   ├── task
            │   └── user
            ├── config
            ├── domain
            │   ├── shared
            │   ├── task
            │   ├── task_report
            │   └── user
            └── presentation
                ├── api.user
                └── web.user
  • 各レイヤーの下には処理のカテゴリや集約ごとにわけるとよい

共有パッケージ

  • 該当するレイヤーのしたにsharedなどのパッケージを作り、共通して利用するオブジェクトなどを配置する

10.3. Webフレームワークへの依存

  • Webフレームワークにどこまで依存するか問題
    • ドメイン層
      • 一切依存しない形が望ましい
        • DIはしゃあない
    • ユースケース層
      • 部分的に可能
      • DIはok
      • ロギングは?
        • ユースケース層にロガーのI/Fを持ちインフラ層で実装すると良い
    • プレゼンテーション
      • ここは責務上仕方がない
      • むしろ極力ここで押し留められるかが腕の見せどころ

10.4. ORマッパー

  • 代表的なのがActive Record型のテーブルと対応したクラス
    • これはドメイン層としてそのままつかってはいけない
      • 主なデメリット
        • セッターがあるため更新に制限をかけられない
        • テーブルとオブジェクトが切り離せなくなるのでドメインに、本来全く関係ない実装部分の制限が反映されちゃう
    • なのでこれはインフラ層に閉じ込めて、ドメイン層の型に詰め替える

10.5. 言語

  • 向いている言語は静的型付け言語
    • 制約を型として表現できるから
    • その表現のしやすさ、使いやすさ、エコシステムなどを考えると以下のtier表くらい?
      • Scala, Kotln, Rust, Haskell, F#
      • Java, Go
      • その他型なし(動的型付け)言語

10.6. Q&A

10.6.1. アーキテクチャの部分適用

  • 適用範囲と基準を明文化してからやるべき