CQRS + ES'deki bir nesne tam olarak nerede başlatılmalıdır: kurucuda veya ilk olayı uygularken?


9

OOP topluluğunda, sınıf oluşturucunun bir nesneyi kısmen veya tamamen başlatılmamış bırakması konusunda yaygın bir anlaşma olduğu görülmektedir.

"Başlatma" ile ne demek istiyorum? Kabaca söylemek gerekirse, yeni oluşturulan bir nesneyi tüm sınıf değişmezlerinin sahip olduğu bir duruma getiren atomik süreç. Bir nesnede gerçekleşen ilk şey olmalı (her nesne için yalnızca bir kez çalıştırılmalıdır) ve başlatılmamış bir nesneyi ele geçirmesine izin verilmemelidir. (Bu nedenle, sınıf yapıcısında nesne başlatma işlemini gerçekleştirmek için sık sık tavsiye edilir. Aynı nedenden ötürü, Initializebunlar genellikle atomikliği parçaladığı ve henüz olmayan bir nesneyi elde etmeyi ve kullanmayı mümkün kıldığından , yöntemler genellikle kaşlarını çatar. iyi tanımlanmış bir durumda.)

Sorun: CQRS, bir nesnenin tüm durum değişikliklerinin sıralı bir olay serisine (olay akışı) yakalandığı olay kaynağı (CQRS + ES) ile birleştirildiğinde, bir nesnenin gerçekten tamamen başlatılmış bir duruma ulaştığını merak ediyorum: Sınıf yapıcısının sonunda mı yoksa nesneye ilk olay uygulandıktan sonra mı?

Not: "Toplam kök" terimini kullanmaktan kaçınıyorum. İsterseniz, "nesne" yi her okuduğunuzda değiştirin.

Tartışma örneği: Her nesnenin bazı opak Iddeğerlerle benzersiz bir şekilde tanımlandığını varsayın (GUID düşünün). Bu nesnenin durum değişikliklerini temsil eden bir olay akışı, olay deposunda aynı Iddeğerle tanımlanabilir: (Doğru olay sırası konusunda endişelenmeyelim.)

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

Ayrıca iki nesne türü olduğunu Customerve varsayalım ShoppingCart. Şunlara odaklanalım ShoppingCart: Oluşturulduğunda, alışveriş sepetleri boştur ve tam olarak bir müşteriyle ilişkilendirilmelidir. Bu son bit bir sınıf değişmezidir: a ile ShoppingCartilişkili olmayan bir nesne Customergeçersiz bir durumda.

Geleneksel OOP'de, bunu yapıcıda modelleyebilir:

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

Ancak ertelenmiş başlatma ile bitmeden bu CQRS + ES modelleme kaybı var. Bu basit başlatma biti etkin bir şekilde devlet değişikliği olduğundan, bir olay olarak modellenmesi gerekmez mi ?:

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.

Bunun, herhangi bir ShoppingCartnesnenin olay akışındaki ilk olay olması gerektiği açıktır ve bu nesne yalnızca olay kendisine uygulandıktan sonra başlatılır.

Bu yüzden başlatma, olay akışının "playback" (bir Customernesne veya ShoppingCartnesne veya bu konu için herhangi bir nesne türü için aynı çalışacak çok genel bir işlem) parçası haline gelirse ...

  • Yapıcı parametresiz olmalı ve hiçbir şey yapmamalı, tüm işleri bir void Apply(CreatedEmptyShoppingCart)yönteme bırakmalıdır (kaşlarını çatmış olanla aynıdır Initialize())?
  • Ya da kurucu bir olay akışı almalı ve oynatmalı mı (yeniden başlatma atomunu tekrar yapar, ama her sınıfın yapıcısının aynı genel "oynat ve uygula" mantığını, yani istenmeyen kod çoğaltmasını içerdiği anlamına gelir)?
  • Düzgün nesnesini başlatır ki (yukarıda gösterildiği gibi) ve Ya da her ikisi geleneksel cepten yapıcı olmalıdır sonra tüm olayları ancak ilk olan void Apply(…)kendisine -ied?

Tam çalışan bir demo uygulaması sağlamak için cevap beklemiyorum; Birisi benim akıl kusurlu veya nesne başlatma gerçekten olup olmadığını nerede açıklayabilir ben zaten çok mutlu olurum olan en CQRS + ES uygulamalarda bir "acı noktası".

Yanıtlar:


3

CQRS + ES yaparken kamu kurucuları olmamayı tercih ederim. Agrega köklerimi oluşturmak bir fabrika (bunun gibi yeterince basit yapılar için) veya bir yapıcı (daha karmaşık agrega kökleri için) aracılığıyla yapılmalıdır.

