Di mana objek dalam CQRS + ES harus sepenuhnya diinisialisasi: di konstruktor, atau ketika menerapkan acara pertama?


9

Tampaknya ada kesepakatan luas dalam komunitas OOP bahwa konstruktor kelas tidak boleh meninggalkan objek sebagian atau bahkan sepenuhnya diinisialisasi.

Apa yang saya maksud dengan "inisialisasi"? Secara kasar, proses atom yang membawa objek yang baru dibuat ke dalam keadaan di mana semua kelas invariannya bertahan. Ini harus menjadi hal pertama yang terjadi pada suatu objek, (seharusnya hanya berjalan sekali per objek,) dan tidak ada yang diizinkan untuk mendapatkan objek yang tidak diinisialisasi. (Demikianlah saran yang sering dilakukan untuk melakukan inisialisasi objek tepat di konstruktor kelas. Untuk alasan yang sama, Initializemetode sering disukai, karena ini memecah atomicity dan memungkinkan untuk mendapatkan, dan menggunakan, objek yang belum dalam kondisi terdefinisi dengan baik.)

Masalah: Ketika CQRS dikombinasikan dengan event sourcing (CQRS + ES), di mana semua perubahan keadaan suatu objek terperangkap dalam serangkaian peristiwa yang terurut (aliran acara), saya bertanya-tanya kapan suatu objek benar-benar mencapai kondisi diinisialisasi penuh: Di akhir konstruktor kelas, atau setelah peristiwa pertama diterapkan pada objek?

Catatan: Saya menahan diri untuk tidak menggunakan istilah "root agregat". Jika Anda suka, gantilah setiap kali Anda membaca "objek".

Contoh untuk diskusi: Asumsikan bahwa setiap objek diidentifikasi secara unik oleh beberapa Idnilai buram (pikirkan GUID). Aliran peristiwa yang mewakili perubahan keadaan objek itu dapat diidentifikasi di toko peristiwa dengan nilai yang sama Id: (Jangan khawatir tentang urutan peristiwa yang benar.)

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

Asumsikan lebih lanjut bahwa ada dua jenis objek Customerdan ShoppingCart. Mari kita fokus pada ShoppingCart: Ketika dibuat, kereta belanja kosong dan harus dikaitkan dengan tepat satu pelanggan. Bit terakhir itu adalah kelas invarian: ShoppingCartObjek yang tidak terkait dengan a Customerdalam keadaan tidak valid.

Dalam OOP tradisional, orang mungkin memodelkan ini dalam konstruktor:

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

Namun saya bingung bagaimana memodelkan ini dalam CQRS + ES tanpa berakhir dengan inisialisasi ditangguhkan. Karena inisialisasi sederhana ini secara efektif merupakan perubahan keadaan, bukankah harus dimodelkan sebagai suatu peristiwa ?:

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.

Ini jelas harus menjadi peristiwa pertama dalam ShoppingCartaliran peristiwa objek apa pun , dan objek itu hanya akan diinisialisasi setelah acara diterapkan.

Jadi jika inisialisasi menjadi bagian dari "pemutaran" aliran acara (yang merupakan proses yang sangat umum yang kemungkinan akan bekerja sama, baik untuk Customerobjek atau ShoppingCartobjek atau jenis objek lainnya dalam hal ini) ...

  • Haruskah konstruktor menjadi parameter-kurang dan tidak melakukan apa-apa, membiarkan semua pekerjaan untuk beberapa void Apply(CreatedEmptyShoppingCart)metode (yang hampir sama dengan yang disukai Initialize())?
  • Atau haruskah konstruktor menerima aliran peristiwa dan memutarnya (yang membuat inisialisasi atom lagi, tetapi berarti konstruktor masing-masing kelas berisi logika generik yang sama "putar & terapkan", yaitu duplikasi kode yang tidak diinginkan)?
  • Atau haruskah ada konstruktor OOP tradisional (seperti yang ditunjukkan di atas) yang menginisialisasi objek dengan benar, dan kemudian semua peristiwa tetapi yang pertama void Apply(…)terkait dengannya?

Saya tidak mengharapkan jawaban untuk menyediakan implementasi demo yang berfungsi penuh; Saya sudah sangat senang jika seseorang bisa menjelaskan di mana alasan saya cacat, atau apakah inisialisasi objek benar - benar merupakan "titik nyeri" di sebagian besar implementasi CQRS + ES.

Jawaban:


3

Ketika melakukan CQRS + ES saya lebih suka tidak memiliki konstruktor publik sama sekali. Membuat akar agregat saya harus dilakukan melalui pabrik (untuk konstruksi yang cukup sederhana seperti ini) atau pembangun (untuk akar agregat yang lebih rumit).

