web-dev-qa-db-ja.com

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

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

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

Problem: CQRSがイベントソース(CQRS + ES)と組み合わされている場合、オブジェクトのすべての状態変化が順序付けられた一連のイベント(イベントストリーム)でキャッチされるので、オブジェクトが実際にいつ発生するのか疑問に思います完全に初期化された状態に達する:クラスコンストラクターの最後、または最初のイベントがオブジェクトに適用された後?

注:「集約ルート」という用語の使用は控えています。必要に応じて、「オブジェクト」を読むたびに置き換えてください。

説明の例:各オブジェクトは、不透明なId値(GUIDと考える)によって一意に識別されると想定します。オブジェクトの状態変化を表すイベントストリームは、同じId値によってイベントストアで識別できます(正しいイベントの順序については気にしないでください)。

_interface IEventStore
{
    IEnumerable<IEvent> GetEventsOfObject(Id objectId); 
}
_

さらに、2つのオブジェクトタイプCustomerShoppingCartがあると仮定します。 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されていますか?

完全に機能するデモ実装を提供するための回答は期待していません。誰かが私の推論のどこに欠陥があるのか​​、またはオブジェクトの初期化が本当にであるかどうかを説明できたら、私はすでに非常に満足していますESの実装。

9
stakx

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つの整数だけではなく、いくつかの値オブジェクトのリストや、他の毛深いものの辞書が含まれている可能性があります。ただし、オプションのパラメーターの組み合わせが多い場合にも非常に役立ちます。

この方法で重要なことは、次のとおりです。

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

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

3
wasatz

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

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

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

0
Andrea

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

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

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

0
VoiceOfUnreason