Closed15

『「実践ドメイン駆動設計」から学ぶDDDの実装入門』要点・考えたことメモ

ピン留めされたアイテム
susansusan

『「実践ドメイン駆動設計」から学ぶDDDの実装入門』(ISBN-10: 4798161500) を読む。
読みながら、自分に役立ちそうな要点をまとめたり、考えたことをメモしていく。
私はDjango使いなのでDjangoに引きつけて考えたこともメモする。

ピン留めされたアイテム
susansusan

全体の内容要点まとめ

DDDとは

  • 顧客と開発者が共通のユビキタス言語で業務を理解・共有し、ソフトウェアを開発する手法
  • 戦略的設計(ドメイン/境界づけられたコンテキスト)と戦術的設計(値オブジェクト・ドメインイベント・リポジトリなど)の両輪で構成
  • 軽量DDDの落とし穴:戦術的設計のみ行って事業価値が発揮されないこと

ドメインの切り分け

  • コアドメイン/支援サブドメイン/汎用サブドメイン:事業的優先度や独自性で分類
  • 境界づけられたコンテキスト:ユビキタス言語の整合性が崩れる境界で分割し、1ドメインにつき1コンテキストで1チーム担当を原則

コンテキストマップ

  • 複数コンテキスト間の関係性(Upstream/Downstream)を俯瞰
  • RESTやJSON/XML、ACL(腐敗防止層)などの統合パターンで連携方式を定義
  • 非同期設計や結果整合性を重視し、依存を最小限に

サンプルプロジェクトのアーキテクチャ進化

  • レイヤード → DIP → ヘキサゴナル(ポート&アダプター) → CQRS → イベントソーシング
  • 依存性逆転(DIP)により上下層を抽象化し、技術切り替えを容易に
  • ダッシュボード画面の描写のためにCQRSを導入。高速化のため事前にテーブルをJOINして非正規化したマテリアライズドビューを使用するなど

ドメインモデル層の要素

  • エンティティ:一意識別子で同一性を保証し、振る舞いを持つ
  • 値オブジェクト:属性の集合として不変
  • 集約:整合性を保ちながらデータを更新する単位。オブジェクトの集まりのCRUDのライフサイクル管理を行う
  • ドメインサービス:エンティティ・値オブジェクト・集約を横断するビジネスロジックをステートレスに実装

アプリケーション層

  • ユースケース単位の「薄い」サービス群
  • コマンド(更新)とクエリの役割を分離
  • DTOは結合度低減、DPOは性能とドメイン検証を重視し、トレードオフを適宜選択
  • 描画データ取得時、ユースケースで最適化したクエリをリポジトリに用意し、値オブジェクトを返す方法もある

モジュール設計

  • 高凝集・疎結合を目指し、ユビキタス言語に沿ったパッケージ/名前空間構成
  • 循環依存の回避、概念ごとの責任分担に基づく命名規約
  • ドメインモデル層はコンセプト単位でディレクトリを切る
  • アプリケーション層はドメインモデルとは別ディレクトリに配置し、機能単位でディレクトリを切る
ピン留めされたアイテム
susansusan

私の考えたこと・感じたことまとめ

導入コストの見極め

  • 単純なマスタメンテナンスやそれほど大規模でないユースケースフローの規模では、DDD適用により、かえって過剰・複雑化するため見極めが重要
  • 例えば、フレームワーク標準機能(例:DRFのGenericAPIView)で簡潔に実装できるCRUDを、無理にDDDで実装した場合、かえって複雑度が増大し、生産性が低下しそうだ

ルール整備と秩序の維持コスト

  • 本書によるとビジネスロジックの定義場所がエンティティ/値オブジェクト/集約/ドメインサービスと多岐に渡る。そのため、どういう種類の処理はどこに定義するべきかについて、プロジェクトへの新規参画者にも明確に判断できる基準が整備されている必要がある
  • 開発者間で上記のようなルールを形成・合意・維持・教育していくことも、コミュニケーションコストを含め、時間的・認知的に高コストになりそう
  • 秩序が崩れると、シンプルなレイヤ構成よりもゴチャついて混乱を招きやすいものになりそう

