私はC#を使用してWebアプリケーションを構築しています。いくつかのエンティティと値オブジェクトを持つ集約ルートがあります。次に、データベースに集約を永続化するリポジトリオブジェクトがあります(インフラストラクチャ永続化レイヤーとしてADO.NETを使用しています)。
アグリゲートにはいくつかのイベントがメソッドにアタッチされているため、アグリゲートの外で他のアクションを実行できます(たとえば、CustomerアグリゲートのメソッドOpenRequisitionがイベントRequisitionCreatedEventを起動し、ハンドラーがアクションをキューに入れて、顧客に関する統計を更新します。
これは簡略化されたサンプルです。
class Customer {
private Guid _id;
private string _shortName;
private string _legalName;
private TaxCode _taxCode;
private ContactInfo _primaryContact;
private List<Requisitions> _requisitions; //Requisition is an entity
private IDomainEvents _events;
// and so many other fields
public Requisition OpenRequisition(RequisitionDefinition definition, User requestedBy)
{
Requisition req = ... // create the new requisition somehow
_requisitions.Add(req);
_events.Invoke(new RequisitionCreatedEvent(this, req));
return req;
}
// and so on
}
私が直面している問題は、永続化の方法ではなく、データベースから集約オブジェクトを再構築する方法です。顧客のフィールドの多くには、ShortName、LegalName、TaxCodeなどのセッターがあります。 _requisitionsフィールドなど、その他は含まれません。既存の求人を再構築する直接的な方法はありません。 _requisitionsを何らかの方法で(Listプロパティとして)公開すると、リポジトリで次のように簡単に実行できます。
var requisitions = ... // get requisitions for this customer
var customer = new Customer(id);
customer.Requisitions.AddRange(requisitions);
しかし、他の場所でできること:
customer.Requisitions.Add(new Requisition());
openRequisitionメソッドで設定されたロジックと検証をバイパスします。一方、リポジトリからOpenRequisitionメソッドを呼び出すと、イベントハンドラーは毎回呼び出されますが、実際には新しい要求を開かずにデータベースからオブジェクトを再構築しているため、呼び出されません。
これまでのところ、コンストラクタを作成してから、これらのすべてのパラメータを次のように渡します。
public Customer(IDomainEvents events, Guid id, IEnumerable<Requisition> requisitions, ... and so on)
{
... // perform validations
_events = events;
_id = id;
_requisitions = new List<Requisition>(requisitions);
... // and so on
}
ただし、この集約には他にいくつかの子ノードがあるため、コンストラクターはどんどん成長しており、正しくありません。ある時点でファクトリクラスを作成しようとしましたが、大きなコンストラクター、または_eventsと_requisitionsに対してさえ内部セッターを持つようなものが必要ですが、それでもまだ正しくないようです。
したがって、私の質問は、複雑な集約ルートを再構築する方法、集約が機能するために必要なすべてのパラメーターを渡す方法です。それ以外の場合、このようなシナリオではどのようなオプションを検討しますか?
これは、集計を実装する際の一般的な問題です。通常は、集計状態を別のオブジェクトに分離します。そのオブジェクトは、便宜上、パブリックゲッター/セッターを持つ純粋なデータバケットにすることができます。外部の呼び出し元は集約の状態クラスを認識できますが、集約に対してプライベートなままです(たとえば、プライベートフィールドとして)。リポジトリーは、集約の状態を直接ロードできます。これは、単一のオブジェクトとして集約のコンストラクターに渡すことができます。
あなたは:
public CustomerState
{
public Guid Id { get; set; }
public string ShortName { get; set; }
public string LegalName { get; set; }
public TaxCode TaxCode { get; set; }
public ContactInfo PrimaryContact { get; set; }
public List<Requisitions> Requisitions { get; set; }
}
public Customer
{
private readonly CustomerState _state;
private readonly IDomainEvents _events;
public Customer(CustomerState state, IDomainEvents events)
{
_state = state;
_events = events;
}
}
...
public Requisition OpenRequisition(RequisitionDefinition definition, User requestedBy)
{
Requisition req = ... // create the new requisition somehow
_state.Requisitions.Add(req);
_events.Invoke(new RequisitionCreatedEvent(this, req));
return req;
}
IDomainEvents
を状態クラスではなく集約に配置したことに注意してください。これは、いくつかの動作(ディスパッチの可能性)があるためです。状態オブジェクトについては、純粋なデータのみが必要です。
非常に表面的には、これは組織的に貧血ドメインモデルに似ています。これは、状態が分離されており、集合体には基本的に、ファサードパターンに類似したメソッドのみが含まれているためです。ただし、貧血ドメインにより、ユーザーは状態を直接操作し、その操作でトランザクションスクリプトを実行できるという点が異なります。しかし、これにより、ユーザーは状態を直接操作することができなくなります...ドメインメソッドを介して検証済みの状態変更の制御を保持しますが、ロード(および保存)を容易にします。
さて、このように国家を分離することは、それ自身の課題を提示します。通常、名前の混乱は最初は混乱しています。データテーブルにCustomerStateという名前を付けたくないでしょう(そして既にCustomerという名前になっているでしょう)。ただし、テーブルにCustomerという名前を付けると、同じ名前の集計が構造的に異なる場合に混乱が生じます。再編成の準備ができている場合は、総売上や注文などに名前を付けることを検討し(それが扱うエリアに基づいて)、顧客の名前をCustomerのままにしておきます。とにかく、それがユースケースの組織をより明確にすることにつながるかもしれません。
値オブジェクトを保存するクラスと、各値オブジェクトをロードするクラスを作成します。ここでの値オブジェクトはプリミティブのみを意味します。また、検証して不変性を試すことで、スレッドロックが不要になります(不変の場合、データデータの競合は発生しません)。 Builderパターンが役立つ場合があります。
次に、Fooという名前のagretesに対して、永続的なFooクラスを作成します。最終的にFooのすべての値オブジェクトを保持し、ビルダーを持っています。 Fooクラスには、PersistとPersistの2つの静的メソッドがあります。これらは単なる変換メソッドですが、失敗することはありません。
次に、そのための保存クラスとロードクラスを追加します。その時点で委任され、保存してロードする必要があります。
最後のヒントとして、保存と読み込みは時々パラメータを受け取りますが、状態はありません。