Cara kemudian menginisialisasi objek sebenarnya adalah detail implementasi. OOP "Don't use inisialisasi" -adalah saran tentang antarmuka publik . Anda tidak boleh berharap bahwa siapa pun yang menggunakan kode Anda tahu bahwa mereka harus memanggil SecretInitializeMethod42 (bool, int, string) - itu desain API publik yang buruk. Namun jika kelas Anda tidak menyediakan konstruktor publik tetapi sebaliknya ada ShoppingCartFactory dengan metode CreateNewShoppingCart (string) maka implementasi pabrik itu mungkin sangat menyembunyikan segala jenis inisialisasi / konstruktor sihir yang pengguna Anda kemudian tidak perlu tahu about (dengan demikian menyediakan API publik yang bagus, tetapi memungkinkan Anda untuk melakukan pembuatan objek yang lebih canggih di balik layar).

Pabrik mendapatkan reputasi buruk dari orang-orang yang berpikir ada terlalu banyak, tetapi jika digunakan dengan benar mereka dapat menyembunyikan banyak kerumitan di balik API publik yang mudah dimengerti. Jangan takut untuk menggunakannya, mereka adalah alat yang ampuh yang dapat membantu Anda membuat konstruksi objek yang kompleks menjadi lebih mudah - selama Anda dapat hidup dengan beberapa baris kode lagi.

Ini bukan perlombaan untuk melihat siapa yang dapat menyelesaikan masalah dengan baris kode paling sedikit - namun ini merupakan kompetisi yang berkelanjutan untuk siapa yang dapat membuat API publik terbaik! ;)

Sunting: Menambahkan beberapa contoh tentang bagaimana penerapan pola ini dapat terlihat

Jika Anda hanya memiliki konstruktor agregat "mudah" yang memiliki beberapa parameter yang diperlukan, Anda dapat menggunakan hanya dengan implementasi pabrik yang sangat mendasar, sesuatu di sepanjang garis ini

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

Tentu saja, persis bagaimana Anda membagi menciptakan FooCreatedEvent dalam hal ini terserah Anda. Seseorang juga dapat membuat kasus untuk memiliki konstruktor FooAggregate (FooCreatedEvent), atau memiliki konstruktor FooAggregate (int, int) yang menciptakan acara. Tepatnya bagaimana Anda memilih untuk membagi tanggung jawab di sini tergantung pada apa yang Anda anggap paling bersih dan bagaimana Anda telah menerapkan pendaftaran acara domain Anda. Saya sering memilih untuk membuat pabrik membuat acara - tetapi terserah Anda karena pembuatan acara sekarang merupakan detail implementasi internal yang dapat Anda ubah dan ubah setiap saat tanpa mengubah antarmuka eksternal Anda. Detail penting di sini adalah bahwa agregat tidak memiliki konstruktor publik dan bahwa semua setter bersifat pribadi. Anda tidak ingin siapa pun menggunakannya secara eksternal.

Pola ini berfungsi dengan baik ketika Anda hanya kurang lebih mengganti konstruktor, tetapi jika Anda memiliki konstruksi objek yang lebih maju ini mungkin menjadi terlalu rumit untuk digunakan. Dalam hal ini saya biasanya melepaskan pola pabrik dan beralih ke pola pembangun - sering dengan sintaks yang lebih lancar.

Contoh ini agak dipaksakan karena kelas yang dibangunnya tidak terlalu kompleks, tetapi Anda dapat dengan mudah memahami gagasan itu dan melihat bagaimana hal itu akan memudahkan tugas konstruksi yang lebih rumit

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

Dan kemudian Anda menggunakannya seperti

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

Dan tentu saja, secara umum ketika saya beralih ke pola pembangun itu bukan hanya dua int, itu mungkin berisi daftar beberapa objek bernilai atau kamus dari beberapa jenis mungkin beberapa hal yang lebih berbulu lainnya. Namun ini juga cukup berguna jika Anda memiliki banyak kombinasi parameter opsional.