モジュール・ファイル構成の懸念

  • Java/C#由来の「1クラス1ファイル」「コンセプト単位のネストした末端ディレクトリ」は、Python/Djangoでは過剰に感じられる
  • ファイル数・階層が深くなると探索が大変になり、Pythonの場合、再エクスポート作業が煩雑化する
  • Djangoでは、デフォルトのシンプルなアプリ構成(models.py, views.py など)を基本としつつ、services.pyや必要に応じてrepositories.py 位の粒度のファイルを配置するのが好ましく感じる
susansusan

「はじめに」要点まとめ

DDDとは

顧客と開発者が業務を戦略的に理解し、共通の言葉を使いながらシステムを発展させる手法

DDDで使う設計パターン

  • 戦略的設計: チームで使うパターン(ドメイン、境界づけられたコンテキスト)
  • 戦術的設計: テクニカルなパターン(値オブジェクト、ドメインイベント、リポジトリ、etc.)

軽量DDDとは

「戦略的設計」を実施せず、「戦術的設計」にだけ注力し、事業価値を発揮できない貧弱なDDD

susansusan

「第1章 「DDDへの誘い」~ドメイン駆動設計のメリットと始め方~」要点まとめ

3W(what/why/who)1H(how)でDDDを説明している。

what

DDDは「事業を理解し、チームの知識を1つにまとめる」ことを重視し、それを「ユビキタス言語」というチームの共通語で実装する。「顧客と開発者が共通言語で会話して、一体感あるチームとして、事業価値の高いソフトウェアを開発する」。

who

ドメインエキスパートとは、担当業務やシステムに一番詳しい人。職種や肩書に関係ない。

why3: ドメインモデルによる「複雑さ」への対処

トランザクションスクリプトとドメインモデルの違い

トランザクションスクリプト: 「処理をその手順で記述する」「呼び出し処理ごとにプログラムを記述する」
ドメインモデル: 「業務をモデルとして表し、適切な箇所で一元管理する」

DDDのコストが高くつく場合

「単純なマスタメンテナンスや30程度のユースケースフロー」では、DDDを導入するコストのほうが高く付く。
最初から複雑とわかっているシステムや、将来的に複雑になるとわかっている場合に、メリットがある。

how1: ユビキタス言語で設計

ユビキタス言語

ユビキタス言語とは、同じ用語を使って話すという表面的なものではなく、自然言語をもとに「チーム全体で作り上げる特別な共有言語」。DSLの一種ともいえる

ユビキタス言語の見つけ方

  1. ドメインに登場する用語の名称とアクションを記載
  2. 用語集を作成する。用語の定義を書く
  3. 用語集の作成が困難ならば、既存ドキュメントを集めて重要な用語を取り出す

ユビキタス言語を見つけた後のステップ

そのままソースコードに反映する。
例: 「顧客」に「性名を変える」振る舞いがあればそのままドメインモデルにメソッドとして実装する。
用語集はそのうち使用しなくなり、ドキュメントの代わりに「コード内のモデル」「会話」として、ユビキタス言語をメンテナンスする。
言い換えれば、ユビキタス言語に含まれない概念はコードにも存在しないことになる

susansusan

第1章 考えたこと

「単純なマスタメンテナンスや30程度のユースケースフロー」では、DDDを導入するコストのほうが高く付く。

あるDRFの現場では、DRFのGenericAPIView(CreateAPIViewなどの汎用APIViewやModelViewSetなど)を使えばCRUDを簡単に実装できるところを、軽量DDD的なアーキテクチャで統一しなければならないという規約があるために、比較的簡単なCRUDについてもコードの記述量やファイル量が増え、複雑化している現場があった。
これなどは「DDDを導入するコストのほうが高く付く」場合に思える。
(余談だがその現場は何故かDRF標準のシリアライザを使わず、attrsでバリデーションをしていた)

そう考えるとDDD導入時の検討点として、アーキテクチャ統一を重視するあまり、フレームワークの標準機能で単純に実装できるユースケースまで、構造が無駄に複雑化しないか、という観点を追加できそうだ。

susansusan

「第2章 「ドメイン」「サブドメイン」「境界づけられたコンテキスト」~DDDで取り組む領域~」要点まとめ

