Dove dovrebbe essere completamente inizializzato un oggetto in CQRS + ES: nel costruttore, o quando si applica il primo evento?

9

Sembra esserci un accordo diffuso nella comunità OOP sul fatto che il costruttore della classe non dovrebbe lasciare un oggetto in parte o addirittura completamente non inizializzato.

What do I mean by "initialization"? Roughly speaking, the atomic process that brings a newly created object into a state where all of its class invariants hold. It should be the first thing that happens to an object, (it should only run once per object,) and nothing should be permitted to get hold of an un-initialized object. (Thus the frequent advice to perform object initialization right in the class constructor. For the same reason, Initialize methods are often frowned upon, as these break apart the atomicity and make it possible to get hold of, and use, an object that is not yet in a well-defined state.)

Problema: Quando CQRS è combinato con il sourcing di eventi (CQRS + ES), in cui tutte le modifiche di stato di un oggetto vengono catturate in una serie ordinata di eventi (flusso di eventi), mi chiedo quando un oggetto raggiunge effettivamente uno stato completamente inizializzato: alla fine del costruttore della classe, o dopo che il primo evento è stato applicato all'oggetto?

Note: I'm refraining from using the term "aggregate root". If you prefer, substitute it whenever you read "object".

Esempio di discussione: Supponiamo che ogni oggetto sia identificato in modo univoco da un valore opaco di Id (pensa GUID). Un flusso di eventi che rappresenta le modifiche di stato di quell'oggetto può essere identificato nello store eventi con lo stesso valore di Id : (Non preoccupiamoci dell'ordine di evento corretto.)

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

Supponiamo inoltre che ci siano due tipi di oggetto Customer e ShoppingCart . Concentriamoci su ShoppingCart : una volta creati, i carrelli degli acquisti sono vuoti e devono essere associati esattamente a un cliente. L'ultimo bit è un invariante di classe: un oggetto ShoppingCart non associato a Customer è in uno stato non valido.

Nel tradizionale OOP, si potrebbe modellare questo nel costruttore:

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;
    }
}

Tuttavia non riesco a capire come modellare questo in CQRS + ES senza finire con l'inizializzazione posticipata. Poiché questo semplice bit di inizializzazione è effettivamente un cambiamento di stato, non dovrebbe essere modellato come un evento?:

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.

Questo dovrebbe ovviamente essere il primo evento in qualsiasi flusso di eventi di ShoppingCart dell'oggetto, e quell'oggetto sarebbe inizializzato solo una volta che l'evento è stato applicato ad esso.

Quindi se l'inizializzazione diventa parte del flusso di eventi "playback" (che è un processo molto generico che probabilmente funzionerebbe allo stesso modo, sia per un oggetto Customer o un ShoppingCart o qualsiasi altro tipo di oggetto per quella materia ) ...

  • Il costruttore deve essere parametrico e non fare nulla, lasciando tutto il lavoro a un metodo void Apply(CreatedEmptyShoppingCart) (che è molto simile al Initialize() accigliato)?
  • Oppure il costruttore dovrebbe ricevere un flusso di eventi e riprodurlo (il che rende di nuovo l'inizializzazione atomica, ma significa che ogni costruttore di classi contiene la stessa logica generica di "riproduzione e applicazione", ovvero la duplicazione indesiderata del codice)?
  • O dovrebbe esserci sia un costruttore OOP tradizionale (come mostrato sopra) che inizializza correttamente l'oggetto, sia quindi tutti gli eventi ma il primo è void Apply(…) -ed ad esso?

Non mi aspetto che la risposta fornisca un'implementazione demo pienamente funzionante; Sarei già molto felice se qualcuno potesse spiegare dove il mio ragionamento è difettoso, o se l'inizializzazione dell'oggetto è un "punto di dolore" nella maggior parte delle implementazioni di CQRS + ES.

    
posta stakx 13.01.2014 - 22:58
fonte

4 risposte

3

Quando faccio CQRS + ES preferisco non avere costruttori pubblici. La creazione delle mie radici aggregate dovrebbe essere effettuata tramite una factory (per costruzioni abbastanza semplici come questa) o un builder (per radici aggregate più complicate).

Come inizializzare effettivamente l'oggetto è un dettaglio di implementazione. L'OOP "Non utilizzare l'inizializzazione" -ad esempio è imho su interfacce pubbliche . Non dovresti aspettarti che chiunque usi il tuo codice sappia che devono chiamare SecretInitializeMethod42 (bool, int, string) - questa è una cattiva progettazione dell'API pubblica. Tuttavia se la tua classe non fornisce alcun costruttore pubblico ma invece c'è un ShoppingCartFactory con il metodo CreateNewShoppingCart (string), allora l'implementazione di quella factory potrebbe nascondere qualsiasi tipo di magia di inizializzazione / costruttore che l'utente non ha bisogno di sapere circa (fornendo quindi una buona API pubblica, ma consentendoti di fare la creazione di oggetti più avanzati dietro le quinte).

Le fabbriche ricevono una cattiva reputazione da persone che pensano che ce ne siano troppe, ma usate correttamente possono nascondere molta complessità dietro una bella API pubblica facile da capire. Non aver paura di usarli, sono uno strumento potente che può aiutarti a semplificare la costruzione di oggetti complessi, a patto che tu possa vivere con più linee di codice.

Non è una gara vedere chi può risolvere il problema con le meno linee di codice - è comunque una competizione in corso su chi può creare le API pubbliche più belle! ;)

