OOPコミュニティでは、クラスコンストラクターがオブジェクトを部分的または完全に初期化されていない状態にしてはならないという合意が広まっています。
「初期化」とはどういう意味ですか?大まかに言えば、新しく作成されたオブジェクトを次の状態にするatomicプロセスクラスのすべての不変条件が成立します。これはオブジェクトに最初に発生するものであり(オブジェクトごとに1回だけ実行する必要があります)、初期化されていないオブジェクトを取得することは許可されません。 (したがって、クラスコンストラクターでオブジェクトの初期化を正しく実行するための頻繁なアドバイス。同じ理由で、
Initialize
メソッドはしばしばアトミック性を分解し、把握して使用できるようになるため、不快になります。まだ明確な状態になっていないオブジェクト)
Problem: CQRSがイベントソース(CQRS + ES)と組み合わされている場合、オブジェクトのすべての状態変化が順序付けられた一連のイベント(イベントストリーム)でキャッチされるので、オブジェクトが実際にいつ発生するのか疑問に思います完全に初期化された状態に達する:クラスコンストラクターの最後、または最初のイベントがオブジェクトに適用された後?
注:「集約ルート」という用語の使用は控えています。必要に応じて、「オブジェクト」を読むたびに置き換えてください。
説明の例:各オブジェクトは、不透明なId
値(GUIDと考える)によって一意に識別されると想定します。オブジェクトの状態変化を表すイベントストリームは、同じId
値によってイベントストアで識別できます(正しいイベントの順序については気にしないでください)。
_interface IEventStore
{
IEnumerable<IEvent> GetEventsOfObject(Id objectId);
}
_
さらに、2つのオブジェクトタイプCustomer
とShoppingCart
があると仮定します。 ShoppingCart
に焦点を当てましょう:作成されたとき、ショッピングカートは空であり、正確に1人の顧客に関連付けられている必要があります。その最後のビットはクラス不変です。ShoppingCart
に関連付けられていないCustomer
オブジェクトは無効な状態です。
従来のOOPでは、コンストラクターでこれをモデル化できます。
_partial class ShoppingCart
{
public Id Id { get; private set; }
public Customer Customer { get; private set; }
public ShoppingCart(Id id, Customer customer)
{
this.Id = id;
this.Customer = customer;
}
}
_
しかし、遅延初期化を行わずにCQRS + ESでこれをモデル化する方法に困っています。この単純な初期化は事実上状態の変化なので、イベントとしてモデル化する必要はありませんか?
_partial class CreatedEmptyShoppingCart
{
public ShoppingCartId { get; private set; }
public CustomerId { get; private set; }
}
// Note: `ShoppingCartId` is not actually required, since that Id must be
// known in advance in order to fetch the event stream from the event store.
_
これは明らかにShoppingCart
オブジェクトのイベントストリームの最初のイベントである必要があり、そのオブジェクトはイベントが適用された後にのみ初期化されます。
したがって、初期化がイベントストリームの「再生」の一部になる場合(これは、Customer
オブジェクトまたはShoppingCart
オブジェクトまたはその他のオブジェクトタイプに関係なく、同じように機能する非常に一般的なプロセスですそのことについては)…
void Apply(CreatedEmptyShoppingCart)
メソッドに任せます(これは、眉をひそめたInitialize()
とほとんど同じです)?void Apply(…)
- iedされていますか?完全に機能するデモ実装を提供するための回答は期待していません。誰かが私の推論のどこに欠陥があるのか、またはオブジェクトの初期化が本当にであるかどうかを説明できたら、私はすでに非常に満足していますESの実装。
CQRS + ESを実行するとき、私はパブリックコンストラクターをまったく使用しないことを好みます。集計ルートの作成は、ファクトリ(これのような単純な構成の場合)またはビルダー(より複雑な集計ルートの場合)を介して行う必要があります。
オブジェクトを実際に初期化する方法は、実装の詳細です。 OOP "Do n't use initialize" -advice is imho about public interfaces。あなたのコードを使用する誰もが、SecretInitializeMethod42( bool、int、string)-これは悪いパブリックAPI設計です。ただし、クラスがパブリックコンストラクタを提供せず、代わりにメソッドCreateNewShoppingCart(string)を備えたShoppingCartFactoryがある場合、そのファクトリの実装はあらゆる種類の初期化を十分に隠す可能性があります。ユーザーが知る必要のない/ constructor magic(したがって、NiceパブリックAPIを提供しますが、舞台裏でより高度なオブジェクト作成を行うことができます)。
ファクトリーの数が多すぎると思っている人からは悪い反応を示しますが、正しく使用すれば、わかりやすいNiceパブリックAPIの背後にある多くの複雑さを隠すことができます。それらを使用することを恐れないでください。これらは、複雑なオブジェクトの構築をはるかに簡単にするのに役立つ強力なツールです。
最小限のコード行で誰が問題を解決できるかを見極めるのは競争ではありませんが、誰が最も優れたパブリックAPIを作成できるかについては、現在も競争が続いています。 ;)
編集:これらのパターンの適用がどのように見えるかに関するいくつかの例を追加します
いくつかの必須パラメーターを持つ「簡単な」集約コンストラクターがある場合は、非常に基本的なファクトリー実装(これらの行に沿ったもの)を使用できます。
public class FooAggregate {
internal FooAggregate() { }
public int A { get; private set; }
public int B { get; private set; }
internal Handle(FooCreatedEvent evt) {
this.A = a;
this.B = b;
}
}
public class FooFactory {
public FooAggregate Create(int a, int b) {
var evt = new FooCreatedEvent(a, b);
var result = new FooAggregate();
result.Handle(evt);
DomainEvents.Register(result, evt);
return result;
}
}
もちろん、FooCreatedEventの作成を分割する方法は、この場合はあなた次第です。また、FooAggregate(FooCreatedEvent)コンストラクターがある場合や、イベントを作成するFooAggregate(int、int)コンストラクターがある場合もあります。ここで責任を分割することをどのように選択するかは、最もクリーンであると考えるものと、ドメインイベント登録をどのように実装したかによって異なります。多くの場合、ファクトリーにイベントを作成させることを選択します。ただし、イベントの作成は内部実装の詳細であるため、外部インターフェースを変更せずにいつでも変更およびリファクタリングできるため、それはあなた次第です。ここでの重要な詳細は、集約にパブリックコンストラクターがなく、すべてのセッターがプライベートであることです。あなたは誰もがそれらを外部で使用することを望んでいません。
このパターンは、コンストラクタを多少なりとも置き換えている場合は問題なく機能しますが、より高度なオブジェクト構築がある場合、これは複雑すぎて使用できなくなる可能性があります。この場合、私は通常、ファクトリパターンを無視して、代わりにビルダーパターンに切り替えます-多くの場合、より流暢な構文を使用します。
作成するクラスはそれほど複雑ではないため、この例は少し強制されていますが、うまくいけば、アイデアを理解して、より複雑な構築タスクをどのように容易にするかを確認できます。
public class FooBuilder {
private int a;
private int b;
public FooBuilder WithA(int a) {
this.a = a;
return this;
}
public FooBuilder WithB(int b) {
this.b = b;
return this;
}
public FooAggregate Build() {
if(!someChecksThatWeHaveAllState()) {
throw new OmgException();
}
// Some hairy logic on how to create a FooAggregate and the creation events from our state
var foo = new FooAggregate(....);
foo.PlentyOfHairyInitialization(...);
DomainEvents.Register(....);
return foo;
}
}
そして、あなたはそれを
var foo = new FooBuilder().WithA(1).Build();
そしてもちろん、一般的にビルダーパターンを見ると、2つの整数だけではなく、いくつかの値オブジェクトのリストや、他の毛深いものの辞書が含まれている可能性があります。ただし、オプションのパラメーターの組み合わせが多い場合にも非常に役立ちます。
この方法で重要なことは、次のとおりです。
それがもう少し役立つことを願って、それ以外の場合はコメントで説明を求めてください:)
私の意見では、答えは既存の集計に対する提案#2に近いと思います。 #3に似た新しい集計の場合(ただし、提案どおりにイベントを処理します)。
ここにいくつかのコードがあります、それが何らかの助けになることを願っています。
public abstract class Aggregate
{
Dictionary<Type, Delegate> _handlers = new Dictionary<Type, Delegate>();
protected Aggregate(long version = 0)
{
this.Version = version;
}
public long Version { get; private set; }
protected void Handles<TEvent>(Action<TEvent> action)
where TEvent : IDomainEvent
{
this._handlers[typeof(TEvent)] = action;
}
private IList<IDomainEvent> _pendingEvents = new List<IDomainEvent>();
// Apply a new event, and add to pending events to be committed to event store
// when transaction completes
protected void Apply(IDomainEvent @event)
{
this.Invoke(@event);
this._pendingEvents.Add(@event);
}
// Invoke handler to change state of aggregate in response to event
// Event may be an old event from the event store, or may be an event triggered
// during the lifetime of this instance.
protected void Invoke(IDomainEvent @event)
{
Delegate handler;
if (this._handlers.TryGetValue(@event.GetType(), out handler))
((Action<TEvent>)handler)(@event);
}
}
public class ShoppingCart : Aggregate
{
private Guid _id, _customerId;
private ShoppingCart(long version = 0)
: base(version)
{
// Setup handlers for events
Handles<ShoppingCartCreated>(OnShoppingCartCreated);
// Handles<ItemAddedToShoppingCart>(OnItemAddedToShoppingCart);
// etc...
}
public ShoppingCart(long version, IEnumerable<IDomainEvent> events)
: this(version)
{
// Replay existing events to get current state
foreach (var @event in events)
this.Invoke(@event);
}
public ShoppingCart(Guid id, Guid customerId)
: this()
{
// Process new event, changing state and storing event as pending event
// to be saved when aggregate is committed.
this.Apply(new ShoppingCartCreated(id, customerId));
}
private void OnShoppingCartCreated(ShoppingCartCreated @event)
{
this._id = @event.Id;
this._customerId = @event.CustomerId;
}
}
public class ShoppingCartCreated : IDomainEvent
{
public ShoppingCartCreated(Guid id, Guid customerId)
{
this.Id = id;
this.CustomerId = customerId;
}
public Guid Id { get; private set; }
public Guid CustomerID { get; private set; }
}
さて、最初のイベントは顧客がショッピングカートを作成するであるべきです。そのため、ショッピングカートが作成されたとき、イベントの一部としてすでに顧客IDを持っています。
2つの異なるイベント間で状態が保持されている場合、定義上、それは有効な状態です。したがって、有効なショッピングカートが顧客に関連付けられていると言う場合、これはカート自体を作成するときに顧客情報が必要であることを意味します
注:集約ルートを使用したくない場合、「エンティティ」は、トランザクション境界に関する懸念を回避しながら、ここで尋ねているほとんどのことを網羅します。
これについての別の考え方があります。エンティティはアイデンティティ+状態です。値とは異なり、エンティティは状態が変わっても同じ人です。
しかし、状態自体は値オブジェクトと考えることができます。つまり、状態は不変です。エンティティの履歴は、1つの不変状態から次の状態への遷移です。各遷移は、イベントストリーム内のイベントに対応しています。
State nextState = currentState.onEvent(e);
もちろん、onEvent()メソッドはクエリです-currentStateはまったく変更されず、代わりにcurrentStateがnextStateの作成に使用される引数を計算します。
このモデルに従って、ショッピングカートのすべてのインスタンスは、同じシード値から始まると考えることができます。
State currentState = ShoppingCart.SEED;
for (Event e : history) {
currentState = currentState.onEvent(e);
}
ShoppingCart cart = new ShoppingCart(id, currentState);
懸念の分離-ShoppingCartはコマンドを処理して、次のイベントを特定します。 ShoppingCart州は次の州への行き方を知っています。