全体像

  • 広義のドメイン: チームが取り組む事業全体
    • 狭義のドメイン: 問題領域、戦略課題を分析/明確化
      • コアドメイン: 事業的に最も重要で最優先で取り組まれる部分
      • サブドメイン: コアドメインではない部分
        • 支援サブドメイン: 業務的に特別だが、重要ではないもの(例: コアドメインの支援を行う独自機能)
        • 汎用サブドメイン: 業務的に特別でないが、必要なもの。交換可能なもの(例: 認証機能、ERP、EC)
    • 境界づけられたコンテキスト: 解決領域、戦略課題を解決

「境界づけられたコンテキスト」の粒度

1つの「コアドメイン」or「サブドメイン」に、1つの「境界づけられたコンテキスト」が対応する

「境界づけられたコンテキスト」の分割

DDDでは巨大な「エンタープライズモデル」は設計しない。
ユビキタス言語の意味が変わる境界で「境界づけられたコンテキスト」を分割する。
例えば「アカウント」という言語が、2つ以上の意味を持たないように分割しプログラムの複雑化を防ぐ。

「境界づけられたコンテキスト」の担当

1つの「境界づけられたコンテキスト」は1つのチームで担当する。
そのためドメインエキスパートは裁量でユビキタス言語を定義しシンプルなモデルを構築できる。

本書のサンプルプロジェクト「SaaSOvation」

  • コアドメイン: アジャイルプロジェクト管理ドメイン
  • 支援サブドメイン: コラボレーションドメイン
  • 汎用サブドメイン: 認証・アクセスドメイン
susansusan

「第3章 「コンテキストマップ」~境界づけられたコンテキスト間のチーム関係~」要点まとめ

コンテキストマップとは

「コンテキストマップ」とは複数の「境界づけられたコンテキスト」の関係を俯瞰する地図。
U(Upstream)は上流、D(Downstream)は下流。Uは影響を与える側、Dは影響を受ける側。

例:

アジャイルプロジェクト管理コンテキスト D ---------- U 認証・アクセスコンテキスト
アジャイルプロジェクト管理コンテキスト D ---------- U コラボレーションコンテキスト
コラボレーションコンテキスト D ---------- U 認証・アクセスコンテキスト

コンテキストマップを書く理由

システム間の連携方法を把握するため。
他チームとのコミュニケーションの必要性を判断するため。

コンテキストマップの共有場所

チームの作業エリア。活発なwikiポータル。

コンテキスト間の「統合パターン」

  1. 共有カーネル: 共通ソースコード(共通ドメインモデル)を共用する。他チームの承認が必要となるため、この部分は極力小さくするべき。
  2. 巨大な泥団子: 既存システムが大規模で複雑な場合。1つの塊として使用。
  3. 公開ホストサービス(OHS): コンテキストの内容にアクセスする公開サービス。RESTやSOAP。
  4. 公表された言語(PL): 2つのコンテキストのモデル変換で使う。JSONやXML。
  5. 腐敗防止層(ACL): 上流のドメインモデルの影響を受けないように、下流で変換レイヤを用意。

サンプルプロジェクト「SaaSOvation」を例に

他コンテキストに依存しない設計

DDDでは複数コンテキスト間の連携を実装するため、非同期処理がよく使用される。
リアルタイムの場合、接続エラーや性能の懸念があるため。
他コンテキストにアクセスできなくても自コンテキストが正しく動作するようにする。

結果整合性

最終的に整合性が取れていれば、リアルタイム性が重要でないことも多い(銀行口座などの分野は別)

ミニマル指向

ミニマル指向: 腐敗防止層でモデルの変換を行う際、リモートモデルの属性のうち必要なものだけをローカルモデルに連携すること

susansusan

「第4章 「アーキテクチャ」~レイヤからヘキサゴナルへ~」要点まとめ

サンプルプロジェクト「SaaSOvation」のアーキテクチャ変遷(抜粋)

サンプルプロジェクト「SaaSOvation」はDDDを採用していたからこそ、
以下のように柔軟にアーキテクチャを拡張できた。

  1. レイヤアーキテクチャ
  2. レイヤアーキテクチャに「依存関係逆転の原則」を導入: DIとUTを導入、UI層や永続化層を入替
  3. ヘキサゴナルアーキテクチャ: スマホへの対応、認証の一元管理、ポート&アダプターによりNoSQLやメッセージングに対応
  4. CQRSの導入: 複雑なダッシュボードの表示が可能に
  5. イベントソーシングの導入: ドメインモデルでのすべての変更内容を追跡することで法的監査に対応