Hal penting yang harus diambil untuk menuju ke arah ini adalah:

  • Tujuan utama Anda adalah dapat membuat abstrak objek konstruksi sehingga pengguna eksternal tidak perlu tahu tentang sistem acara Anda.
  • Tidak penting di mana atau siapa yang mendaftarkan acara pembuatan, bagian yang penting adalah bahwa itu akan terdaftar dan bahwa Anda dapat menjamin bahwa - selain dari itu, ini merupakan detail implementasi internal. Lakukan yang paling sesuai dengan kode Anda, jangan ikuti contoh saya karena semacam "ini jalan yang benar" -sesuatu.
  • Jika Anda mau, dengan cara ini Anda bisa meminta pabrik / repositori Anda mengembalikan antarmuka alih-alih kelas konkret - membuatnya lebih mudah untuk dipermainkan untuk unit test!
  • Ini terkadang banyak kode tambahan, yang membuat banyak orang menghindar darinya. Tetapi sering kali ini adalah kode yang cukup mudah dibandingkan dengan alternatif dan itu memang memberikan nilai ketika Anda cepat atau lambat perlu mengubah hal-hal. Ada alasan bahwa Eric Evans berbicara tentang pabrik / repositori dalam bukunya DDD sebagai bagian penting dari DDD - mereka adalah abstraksi yang diperlukan untuk tidak membocorkan detail implementasi tertentu kepada pengguna. Dan abstraksi bocor itu buruk.

Semoga itu bisa membantu lebih banyak, cukup minta klarifikasi dalam komentar jika tidak :)


+1, saya bahkan tidak pernah menganggap pabrik sebagai solusi desain yang memungkinkan. Namun satu hal: Kedengarannya seolah-olah pabrik pada dasarnya mengambil tempat yang sama di antarmuka publik yang Initializedihuni oleh konstruktor agregat (+ mungkin suatu metode). Itu mengarahkan saya ke pertanyaan, seperti apa pabrik Anda nantinya?
stakx

3

Menurut pendapat saya, saya pikir jawabannya lebih seperti saran Anda # 2 untuk agregat yang ada; untuk agregat baru lebih seperti # 3 (tetapi memproses acara seperti yang Anda sarankan).

Ini beberapa kode, semoga bisa membantu.

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

Yah, acara pertama harus bahwa pelanggan membuat keranjang belanja , jadi ketika keranjang belanja dibuat Anda sudah memiliki id pelanggan sebagai bagian dari acara tersebut.

Jika suatu keadaan berlaku di antara dua peristiwa yang berbeda, menurut definisi itu adalah keadaan yang valid. Jadi, jika Anda mengatakan bahwa keranjang belanja yang valid dikaitkan dengan pelanggan, ini berarti Anda harus memiliki info pelanggan saat membuat kereta itu sendiri


Pertanyaan saya bukan tentang bagaimana memodelkan domain; Saya sengaja menggunakan model domain yang sederhana (tapi mungkin tidak sempurna) demi mengajukan pertanyaan. Lihatlah CreatedEmptyShoppingCartacara di pertanyaan saya: Ini memiliki informasi pelanggan, seperti yang Anda sarankan. Pertanyaan saya lebih tentang bagaimana peristiwa semacam itu berhubungan dengan, atau bersaing dengan, konstruktor kelas ShoppingCartselama implementasi.
stakx

1
Saya lebih mengerti pertanyaannya. Jadi, saya akan mengatakan, jawaban yang benar harus yang ketiga. Saya pikir entitas Anda harus selalu dalam keadaan valid. Jika ini mengharuskan kereta belanja memiliki id pelanggan, maka ini harus disediakan pada waktu pembuatan, maka kebutuhan konstruktor yang menerima id pelanggan. Saya lebih terbiasa dengan CQRS di Scala, di mana objek domain akan diwakili oleh kelas kasus, yang tidak berubah dan memiliki konstruktor untuk semua kemungkinan kombinasi parameter, jadi saya memberikan ini begitu saja
Andrea

0

Catatan: jika Anda tidak ingin menggunakan root agregat, "entitas" mencakup sebagian besar dari apa yang Anda tanyakan di sini sementara sisi melangkah kekhawatiran tentang batas-batas transaksi.

Inilah cara lain untuk memikirkannya: entitas adalah identitas + negara. Tidak seperti nilai, entitas adalah orang yang sama bahkan ketika kondisinya berubah.

Tetapi negara itu sendiri dapat dianggap sebagai objek nilai. Maksud saya, negara tidak dapat diubah; sejarah entitas adalah transisi dari satu keadaan tidak berubah ke yang berikutnya - setiap transisi berhubungan dengan suatu peristiwa dalam aliran peristiwa.

State nextState = currentState.onEvent(e);

Metode onEvent () adalah query, tentu saja - currentState tidak berubah sama sekali, sebaliknya currentState menghitung argumen yang digunakan untuk membuat nextState.

Mengikuti model ini, semua contoh Keranjang Belanja dapat dianggap sebagai mulai dari nilai benih yang sama ....

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

ShoppingCart cart = new ShoppingCart(id, currentState);

Pemisahan masalah - ShoppingCart memproses perintah untuk mencari tahu acara apa yang harus terjadi selanjutnya; Negara ShoppingCart tahu cara menuju ke negara bagian berikutnya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.