ドメイン駆動設計(DDD)の実装コードをレビューする:落とし穴とチェックポイント
質の高いコードレビューを目指す上で、単に構文の誤りやバグの有無を確認するだけでなく、コードが基盤とする設計思想が適切に反映されているか、意図通りに実装されているかを見極めることは非常に重要です。特に、複雑なビジネスロジックを扱うシステムで採用されることの多いドメイン駆動設計(DDD)は、その概念をコードに正しく落とし込むことが難しく、レビューにおいても特有の観点が必要となります。
本記事では、DDDで実装されたコードをレビューする際に注目すべき主要なチェックポイントと、陥りがちな落とし穴について解説いたします。DDDに関する基本的な知識があることを前提としていますが、コードレビューに役立つ実践的な観点に焦点を当てます。
DDD実装レビューの重要性
DDDは、ソフトウェア開発の中心にドメイン(業務領域)の専門知識を置き、複雑なビジネスロジックを適切にモデル化することを目指すアプローチです。DDDを適切にコードに反映することで、保守性、拡張性、変更容易性の高いシステムを構築できるとされています。
しかし、DDDの概念は多岐にわたり、その解釈や実装方法は開発者によって異なる場合があります。概念を誤ってコードに適用すると、DDDのメリットを享受できないばかりか、かえってコードの可読性や保守性を損なう可能性があります。レビュアーは、コードがDDDの原則や意図に沿って実装されているかを確認し、チーム全体のDDD理解を深め、コードベースの健全性を保つ役割を担います。
主要なDDD構成要素とレビュー観点
DDDには様々な概念がありますが、コードレビューで特に注目すべき主要な構成要素とそのレビュー観点について述べます。
1. エンティティ(Entity)と値オブジェクト(Value Object)
- 概念の混同: エンティティ(同一性を持つオブジェクト、ライフサイクルがある)と値オブジェクト(属性の等価性でのみ識別されるオブジェクト、不変)が適切に区別され、実装されているか確認します。例えば、住所のように、その属性の組み合わせが同じであれば区別する必要がないものは値オブジェクトとして実装されているか、といった観点です。
- 値オブジェクトの不変性: 値オブジェクトは一度生成されたら状態が変化しない(不変)であるべきです。値オブジェクトのフィールドが
final
(Javaなど)やreadonly
(C#など)になっているか、またはセッターなどの状態を変更するメソッドが存在しないかを確認します。 - 自己カプセル化: エンティティは自身の状態と振る舞いをカプセル化し、外部から直接内部状態を変更されないようにするべきです。状態を変更する際は、ドメインの操作として意味のあるメソッド(例えば、
order.addItem(item)
のような振る舞いを表すメソッド)を経由しているか確認します。
// 値オブジェクトの良い例 (不変)
public final class Address {
private final String street;
private final String city;
public Address(String street, String city) {
if (street == null || city == null) {
throw new IllegalArgumentException("...");
}
this.street = street;
this.city = city;
}
// ゲッターのみ提供
public String getStreet() { return street; }
public String getCity() { return city; }
// 等価性の判断 (equals, hashCode)
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
// エンティティの良い例 (振る舞いをカプセル化)
public class Order {
private OrderId id; // 同一性
private List<OrderItem> items;
private OrderStatus status;
// コンストラクタ
// 振る舞いを表すメソッド
public void addItem(OrderItem item) {
if (status == OrderStatus.CANCELLED) {
throw new DomainException("...");
}
items.add(item);
// 関連するドメインイベントの発行など
}
// ゲッターなど
}
2. 集約(Aggregate)
- 集約ルートの役割: 集約ルート(Aggregate Root)は、集約内の整合性を維持する責任を持ちます。外部から集約内の他のオブジェクト(エンティティや値オブジェクト)に直接アクセスして状態を変更していないか確認します。集約内のオブジェクトへの変更は、必ず集約ルートのメソッドを経由して行われるべきです。
- 集約境界の適切性: 集約は、常に一貫性が保たれるべきオブジェクトのまとまりです。他の集約への参照を持つ場合、それは通常、その集約の識別子(ID)による参照であるべきです。他の集約内のオブジェクトへの直接参照や、他の集約の状態を頻繁に変更するようなコードは、集約境界の定義が適切でない可能性を示唆します。
- 整合性の維持: 集約ルートのメソッド内で、集約内のビジネスルール(不変条件)が守られているか確認します。例えば、注文集約で商品を削除する際に、商品の合計金額がゼロ未満にならないか、といったチェックが集約ルートのメソッド内に記述されているか、などです。
// 集約の良い例 (集約ルート経由で内部状態を変更)
public class Order {
private OrderId id; // 集約ルート
private CustomerId customerId; // 他の集約のIDを参照
private List<OrderItem> items = new ArrayList<>();
private OrderStatus status = OrderStatus.CREATED;
// コンストラクタ
public void addOrderItem(Product product, int quantity) {
// 集約内の整合性チェック
if (status != OrderStatus.CREATED) {
throw new DomainException("Cannot add item to processed order.");
}
// OrderItemはここでは値オブジェクトや内部エンティティとして扱われる
OrderItem newItem = new OrderItem(product.getId(), quantity, product.getPrice());
items.add(newItem);
// 必要に応じてドメインイベントを発行
}
// 外部から直接 items.add(...) を呼び出すようなメソッドは避けるべき
}
3. リポジトリ(Repository)
- 集約単位の操作: リポジトリは集約単位でオブジェクトの取得や永続化を行います。集約ルートのみを取得/保存し、集約内部のオブジェクト単体を直接操作するようなメソッドがないか確認します。
- ドメインロジックとの分離: リポジトリは永続化のメカニズム(DBアクセス、外部API呼び出しなど)に関わる部分であり、ドメインの振る舞いを記述する場所ではありません。ドメインロジックがリポジトリに混入していないか確認します。また、複雑なクエリロジックが必要な場合、それがドメインの知識に基づいて適切に表現されているかを見極めます。
4. ドメインサービス(Domain Service)
- ステートレス: ドメインサービスは通常、状態を持たないべきです。複数の集約やリポジトリを跨ぐような操作や、特定のエンティティに所属させることが不自然なドメインの振る舞いを記述する場所です。インスタンス変数に状態が保持されていないか確認します。
- 責務の明確化: ドメインサービスの名称やメソッド名が、どのようなドメインの振る舞いを実行するのか明確に示しているか確認します。
5. ドメインイベント(Domain Event)
- イベントの表現力: ドメインイベントは、ドメイン内で「何かが起こった」事実を表現します。イベント名やイベントに含まれる情報が、ドメインの専門家にとって意味のある形で表現されているか確認します。
- 発行タイミング: イベントが適切なタイミング(状態変更後など)で発行されているか確認します。
6. ユビキタス言語(Ubiquitous Language)
- コードとドメイン用語の一致: クラス名、メソッド名、変数名などが、ドメインの専門家と開発チームの間で共有されている「ユビキタス言語」と一致しているかを確認します。コードがドメインの言葉で語られているか、という視点は、コードの可読性やドメイン知識の維持において非常に重要です。
DDD実装におけるその他のレビュー観点
- 疎結合と高凝集: DDDの各要素(エンティティ、値オブジェクト、集約、サービスなど)が、それぞれの責務に集中し、適切に結合されているか確認します。特に、集約間の不必要な依存関係や、境界を跨いだ密結合がないかを見極めます。
- アプリケーション層とドメイン層の分離: ユーザーインターフェースや外部システム連携、トランザクション管理といったアプリケーション層の関心事が、純粋なドメインロジックを記述するドメイン層に混入していないか確認します。
- 例外処理とドメイン例外: ドメイン内で発生する例外(ドメインルール違反など)が、技術的な例外と区別され、ドメインの言葉で表現された例外(ドメイン例外)として適切に扱われているか確認します。
レビューの進め方と注意点
- 背景理解: DDDの実装コードをレビューする際は、対象のビジネスロジックやドメインのモデル、そしてチームが採用しているDDDのスタイルや規約について事前に理解しておくことが重要です。必要であれば、ドメインの専門家や設計に関わった開発者に意図を確認します。
- コード全体を俯瞰する: 特定のクラスやメソッドだけでなく、集約全体の構成、リポジトリとの連携、サービスへの配置など、コード全体の構造とDDDの概念が整合しているかを俯瞰的に見ます。
- 意図を質問する: コードの背後にあるDDD的な意図が不明確な場合は、レビュイーに質問し、考えを共有してもらうことが有効です。例えば、「なぜこれを値オブジェクトにしたのですか?」「このメソッドを集約ルートに置いたのはなぜですか?」といった質問は、レビュイー自身の理解を深めるきっかけにもなります。
- 原則に立ち返る: DDDの原則(集約ルート経由での操作、値オブジェクトの不変性など)に立ち返りながら、コードがそれに従っているかを確認します。
- 過度な適用に注意: DDDの概念を無理に全てのコードに適用しようとして、かえってコードが複雑になっていないか、いわゆる「貧血症ドメインモデル」(データクラスとサービスにロジックが分散)や「豊血症ドメインモデル」(エンティティに過剰なロジックが集中)になっていないか、といったバランスも考慮します。
レビュアースキル向上のための学習方法
DDD実装のレビュー能力を高めるためには、DDDに関する知識自体を深めることが不可欠です。
- 書籍や記事での学習: Eric Evansの「Domain-Driven Design」やVaughn Vernonの「Implementing Domain-Driven Design」といった古典や、DDDに関するブログ記事などを継続的に学習します。
- 既存コードの探索: チーム内の既存のDDDプロジェクトのコードを読み解き、様々な実装パターンや過去の設計判断について学びます。
- 設計議論への参加: ドメインモデリングや設計に関するチーム内の議論に積極的に参加し、異なる視点やトレードオフについて理解を深めます。
- 実際にDDDで実装してみる: 自身で小さな機能でも良いのでDDDを意識して実装してみることで、概念の理解が深まり、レビューする際の解像度が上がります。
まとめ
DDDで実装されたコードのレビューは、単なるバグ発見に留まらず、コードがドメインの知識をいかに正確かつ効果的に表現しているか、DDDの原則が適切に守られているかを見極める高度なスキルが求められます。エンティティと値オブジェクトの区別、集約境界と集約ルートの役割、リポジトリの責務、ユビキタス言語の一致など、様々な観点からコードを診断することで、コードベースの健全性を維持し、将来にわたる保守性と変更容易性を確保することに貢献できます。
DDDは学習コストが高い側面もありますが、継続的に学び、実践的なレビュー経験を積むことで、必ず質の高いDDD実装レビューができるようになります。今回ご紹介したチェックポイントが、皆様のレビュー活動の一助となれば幸いです。