Nesnenin gerçekte nasıl başlatılacağı bir uygulama detayıdır. OOP "Başlatma kullanma" - acemi kamu arayüzleri hakkında imho . Kodunuzu kullanan hiç kimsenin SecretInitializeMethod42 (bool, int, string) çağırması gerektiğini bilmesini beklememelisiniz - bu kötü bir genel API tasarımıdır. Ancak sınıfınız herhangi bir ortak kurucu sağlamaz ancak bunun yerine CreateNewShoppingCart (string) yöntemiyle bir ShoppingCartFactory varsa, o fabrikanın uygulanması, kullanıcının daha sonra bilmemesi gereken her türlü başlatma / kurucu sihrini çok iyi gizleyebilir. hakkında (böylece güzel bir genel API sağlayın, ancak sahnelerin arkasında daha gelişmiş nesne oluşturmanıza izin verin).

Fabrikalar, çok fazla olduğunu düşünen insanlardan kötü bir destek alır, ancak doğru kullanıldıklarında, anlaşılması kolay bir genel API'nin arkasındaki çok fazla karmaşıklığı gizleyebilirler. Bunları kullanmaktan korkmayın, karmaşık nesne yapısını çok daha kolay hale getirmenize yardımcı olabilecek güçlü bir araçtır - daha fazla kod satırıyla yaşayabildiğiniz sürece.

Sorunu en az kod satırıyla kimin çözebileceğini görmek için bir yarış değil - ancak en iyi ortak API'ları kimin yapabileceği konusunda devam eden bir rekabet! ;)

Düzenleme: Bu kalıpların uygulanmasının nasıl görünebileceğine ilişkin bazı örnekler ekleme

Sadece birkaç basit parametreye sahip "kolay" bir agrega kurucunuz varsa, sadece çok basit bir fabrika uygulamasıyla gidebilirsiniz, bu hatlar boyunca bir şey

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

Tabii ki, tam olarak nasıl FooCreatedEvent oluşturmak için bölmek bu durumda size kalmış. Bir FooAggregate (FooCreatedEvent) yapıcısına veya olayı oluşturan bir FooAggregate (int, int) yapıcısına sahip olmak için de bir durum ortaya çıkabilir. Sorumluluğu tam olarak nasıl bölmeyi seçtiğiniz en temiz olduğunu düşündüğünüz şey ve alan adı etkinliği kaydınızı nasıl uyguladığınıza bağlıdır. Sık sık fabrikanın etkinliği yaratmasını tercih ederim - ancak olay oluşturma artık harici arayüzünüzü değiştirmeden istediğiniz zaman değiştirebileceğiniz ve yeniden düzenleyebileceğiniz bir dahili uygulama detayıdır. Burada önemli bir ayrıntı, agreganın bir kamu kurucuya sahip olmaması ve tüm pasörlerin özel olmasıdır. Kimsenin bunları harici olarak kullanmasını istemezsiniz.

Bu desen, az çok yapıcıları değiştirdiğinizde iyi çalışır, ancak daha gelişmiş nesne yapınız varsa, bu kullanım için çok karmaşık hale gelebilir. Bu durumda genellikle fabrika modelinden vazgeçerim ve bunun yerine daha güçlü bir sözdizimiyle bir oluşturucu desenine dönüyorum.

Bu örnek, oluşturduğu sınıf çok karmaşık olmadığı için biraz zorlandı, ancak umarım fikri kavrayabilir ve daha karmaşık inşaat görevlerini nasıl kolaylaştıracağını görebilirsiniz.

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

Ve sonra onu

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

Ve elbette, genellikle oluşturucu desenine döndüğümde sadece iki inç değil, bazı değer nesnelerinin veya bir çeşit belki de daha kıllı şeylerin sözlüklerinin listesini içerebilir. Bununla birlikte, birçok isteğe bağlı parametre kombinasyonunuz varsa da oldukça kullanışlıdır.

Bu yoldan gitmek için önemli çıkarımlar:

  • Ana hedefiniz, dış kullanıcının olay sisteminizi bilmesine gerek kalmaması için nesne yapımını soyutlayabilmektir.
  • Yaratılış etkinliğini nereye ya da kimin kaydettiği o kadar da önemli değil, önemli olan bu etkinliğin kaydedilmesi ve bunun dışında bir iç uygulama detayı olduğunu garanti edebilmeniz. Kodunuza en uygun olanı yapın, örneğimi takip etmeyin, çünkü bir çeşit "bu doğru yoldur".
  • İsterseniz, bu yolla fabrikalarınızın / depolarınızın somut sınıflar yerine arayüzleri geri getirmesini sağlayabilirsiniz - ünite testleri için alay etmelerini kolaylaştırır!
  • Bu bazen bir sürü ekstra koddur, bu da birçok insanı ondan uzak tutar. Ancak, alternatiflere kıyasla genellikle oldukça kolay bir koddur ve er ya da geç bir şeyleri değiştirmeniz gerektiğinde değer sağlar. Eric Evans DDD önemli parçaları olarak onun DDD-kitapta fabrikalar / depoları bahsettiği bir sebebi var - onlar gerekli kullanıcıya belirli uygulama ayrıntılarını sızdırmaz için soyutlamalar. Ve sızdıran soyutlamalar kötü.

