マイクロサービス ドメイン モデルの設計

ビジネス マイクロサービスまたは有界コンテキストごとに 1 つのリッチ ドメイン モデルを定義します。

ここでの目標は、ビジネス マイクロサービスまたは有界コンテキスト (BC) ごとに 1 つのまとまりのあるドメイン モデルを作成することです。 ただし、BC またはビジネス マイクロサービスは、1 つのドメイン モデルを共有する複数の物理サービスから構成される場合があることに注意してください。 ドメイン モデルは、それが表す 1 つの有界コンテキストまたはビジネス マイクロサービスのルール、ビヘイビアー、ビジネス言語、および制約をキャプチャする必要があります。

ドメイン エンティティ パターン

エンティティは、ドメイン オブジェクトを表すもので、主にその ID、継続性、および永続化によって時間とともに定義されます。エンティティを構成する属性によってのみ定義されるわけではありません。 Eric Evans 氏が言うように、"主にその ID で定義されるオブジェクトは、エンティティと呼ばれます"。 エンティティは、モデルの基礎となるため、ドメイン モデルで非常に重要です。 したがって、エンティティは、慎重に識別し、設計する必要があります。

エンティティの ID は、複数のマイクロサービスまたは有界コンテキストを横断できる。

同じ ID (つまり、同じドメイン エンティティではない可能性がありますが、同じ Id 値) を複数の有界コンテキストまたはマイクロサービス全体でモデリングできます。 ただし、同じ属性とロジックを持つ同じエンティティが複数の有界コンテキストに実装されるわけではありません。 代わりに、それぞれの有界コンテキストのエンティティは、その属性と動作を有界コンテキストのドメインで必要なものに制限します。

たとえば、バイヤー エンティティは、プロファイルまたは ID マイクロサービスのユーザー エンティティで定義された個人の属性 (ID など) の大部分を持っている可能性があります。 しかし、注文マイクロサービスのバイヤー エンティティは、持っている属性が少ない可能性があります。注文プロセスに特定のバイヤー データしか関連付けられていないからです。 各マイクロサービスまたは有界コンテキストのコンテキストは、そのドメイン モデルに影響を与えます。

ドメイン エンティティは、データ属性を実装するだけでなく、ビヘイビアーも実装する必要があります。

DDD のドメイン エンティティは、エンティティ データ (アクセスされるメモリ内のオブジェクト) に関連するドメイン ロジックまたはビヘイビアーを実装する必要があります。 たとえば、注文エンティティ クラスの一部として、ビジネス ロジックと操作を、タスク (注文項目の追加、データ検証、合計計算など) のメソッドとして実装する必要があります。 エンティティのメソッドでは、エンティティのルールをアプリケーション レイヤー全体に広げるのではなく、エンティティの不変条件とルールに対処します。

図 7-8 は、データの属性だけではなく、操作やメソッドを、関連するドメイン ロジックとともに実装するドメイン エンティティを示しています。

ドメイン エンティティのパターンを示す図。

図 7-8。 データとビヘイビアーを実装するドメイン エンティティ設計の例

ドメイン モデル エンティティでは、メソッドからビヘイビアーを実装します。つまり、"貧血症" モデルではありません。 もちろん、エンティティ クラスの一部としてどのロジックも実装しないエンティティを持つ場合もあります。 この状況は、大部分のロジックが集約ルートで定義されており、子エンティティが特別なロジックを持たない場合に、集約内の子エンティティで発生する場合があります。 ドメイン エンティティではなくサービス クラスにロジックが実装されている複雑なマイクロサービスがある場合は、ドメイン モデル貧血症になる場合があります。ドメイン モデル貧血症については、次のセクションで説明します。

リッチ ドメイン モデルと貧血症ドメイン モデルの比較

Martin Fowler は、投稿した AnemicDomainModel で、ドメイン モデル貧血症について、次のように述べています。

ドメイン モデル貧血症の基本的な症状は、一見、それが本物のドメイン モデルに見えるという点です。 オブジェクトがいくつかあり、それらはドメイン空間にある名詞から名前をつけられています。それから、オブジェクト同士がしっかりとしたリレーションで結びついており、本物のドメイン モデルと同じような構造を持っているのです。 ただし、オブジェクトのビヘイビアーを見れば違いがわかります。それらのオブジェクトにはわずかなビヘイビアーしかなく、ゲッターとセッターの入れ物にすぎないということに気づくと思います。

