CQRS + ESのオブジェクトはどこで完全に初期化する必要がありますか:コンストラクター内、または最初のイベントを適用するとき?


9

OOPコミュニティでは、クラスコンストラクターがオブジェクトを部分的または完全に未初期化のままにしてはならないという合意が広まっているようです。

「初期化」とはどういう意味ですか?大まかに言えば、新しく作成されたオブジェクトを、そのクラスのすべての不変条件が保持される状態にするアトミックプロセス。これはオブジェクトに最初に発生するものであり(オブジェクトごとに1回だけ実行する必要があります)、初期化されていないオブジェクトを取得することは許可されません。(したがって、クラスコンストラクターでオブジェクトの初期化を正しく実行するための頻繁なアドバイスです。同じ理由で、Initializeメソッドはしばしばアトミック性を分解し、まだ使用されていないオブジェクトを取得して使用することを可能にするため、眉をひそめられます明確な状態にあります。)

問題: 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()です)?
  • または、コンストラクターはイベントストリームを受け取って再生する必要があります(これにより、初期化が再びアトミックになりますが、各クラスのコンストラクターに同じ一般的な「再生と適用」ロジック、つまり不要なコードの重複が含まれることを意味します)?
  • または、オブジェクトを適切に初期化する従来のOOPコンストラクター(上記を参照)と、最初のイベント除くすべてのイベントの両方にvoid Apply(…)-iedする必要がありますか?

完全に機能するデモ実装を提供するための回答は期待していません。私の推論は欠陥があるところ、誰かが説明できる場合、私はすでに非常に幸せになるだろう、またはオブジェクトの初期化は本当にいるかどうかである最もCQRS + ESの実装における「痛みのポイント」。

回答:


3

CQRS + ESを実行するとき、私はパブリックコンストラクターをまったく使用しないことを好みます。集計ルートの作成は、ファクトリ(これのような単純な構成の場合)またはビルダー(より複雑な集計ルートの場合)を介して行う必要があります。

オブジェクトを実際に初期化する方法は、実装の詳細です。OOPの「初期化を使用しない」というアドバイスは、パブリックインターフェースについては無意味です。コードを使用するユーザーが、SecretInitializeMethod42(bool、int、string)を呼び出さなければならないことを知っていると期待してはいけません。これは、パブリックAPIの設計としては不適切です。ただし、クラスがパブリックコンストラクターを提供せず、代わりにメソッドCreateNewShoppingCart(string)を備えたShoppingCartFactoryがある場合、そのファクトリーの実装は、ユーザーが知る必要のないあらゆる種類の初期化/コンストラクターマジックを非常によく隠す可能性があります。約(したがって、優れたパブリックAPIを提供しますが、舞台裏でより高度なオブジェクト作成を行うことができます)。

ファクトリーの数が多すぎると思っている人からは工場の悪い担当者がいますが、正しく使用すれば、わかりやすいパブリック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つのintだけではなく、いくつかの値オブジェクトのリストや、他の多分毛深いものの辞書が含まれている可能性があります。ただし、オプションのパラメーターの組み合わせが多い場合にも非常に役立ちます。

この方法で重要なポイントは次のとおりです。

  • あなたの主な目的は、外部ユーザーがあなたのイベントシステムについて知る必要がないようにオブジェクト構築を抽象化できることです。
  • 作成イベントを登録する場所や登録者はそれほど重要ではありませんが、重要な部分は、登録されることと、それを保証できることです。それとは別に、それは内部実装の詳細です。コードに最も適合するものを実行します。ある種の「これは正しい方法です」ということなので、私の例には従わないでください。
  • 必要に応じて、この方法で、ファクトリー/リポジトリーに具象クラスの代わりにインターフェースを返すようにすることができます-ユニットテスト用にモックするのが簡単になります!
  • これは、多くの余分なコードである場合があり、多くの人々はそれをためらいます。しかし、他の方法に比べてコードは非常に簡単であることが多く、後で変更する必要が生じたときに価値を提供します。Eric EvansがDDDブックでDDDの重要な部分としてファクトリー/リポジトリーについて話すのには理由があります。これらは、特定の実装の詳細をユーザーに漏らさないようにするために必要な抽象化です。そして、漏れやすい抽象化は悪いです。

それがもう少し役立つことを願って、それ以外の場合はコメントで説明を求めてください:)


+1、私は工場を考えられる設計ソリューションとさえ考えていませんでした。ただし、パブリックインターフェースで、ファクトリーが集約コンストラクター(おそらくInitializeメソッド)と同じ場所を占めているように思えます。それは私に質問を導きます、あなたのそのような工場はどのように見えるでしょうか?
stakx 2014年

3

私の意見では、答えは既存の集計に対する提案#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; }
}

0

最初のイベントは、顧客がショッピングカートを作成することです。そのため、ショッピングカートが作成されたとき、イベントの一部として顧客IDがすでにあります。

2つの異なるイベント間で状態が保持されている場合、定義上、それは有効な状態です。したがって、有効なショッピングカートが顧客に関連付けられていると言う場合、これはカート自体を作成するときに顧客情報が必要であることを意味します


私の質問はドメインをモデル化する方法についてではありません。私は質問をするために、単純な(しかしおそらく不完全な)ドメインモデルを意図的に使用しています。CreatedEmptyShoppingCart私の質問のイベントを見てください。あなたが提案したように、それは顧客情報を持っています。私の質問は、そのようなイベントがShoppingCart実装時のクラスコンストラクターにどのように関連するか、またはどのように競合するかについてです。
stakx 2014

1
私はその質問をよく理解しています。ですから、正解は3番目の答えでなければなりません。エンティティは常に有効な状態である必要があると思います。ショッピングカートに顧客IDが必要な場合は、作成時に提供する必要があるため、顧客IDを受け入れるコンストラクタが必要です。私はScalaのCQRSに慣れています。ドメインオブジェクトは、不変で、パラメーターのすべての可能な組み合わせのコンストラクターを持つケースクラスによって表されるため、これを当然のこととして提供しました
Andrea

0

注:集約ルートを使用したくない場合、「エンティティ」には、トランザクション境界に関する懸念を回避しながら、ここで質問する内容のほとんどが含まれます。

これについての別の考え方があります。エンティティはアイデンティティ+状態です。値とは異なり、エンティティは状態が変化しても同じ人です。

しかし、状態自体は値オブジェクトと考えることができます。つまり、状態は不変です。エンティティの履歴は、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州は次の州への行き方を知っています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.