依存関係逆転の原則(DIP)

上位が下位に依存するのをやめ、実装が抽象に依存するべきという原則。
DIコンテナ、サービスファクトリ、プラグインなどの方式が使用される。

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

アプリケーション層とドメイン層を中心に据え、
入力(ユーザー操作/自動テスト)も出力(DB/モック)も、差替可能なインタフェースとして扱う。
「ドメイン」部分は機能やユースケースを基に設計し、
「ポート&アダプター」部分は技術的なインタフェース別で設計する。

CQRS: Command Query Responsibility Segregation

複雑化したダッシュボード画面のために「SaaSOvation」はCQRSを導入。
CQRSは以下の流れ。

  1. コマンド処理: 更新処理を開始
  2. コマンドモデル: 更新処理を実行、ドメインイベントを発行
  3. コマンドモデル用データストア: 更新結果を保存
  4. クエリモデル(ビュー)を更新: サブスクライバがドメインイベントを受信し、クエリモデルの更新を実行
  5. クエリモデル用データストア: 描画用のデータが格納されている。高速化のため事前にテーブルをJOINして非正規化したマテリアライズドビューを使用することもある。マテビューを使うときはDBの標準機能を使用する場合もあるし、プログラムで事前にデータを構築する場合もある。性能の観点から複数台のレプリカを持つこともある。
  6. クエリモデル: 表示用の非正規化されたデータモデル
  7. クエリ処理: 結果をJSONで戻したりする
susansusan

「第5章 「エンティティ」~一意な識別子で同一性を識別~」要点まとめ

DDDでよくある失敗

ドメインモデル貧血症

DB項目のプロパティしか存在しないモデル。データと処理を一緒に取り扱わない。これを避けるためDOAでなくOOAで設計するべき。

DDD採用自体が間違っている場合

エンティティが多数を占める単純なCRUDシステムであれば、DDDを採用したこと自体が間違っている可能性もあります。このような場合にはDDDを採用せず、普通に開発したほうがよいでしょう

モデリングとエンティティ発見の流れ

  1. 要件の理解: ドメインエキスパートの要望を聞く。成果物: 簡単な業務仕様の列挙、ユースケースやユーザーストーリーなど
  2. モデリング: ユビキタス言語構築を意識して、ドメインエキスパートと議論。エンティティの抽出においては、「変更」の主語がエンティティとなる可能性が高い
  3. エンティティを識別する属性を検討
  4. 「一意な識別子」設計: UUID/シーケンス/ORMの代理識別子 など。Javaでは equals/hashCode の実装など。
  5. エンティティの振る舞いを検討
  6. エンティティの作成(コンストラクタ/ファクトリ)を検討
  7. エンティティのバリデーションを検討: 属性/オブジェクト/複数オブジェクトのバリデーション
susansusan

「第7章 「ドメインサービス」~複数の物を扱うビジネスルール~」要点まとめ

DDDにおけるサービスとは

エンティティ・値オブジェクト・集約の外に記述したほうがよいロジックをステートレスな「サービス」として実装する。
次の2つがある。

  1. ドメインサービス: 複数ドメインオブジェクトを使って計算する処理やファサード
  2. アプリケーションサービス: 薄く、ドメインモデルのタスク調整に使う。トランザクション等のドメイン外の関心事を扱う。 (例: 腐敗防止層など)

DDDの「ドメインサービス」と一般的な「サービス」の違い

  • ドメインサービス: ドメインモデルが扱う粒度の細かい振る舞いを担う。主に、エンティティ・値オブジェクト・集約を利用する。ユビキタス言語として表現される
  • 一般的なサービス: ビジネスロジックを使いやすい粒度にまとめたコンポーネント

ドメインサービスの失敗例

多用/誤用

ドメインサービスの危険な点は、ユビキタス言語ではないビジネスロジックを大量に記述できてしまう点です。特にDDDに慣れていない場合、トランザクションスクリプトで処理を書いてしまう可能性があります。使う必要がない箇所でドメインサービスを利用してしまうと、エンティティや値オブジェクトが空っぽのドメインモデル貧血症を引き起こす危険性があります。