Umarım biraz daha yardımcı olur, aksi takdirde yorumlarda açıklama isteyin :)


+1, fabrikaları asla olası bir tasarım çözümü olarak düşünmedim. Yine de bir şey var: Sanki fabrikalar genel arayüzde inşaatçıları (+ belki de bir Initializeyöntemi) işgal edecekleri aynı yeri alıyor gibi görünüyor . Bu beni şu soruya götürüyor, böyle bir fabrikanız neye benzeyebilir?
stakx

3

Benim düşünceme göre, cevabın daha çok mevcut kümeler için öneri # 2'ye benzediğini düşünüyorum; # 3 gibi yeni toplamalar için (ancak etkinliği önerdiğiniz gibi işleyerek).

İşte bazı kod, umarım biraz yardımcı olur.

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

İlk olay, bir müşterinin bir alışveriş sepeti oluşturması olmalıdır , bu nedenle alışveriş sepeti oluşturulduğunda etkinliğin bir parçası olarak zaten bir müşteri kimliğiniz vardır.

Bir durum iki farklı olay arasında duruyorsa, tanımı gereği geçerli bir durumdur. Dolayısıyla, geçerli bir alışveriş sepetinin bir müşteriyle ilişkilendirildiğini söylerseniz, bu, alışveriş sepetinin kendisini oluştururken müşteri bilgisine sahip olmanız gerektiği anlamına gelir


Sorum, etki alanının nasıl modelleneceği ile ilgili değil; Bir soru sormak için kasten basit (ama belki de kusurlu) bir etki alanı modeli kullanıyorum. Sorumdaki CreatedEmptyShoppingCartetkinliğe bir göz atın : Müşteri önerileri, tıpkı önerdiğiniz gibi. Benim sorum daha çok böyle bir olayın ShoppingCartuygulama sırasında sınıf kurucusuyla nasıl ilişkili olduğu veya onunla yarıştığı hakkındadır .
stakx

1
Soruyu daha iyi anlıyorum. Yani, doğru cevap üçüncü cevap olmalıdır. Varlıklarınızın her zaman geçerli bir durumda olması gerektiğini düşünüyorum. Bu, bir alışveriş sepetinin müşteri kimliğine sahip olmasını gerektiriyorsa, bu, oluşturma zamanında sağlanmalıdır, bu nedenle müşteri kimliğini kabul eden bir kurucuya ihtiyaç duyulmaktadır. Etki alanı nesnelerinin değişmez olan ve tüm olası parametre kombinasyonları için yapıcılara sahip olan vaka sınıflarıyla temsil edileceği Scala'daki CQRS'ye daha alışkınım, bu yüzden bunu verdim
Andrea

0

Not: birleştirilmiş kök kullanmak istemiyorsanız, "varlık" işlem sınırlarıyla ilgili kaygıları azaltırken burada sorduğunuz şeylerin çoğunu kapsar.

İşte bunu düşünmenin başka bir yolu: varlık kimlik + durumdur. Bir değerden farklı olarak, bir varlık, durumu değişse bile aynı adamdır.

Fakat devletin kendisi bir değer nesnesi olarak düşünülebilir. Bununla demek istediğim, devlet değişmez; işletmenin geçmişi değişmez bir durumdan diğerine geçiştir - her geçiş olay akışındaki bir olaya karşılık gelir.

State nextState = currentState.onEvent(e);

OnEvent () yöntemi bir sorgudur, elbette - currentState hiç değişmez, bunun yerine currentState, nextState'i oluşturmak için kullanılan bağımsız değişkenleri hesaplar.

Bu modeli takiben, Alışveriş sepetinin tüm örnekleri aynı tohum değerinden başlayarak düşünülebilir.

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

ShoppingCart cart = new ShoppingCart(id, currentState);

Endişelerin ayrılması - ShoppingCart, bir sonraki etkinliğin ne olacağını belirlemek için bir komut işler; ShoppingCart Eyaleti bir sonraki duruma nasıl gidileceğini bilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.