もちろん、貧血症ドメイン モデルを使用する場合、これらのデータ モデルは、すべてのドメインまたはビジネス ロジックをキャプチャする一連のサービス オブジェクト (従来名は "ビジネス レイヤー") から使用されます。 ビジネス レイヤーは、データ モデル上にあり、データ モデルを単にデータとして使用します。

貧血症ドメイン モデルは、手続き型の設計にすぎません。 貧血症エンティティ オブジェクトは、ビヘイビアー (メソッド) がないため、本当のオブジェクトではありません。 また、データ プロパティを保持するだけなので、オブジェクト指向設計ではありません。 すべてのビヘイビアーをサービス オブジェクト (ビジネス レイヤー) に入れると、どうしても最後にはスパゲッティ コードトランザクション スクリプトが出来上がり、その結果、ドメイン モデルが持つ利点が失われます。

とにかく、マイクロサービスまたは有界コンテキストが非常にシンプル (CRUD サービス) である場合は、エンティティ オブジェクト形式でデータ プロパティしか持たない貧血症ドメイン モデルで十分であり、より複雑な DDD パターンを実装する価値はないかもしれません。 その場合、CRUD を目的としてデータしか持たないエンティティを意図的に作成したので、これは単に永続化モデルになります。

このため、有界コンテキストにもよりますが、マイクロサービス アーキテクチャは、マルチアーキテクチャ手法に最適となります。 たとえば、eShopOnContainers の場合、注文マイクロサービスは、DDD パターンを実装しますが、カタログ マイクロサービス (シンプルな CRUD サービス) は、DDD パターンを実装しません。

貧血症ドメイン モデルは、アンチパターンであると言われる場合があります。 実際、このモデルは、実装する内容に依存しています。 作成するマイクロサービスが十分にシンプルである場合 (CRUD サービスなどである場合) は、貧血症ドメイン モデルに従い、アンチパターンになりません。 ただし、絶えず変化するビジネス ルールを多数含むマイクロサービスのドメインの複雑さに取り組む必要がある場合、貧血症ドメイン モデルは、そのマイクロサービスまたは有界コンテキストのアンチ パターンである可能性があります。 その場合は、データとビヘイビアーを含み、追加の DDD パターン (集約や値オブジェクトなど) を実装するエンティティを持つリッチ モデルとして貧血症ドメイン モデルを設計すると、そのようなマイクロサービスを長期的に成功させるために必要な大きな利点が得られる可能性があります。

その他の技術情報

値オブジェクト パターン

Eric Evans 氏は、"多くのオブジェクトは概念 ID を持たない。 これらのオブジェクトは、あるものについての特定の特性を記述している" と述べています。

エンティティには ID が必要ですが、システム内には値オブジェクト パターンのように ID を持たないオブジェクトが多数あります。 値オブジェクトは、ドメインのアスペクトを記述する概念 ID を持たないオブジェクトです。 これは、一時的にしか関係しない設計要素を表すためにインスタンス化するオブジェクトです。 関心を持つ必要があるのは、それが "" であるのかではなく、"" であるのかという点です。 例には数値と文字列が含まれていますが、属性グループのような上位レベルの概念にすることもできます。

あるマイクロサービスではエンティティであるものが、別のマイクロサービスではエンティティでないことがあります。これは、2 番目のケースで、有界コンテキストが別の意味を持つことがあるからです。 たとえば、eコマース アプリケーションのアドレスは、ID を持たない場合があります。これは、個人または会社の顧客プロファイルの属性グループしか表さない場合があるためです。 この場合、アドレスは、値オブジェクトとして分類する必要があります。 ただし、電力会社のアプリケーションでは、顧客のアドレスがビジネス ドメインで重要になります。 そのため、請求システムがアドレスに直接リンクできるようにするために、アドレスには ID が必要です。 この場合、アドレスは、ドメイン エンティティとして分類する必要があります。

通常、個人は ID を持つため、名と姓を持つ個人はエンティティです。これは、その名と姓が別の値セットに一致した場合 (たとえば、それらの名前が別の個人も指している場合) にも当てはまります。