ミニレイヤ

ドメインサービス層という「ミニレイヤ」を常に作ってしまうアンチパターン。ドメインサービスのレイヤを作る必要がない場合は導入しないようにする。

アプリケーションサービス層との混同

アプリケーションサービスは利用者側であり、ドメイン内部のロジックを知る必要はない。

ドメインサービス設計時の検討点

  1. セパレートインタフェースが必要か
  2. サービスの生成方法

セパレートインタフェース

「セパレートインタフェース」は実装クラスと別のパッケージにインタフェースを定義すること。
メリットは、実装クラスもクライアントもインタフェースに依存する為、依存が複雑にならないこと。

「実装クラスのみ」と「セパレートインタフェース」の使い分け

必ずしもセパレートインタフェースにする必要はない。
今後実装クラスを差し替える可能性があるならセパレートインタフェースを使えば良い。

セパレートインタフェースの場合のサービスの生成方法

  1. コンストラクタやメソッドのパラメタを使ってインスタンスを設定
  2. オブジェクトを生成するファクトリを使う
  3. SpringなどのDIコンテナを用いてサービスのインスタンスを注入する

IDDD本(『実践ドメイン駆動設計』)ではどれが正解ではなく、状況に応じた選択をするとよいとしている。

susansusan

第7章 考えたこと

ドメインサービス: ドメインモデルが扱う粒度の細かい振る舞いを担う。主に、エンティティ・値オブジェクト・集約を利用する。ユビキタス言語として表現される

ということはDDDにおいては、ビジネスロジックが書かれる場所が、
エンティティ・値オブジェクト・集約・ドメインサービスと4種類もあることになる。
(本書を全部読んでいないので、さらに他にもあるかもしれないが)

どういう種類の処理はどこに定義するべきかについて、
プロジェクトへの新規参画者にも明確に判断できる基準が整備されている必要がありそうだ。
そうでない限り秩序を保つのが難しそう。
そして開発者間で秩序についてのルールを形成・合意・維持・教育していくことも、
コミュニケーションコストを含め、時間的・認知的に高コストになりそう。

もし秩序が崩れた場合、DDDを導入せず単純なレイヤー構成にした場合よりもゴチャゴチャしそうだ。
ある箇所では処理がエンティティに定義されており、別の箇所では似たような処理がドメインサービスに定義されるなどといった具合に。

susansusan

「第9章 「モジュール」~高凝集で疎結合にまとめる~」要点まとめ

DDDでのモジュールとは

モジュール ≒ Javaのパッケージ、C#の名前空間。
高凝集で疎結合が理想。

モジュールの凝集度

特定の機能に対する責任を持って、正しくまとまっているかを示す指標

モジュールの結合度

モジュール内の変更が別のモジュールに影響しないよう、適切に分割できているか示す指標

モジュールの設計方法

IDDD本におけるモジュール設計ルール(抜粋)

  • モデリングの概念にフィットさせ設計する。例えば集約に対して1つのモジュールを用意する
  • モジュール名をユビキタス言語に従う
  • モジュール同士の循環依存が起きないようにする
    • 循環依存は避けるべきだが、親子関係(上位と下位)に限り、やむを得ない場合がある