Modifica: aggiungendo alcuni esempi su come l'applicazione di questi modelli potrebbe sembrare

Se hai solo un costruttore di aggregati "facile" che ha un paio di parametri obbligatori puoi seguire solo un'implementazione di fabbrica molto semplice, qualcosa del genere

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;
    }
}

Ovviamente, esattamente come dividi la creazione di FooCreatedEvent è in questo caso a te. Si potrebbe anche creare un caso per avere un costruttore FooAggregate (FooCreatedEvent) o un costruttore FooAggregate (int, int) che crea l'evento. Esattamente come si sceglie di dividere la responsabilità, qui dipende da cosa si pensa sia la più pulita e da come è stata implementata la registrazione degli eventi di dominio. Spesso scelgo di far creare la fabbrica all'evento, ma dipende da te dato che la creazione di eventi è ora un dettaglio di implementazione interno che puoi modificare e refactoring in qualsiasi momento senza modificare l'interfaccia esterna. Un dettaglio importante qui è che l'aggregato non ha un costruttore pubblico e che tutti i setter sono privati. Non vuoi che qualcuno li usi esternamente.

Questo modello funziona bene quando si sostituiscono più o meno i costruttori, ma se si dispone di una struttura più avanzata degli oggetti questo può diventare troppo complesso da usare. In questo caso, di solito rinuncio al modello factory e si rivolge invece a un pattern builder, spesso con una sintassi più fluente.

Questo esempio è un po 'forzato dal momento che la classe che costruisce non è molto complessa, ma puoi fiduciosamente cogliere l'idea e vedere come faciliterebbe le attività di costruzione più complesse

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;
    }
}

E poi lo usi come

var foo = new FooBuilder().WithA(1).Build();

E, naturalmente, in genere quando torno al modello di builder non sono solo due inte, può contenere liste di alcuni oggetti di valore o dizionari di qualche tipo forse di altre cose più pelose. È comunque anche molto utile se hai molte combinazioni di parametri opzionali.

L'importante consiglio per andare in questo modo è:

  • Il tuo obiettivo principale è la possibilità di astrarre la costruzione di un oggetto in modo che l'utente esterno non debba conoscere il tuo sistema di eventi.
  • Non è così importante dove o chi registra l'evento di creazione, la parte importante è che viene registrato e che è possibile garantire che, a parte questo, è un dettaglio di implementazione interna. Fai quello che si adatta meglio al tuo codice, non seguire il mio esempio come una sorta di "questo è il modo giusto" -thing.
  • Se lo desideri, andando in questo modo, le tue fabbriche / repository possono restituire interfacce invece di classi concrete, semplificando così i test unitari!
  • Questo è un sacco di codice extra a volte, il che rende molte persone evitabili. Ma è spesso un codice abbastanza semplice rispetto alle alternative e fornisce un valore quando si fa prima o poi a dover cambiare qualcosa. C'è una ragione per cui Eric Evans parla di fabbriche / repository nel suo DDD-book come parti importanti della DDD - sono necessarie astrazioni per non far trapelare determinati dettagli di implementazione all'utente. E le astrazioni che perdono sono cattive.

Spero che questo aiuti un po 'di più, basta chiedere chiarimenti nei commenti altrimenti:)

    
risposta data 10.11.2014 - 13:37
fonte
3

Secondo me, penso che la risposta sia più simile al tuo suggerimento n. 2 per gli aggregati esistenti; per i nuovi aggregati più come # 3 (ma elaborando l'evento come suggerito).

Ecco un po 'di codice, spero che sia di qualche aiuto.

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; }
}
    
risposta data 15.05.2014 - 15:19
fonte
0

Bene, il primo evento dovrebbe essere che un cliente crea un carrello , quindi quando viene creato il carrello acquisti hai già un ID cliente come parte dell'evento.

Se uno stato contiene tra due eventi diversi, per definizione è uno stato valido. Quindi se dici che un carrello della spesa valido è associato a un cliente, significa che devi avere le informazioni sul cliente quando crei il carrello stesso

    
risposta data 15.05.2014 - 18:06
fonte
0

Nota: se non si desidera utilizzare la radice aggregata, "entità" racchiude la maggior parte di ciò che si sta chiedendo in proposito mentre si affrontano le preoccupazioni sui limiti delle transazioni.

Ecco un altro modo di pensarci: un'entità è identità + stato. A differenza di un valore, un'entità è la stessa persona anche quando il suo stato cambia.

Ma lo stato stesso può essere considerato come un oggetto valore. Con questo intendo, lo stato è immutabile; la storia dell'entità è una transizione da uno stato immutabile a quello successivo - ogni transizione corrisponde a un evento nel flusso di eventi.

State nextState = currentState.onEvent(e);

Il metodo onEvent () è una query, ovviamente - currentState non è affatto cambiato, invece currentState sta calcolando gli argomenti usati per creare il nextState.

Seguendo questo modello, tutte le istanze del carrello acquisti possono essere pensate come a partire dallo stesso valore seme ....

State currentState = ShoppingCart.SEED;
for (Event e : history) {
    currentState = currentState.onEvent(e);
}

ShoppingCart cart = new ShoppingCart(id, currentState);

Separazione delle preoccupazioni: la ShoppingCart elabora un comando per capire quale evento dovrebbe essere il prossimo; lo ShoppingCart State sa come arrivare allo stato successivo.

    
risposta data 12.01.2016 - 06:42
fonte

Leggi altre domande sui tag