値オブジェクトは、リレーショナル データベースや Entity Framework (EF) などの ORM では管理が難しいですが、ドキュメント指向データベースでは簡単に実装して使用できます。

EF Core 2.0 以降のバージョンには、後で詳しく説明しているように、値オブジェクトを処理しやすくする所有エンティティ機能が含まれます。

その他の技術情報

集約パターン

ドメイン モデルには、機能の重要な領域 (注文調達やインベントリなど) を制御できる、さまざまなデータ エンティティやプロセスのクラスターが含まれています。 より細かな DDD 単位は集約であり、まとまりのある単位として処理できるエンティティとビヘイビアーのクラスターまたはグループが記述されています。

通常は、必要なトランザクションに基づいて集約を定義します。 典型的な例は、注文項目のリストも含まれる注文です。 通常、注文項目は、エンティティになります。 ただし、注文集約内では子エンティティとなり、注文エンティティもそのルート エンティティとして含まれます (通常、ルート エンティティは、集約ルートと呼ばれます)。

集約を識別することが困難な場合があります。 集約は、互いに一致する必要があるオブジェクトのグループですが、オブジェクトのグループを選び、集約のラベルをつけることはできません。 ドメインの概念から開始し、その概念に関連する最も一般的なトランザクションで使用されるエンティティについて考える必要があります。 トランザクションで一致する必要があるエンティティは、集約を形成するエンティティです。 トランザクションの操作について考えることが、おそらく集約を識別するための最良の方法です。

集約ルートまたはルート エンティティ パターン

集約は、集約ルートという 1 つ以上のエンティティから構成されています (集約ルートは、ルート エンティティまたはプライマリ エンティティとも呼ばれます)。 また、集約は、複数の子エンティティと値オブジェクトを持つことができ、すべてのエンティティとオブジェクトが連携して、必要なビヘイビアーやトランザクションを実装しています。

集約ルートの目的は、集約の一貫性を維持することです。集約ルートは、集約ルート クラスのメソッドまたは操作を通じて集約に入るための唯一の更新用のエントリ ポイントである必要があります。 集約内のエンティティを変更するときは、必ず集約ルートを経由する必要があります。 これは、集約の一貫性を管理するもので、集約で従う必要がある可能性があるすべての不変条件および一貫性のルールを考慮しています。 子エンティティまたは値オブジェクトを別々に変更した場合、集約ルートは、集約が有効な状態であることを保証できません。 本来の機能を発揮できなくなるでしょう。 一貫性の管理は、集約のルートの主も重要な目的です。

図 7-9 には、バイヤー集約などのサンプル集約が示されています。バイヤー集約には、1 つのエンティティ (集約ルート バイヤー) が含まれています。 注文集約には、複数のエンティティと値オブジェクトが含まれています。

バイヤー集約と注文集約を比較している図。

図 7-9。 複数のエンティティまたは 1 つのエンティティを含む集約の例

DDD ドメイン モデルは集約から構成され、集約には 1 つまたは複数のエンティティを含めることができ、値オブジェクトも含めることができます。 バイヤー集約は、ドメインによっては、eShopOnContainers 参照アプリケーションの注文マイクロサービスの場合と同様、追加の子エンティティを持つことができることに注意してください。 図 7-9 は、集約ルートのみを含む集約の例として、バイヤーが 1 つのエンティティを持つケースを示しています。

集約を分離し、集約間の境界を明確にし続けるためには、DDD ドメイン モデルで、集約間の直接の移動を禁止し、eShopOnContainers の注文マイクロサービス ドメイン モデルに実装されている外部キー (FK) フィールドのみを持つことをお勧めします。 注文エンティティは、次のコードに示すように、バイヤーの外部キー フィールドのみを持ち、EF Core ナビゲーション プロパティは持ちません。

public class Order : Entity, IAggregateRoot
{
    private DateTime _orderDate;
    public Address Address { get; private set; }
    private int? _buyerId; // FK pointing to a different aggregate root
    public OrderStatus OrderStatus { get; private set; }
    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
    // ... Additional code
}

集約を識別し、操作するには、研究と経験が必要です。 詳細については、次の「その他の技術情報」のリストを参照してください。

その他の技術情報