モジュールの命名規約(C#のSaasOvationを参考に)

トップレベル下のコンテキスト名

プロダクト名の変更の影響を受けない為に、「境界づけられたコンテキスト」名をつける。

コンテキスト配下の階層名

Domain.Model.コンセプト名

  • 値オブジェクト、エンティティ、イベント、Enumなどを入れる
  • ドメインモデル内ではサービスという用語を使わない(Serviceというディレクトリを作らない)

Application.機能名

  • ドメインオブジェクトを利用するアプリケーションレイヤを、機能単位(Teams, Sprints, Productsなど)で分割する
  • 機能ディレクトリ配下に、アプリケーションサービスとコマンドを格納する

IDDD本のサンプルプロジェクトのソースコードを確認(C#)

$ tree -L 2
.
├── AgilePM.csproj
├── Application
│   ├── ApplicationServiceLifeCycle.cs
│   ├── Notifications
│   ├── Processes
│   ├── Products
│   ├── Sprints
│   └── Teams
├── Domain.Model
│   ├── Discussions
│   ├── Products
│   ├── Teams
│   └── Tenants
├── IDDD.AgilePM.nuspec
├── Properties
│   └── AssemblyInfo.cs
├── Settings.StyleCop
└── packages.config
$ tree -L 5
.
├── AgilePM.csproj
├── Application
│   ├── ApplicationServiceLifeCycle.cs
│   ├── Notifications
│   │   └── NotificationApplicationService.cs
│   ├── Processes
│   │   └── ProcessApplicationService.cs
│   ├── Products
│   │   ├── BacklogItems
│   │   │   └── BacklogItemApplicationService.cs
│   │   ├── InitiateDiscussionCommand.cs
│   │   ├── NewProductCommand.cs
│   │   ├── ProductApplicationService.cs
│   │   ├── RequestProductDiscussionCommand.cs
│   │   ├── RetryProductDiscussionRequestCommand.cs
│   │   ├── StartDiscussionInitiationCommand.cs
│   │   └── TimeOutProductDiscussionRequestCommand.cs
│   ├── Sprints
│   │   ├── CommitBacklogItemToSprintCommand.cs
│   │   └── SprintApplicationService.cs
│   └── Teams
│       ├── ChangeTeamMemberEmailAddressCommand.cs
│       ├── ChangeTeamMemberNameCommand.cs
│       ├── DisableMemberCommand.cs
│       ├── DisableProductOwnerCommand.cs
│       ├── DisableTeamMemberCommand.cs
│       ├── EnableMemberCommand.cs
│       ├── EnableProductOwnerCommand.cs
│       ├── EnableTeamMemberCommand.cs
│       └── TeamApplicationService.cs
├── Domain.Model
│   ├── Discussions
│   │   ├── DiscussionAvailability.cs
│   │   └── DiscussionDescriptor.cs
│   ├── Products
│   │   ├── BacklogItems
│   │   │   ├── BacklogItem.cs
│   │   │   ├── BacklogItemCategoryChanged.cs
│   │   │   ├── BacklogItemCommitted.cs
│   │   │   ├── BacklogItemDiscussion.cs
│   │   │   ├── BacklogItemDiscussionInitiated.cs
│   │   │   ├── BacklogItemDiscussionRequested.cs
│   │   │   ├── BacklogItemId.cs
│   │   │   ├── BacklogItemMarkedAsRemoved.cs
│   │   │   ├── BacklogItemScheduled.cs
│   │   │   ├── BacklogItemStatus.cs
│   │   │   ├── BacklogItemStatusChanged.cs
│   │   │   ├── BacklogItemStoryPointsAssigned.cs
│   │   │   ├── BacklogItemStoryTold.cs
│   │   │   ├── BacklogItemSummarized.cs
│   │   │   ├── BacklogItemType.cs
│   │   │   ├── BacklogItemTypeChanged.cs
│   │   │   ├── BacklogItemUncommitted.cs
│   │   │   ├── BacklogItemUnscheduled.cs
│   │   │   ├── BusinessPriority.cs
│   │   │   ├── BusinessPriorityAssigned.cs
│   │   │   ├── BusinessPriorityRatings.cs
│   │   │   ├── BusinessPriorityTotals.cs
│   │   │   ├── EstimationLogEntry.cs
│   │   │   ├── IBacklogItemRepository.cs
│   │   │   ├── StoryPoints.cs
│   │   │   ├── Task.cs
│   │   │   ├── TaskDefined.cs
│   │   │   ├── TaskDescribed.cs
│   │   │   ├── TaskHoursRemainingEstimated.cs
│   │   │   ├── TaskId.cs
│   │   │   ├── TaskRemoved.cs
│   │   │   ├── TaskRenamed.cs
│   │   │   ├── TaskStatus.cs
│   │   │   ├── TaskStatusChanged.cs
│   │   │   └── TaskVolunteerAssigned.cs
│   │   ├── IProductRepository.cs
│   │   ├── Product.cs
│   │   ├── ProductBacklogItem.cs
│   │   ├── ProductBacklogItemPlanned.cs
│   │   ├── ProductCreated.cs
│   │   ├── ProductDiscussion.cs
│   │   ├── ProductDiscussionInitiated.cs
│   │   ├── ProductDiscussionRequestTimedOut.cs
│   │   ├── ProductDiscussionRequested.cs
│   │   ├── ProductId.cs
│   │   ├── ProductReleaseScheduled.cs
│   │   ├── ProductSprintScheduled.cs
│   │   ├── Releases
│   │   │   ├── IReleaseRepository.cs
│   │   │   ├── Release.cs
│   │   │   ├── ReleaseId.cs
│   │   │   └── ScheduledBacklogItem.cs
│   │   └── Sprints
│   │       ├── CommittedBacklogItem.cs
│   │       ├── ISprintRepository.cs
│   │       ├── Sprint.cs
│   │       └── SprintId.cs
│   ├── Teams
│   │   ├── IProductOwnerRepository.cs
│   │   ├── ITeamMemberRepository.cs
│   │   ├── ITeamRepository.cs
│   │   ├── Member.cs
│   │   ├── MemberChangeTracker.cs
│   │   ├── ProductOwner.cs
│   │   ├── ProductOwnerId.cs
│   │   ├── Team.cs
│   │   ├── TeamMember.cs
│   │   └── TeamMemberId.cs
│   └── Tenants
│       └── TenantId.cs
├── IDDD.AgilePM.nuspec
├── Properties
│   └── AssemblyInfo.cs
├── Settings.StyleCop
└── packages.config
susansusan

第9章 考えたこと

サンプルコードのモジュール階層を見て

コンセプト単位で末端のディレクトリを切ることについて

Domain.Model.コンセプト名

  • 値オブジェクト、エンティティ、イベント、Enumなどを入れる

末端のディレクトリをコンセプト単位にして、
値オブジェクト、エンティティ、イベント、Enumなどをフラットに配置せよとのこと。
だがそれによって、値オブジェクトとエンティティとEnumを、ディレクトリ名やファイル名から区別することができない。

よってこのプロジェクトに慣れてない人=新規参画者は、目当ての処理や定義を探す場合、
1つ1つファイルを開いて探さなければならず、大変そうだ。

さすがにファイルが多すぎないか

│ │ ├── BacklogItems
│ │ │ ├── BacklogItem.cs
│ │ │ ├── BacklogItemCategoryChanged.cs
│ │ │ ├── BacklogItemCommitted.cs
│ │ │ ├── BacklogItemDiscussion.cs
│ │ │ ├── BacklogItemDiscussionInitiated.cs
│ │ │ ├── BacklogItemDiscussionRequested.cs
│ │ │ ├── BacklogItemId.cs
│ │ │ ├── BacklogItemMarkedAsRemoved.cs
│ │ │ ├── BacklogItemScheduled.cs
│ │ │ ├── BacklogItemStatus.cs
│ │ │ ├── BacklogItemStatusChanged.cs
│ │ │ ├── BacklogItemStoryPointsAssigned.cs
│ │ │ ├── BacklogItemStoryTold.cs
│ │ │ ├── BacklogItemSummarized.cs
│ │ │ ├── BacklogItemType.cs
│ │ │ ├── BacklogItemTypeChanged.cs
│ │ │ ├── BacklogItemUncommitted.cs
│ │ │ ├── BacklogItemUnscheduled.cs
│ │ │ ├── BusinessPriority.cs
│ │ │ ├── BusinessPriorityAssigned.cs
│ │ │ ├── BusinessPriorityRatings.cs
│ │ │ ├── BusinessPriorityTotals.cs
│ │ │ ├── EstimationLogEntry.cs
│ │ │ ├── IBacklogItemRepository.cs
│ │ │ ├── StoryPoints.cs
│ │ │ ├── Task.cs
│ │ │ ├── TaskDefined.cs
│ │ │ ├── TaskDescribed.cs
│ │ │ ├── TaskHoursRemainingEstimated.cs
│ │ │ ├── TaskId.cs
│ │ │ ├── TaskRemoved.cs
│ │ │ ├── TaskRenamed.cs
│ │ │ ├── TaskStatus.cs
│ │ │ ├── TaskStatusChanged.cs
│ │ │ └── TaskVolunteerAssigned.cs

Domain.Model/Products/BacklogItems の配下に35個もファイルがある。
ChatGPTに聞いたら、C#は

同一ファイル内に複数の public クラスを定義可能です。ただし、チームのコーディング規約や可読性確保のために「1クラス1ファイル」を慣習として採用するケースが多数派である

とのこと。Python使いとしてはディレクトリの中に35個もファイルが入っていたらかなりゴチャゴチャしていると感じる。

Djangoに引きつけて考えたこと

ディレクトリ構成はシンプルな方が好ましい

Djangoでappを作成するために $ python manage.py startapp <app名>とすると以下のようになる。

app名/
    admin.py
    apps.py
    migrations/
    models.py
    tests.py
    views.py

私の考えではこれくらいシンプルなのが好ましい。
あとは適宜、以下をappの直下に追加するようなイメージが好ましい。

  • consts.py(定数定義。Enum定義などもここに入れる)
  • exceptions.py(自作の例外など)
  • serializers.py(DRFの場合)
  • services.py
  • repositories.py(リポジトリパターンを採用している場合)

Djangoの公式コマンドでappを作成した場合、デフォルトではこれくらいシンプルだと言うことである。
それなのに、なぜDjangoで開発するのに、わざわざJavaやC#のように1ファイル1クラスの粒度にしてファイルをゴチャゴチャ増やしたがるのか?
もっと言うと、JavaやC#の土壌を前提とするDDD(ほとんどのDDD関連書籍のサンプルコードはJavaかC#だ)を、そのモジュール構成まで含めてDjangoに適用するべきなのか? 疑問である。

コンセプトや機能別にまとめたいとしても、app単位で分割するか、
階層を浅く留める(本書の例で言えば、services/teams.py, services/sprints.py 位など)のが好ましく思える。

余談: コンセプトや機能別でディレクトリ階層を切って、パスが深くなる場合のペイン

あるDjangoの現場では、
app_name/models/aaa/bbb/ccc/xxx.py というように、
models配下に、コンセプトや機能別で深いディレクトリ階層が切られていた。

そして、深いパスのモデルを models からインポート可能にするために、
いちいち1ファイル1クラスずつ models/__init__.py で再エクスポートしなければならなかったのが大変面倒だった。これが1つのapp/modelsについて何十クラス(何十ファイル)分もあった。

さらに、modelsだけでなく、サービスやリポジトリについても同様だったため、
全体では膨大な数となっていた。

IDEにPythonのパスをクリップボードにコピーする拡張機能を入れていたからなんとかなったが、もしもそれがなかったら想像を絶する大変さとなっていただろう。

susansusan

「第14章 「アプリケーション」~ドメインモデルを利用するクライアント~」要点まとめ

アプリケーション層(アプリケーションサービス)とは

ユースケースのイベントフローごとにメソッドを提供する。
調整役で薄い処理を行うだけのレイヤ。

UI層からドメイン層への更新処理データの受け渡し方法

コマンド(更新の指示情報をまとめた入れ物)を渡して更新依頼する

ドメイン層からUI層への描画データの受け渡し方法

1. DTO

専用のDTOを用意して、複数の集約の情報を詰め替える。

2. DPO

複数の集約インスタンス(ドメインモデルのオブジェクト)の参照をプロパティで持つ入れ物。UI層にドメインモデルを公開することになる。集約のプロパティに遅延ロード項目がある場合は、事前にロードが必要。

3. ユースケースに最適化したクエリ

ユースケースで最適化したクエリをリポジトリに用意し、値オブジェクトを返す方法。
リポジトリにファインダーメソッドを用意し、複数集約を組み合わせた値オブジェクトを返す。
ユースケースに対応した処理をドメイン層に用意しリポジトリで最適化する。

DTOとDPOのトレードオフ

DTOの場合、UI層に対してドメインモデルを隠せる。
DPOの場合、UI層からドメインモデルが見える。

DTOは、詰替え処理によるメモリ使用とガベージコレクションコストが発生するが、ドメインモデルへの依存がなく結合度が低くなる。
他方、DPOは、詰め替え処理がない分性能が良く、ドメインモデルによるバリデーションが行える。ただし結合度は高くなる。
IDDD本では、好みや最終目標のトレードオフで選ぶことを推奨。

このスクラップは2ヶ月前にクローズされました