Peristiwa domain: Desain dan implementasi

Tip

Konten ini adalah kutipan dari eBook, .NET Microservices Architecture for Containerized .NET Applications, tersedia di .NET Docs atau sebagai PDF yang dapat diunduh gratis dan dapat dibaca secara offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Gunakan peristiwa domain untuk secara eksplisit menerapkan efek samping dari perubahan dalam domain Anda. Dengan kata lain, dan menggunakan terminologi DDD, gunakan peristiwa domain untuk secara eksplisit menerapkan efek samping di beberapa agregat. Secara opsional, untuk skalabilitas yang lebih baik dan dampak yang lebih kecil pada kunci database, gunakan konsistensi akhir antara agregat dalam domain yang sama.

Apa itu peristiwa domain?

Peristiwa adalah sesuatu yang telah terjadi di masa lampau. Peristiwa domain adalah sesuatu yang terjadi di domain agar diperhatikan oleh bagian lain dari domain yang sama (dalam proses). Bagian yang diberi tahu biasanya dapat bereaksi terhadap peristiwa.

Keuntungan penting dari peristiwa domain adalah bahwa efek samping dapat diekspresikan secara eksplisit.

Misalnya, jika Anda hanya menggunakan Entity Framework dan harus ada reaksi terhadap beberapa peristiwa, Anda mungkin akan mengodekan apa pun yang Anda perlukan dekat dengan apa yang memicu peristiwa tersebut. Sehingga aturan digabungkan secara implisit ke kode dan Anda harus melihat ke dalam kode, dengan harapan, Anda menyadari aturan diterapkan di sana.

Di sisi lain, menggunakan peristiwa domain membuat konsep menjadi eksplisit, karena terdapat DomainEvent dan setidaknya satu DomainEventHandler dilibatkan.

Misalnya, dalam aplikasi eShop, ketika pesanan dibuat, pengguna menjadi pembeli, sehingga dinaikkan OrderStartedDomainEvent dan ditangani dalam ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler, sehingga konsep yang mendasar terbukti.

Singkatnya, peristiwa domain membantu Anda untuk mengekspresikan, secara eksplisit, aturan domain, berdasarkan bahasa yang ada di mana-mana yang disediakan oleh ahli domain. Peristiwa domain juga memungkinkan pemisahan yang lebih baik antara kelas-kelas dalam domain yang sama.

Penting untuk memastikan bahwa, seperti halnya transaksi database, semua operasi yang terkait dengan peristiwa domain berhasil diselesaikan atau tidak ada yang berhasil.

Peristiwa domain mirip dengan peristiwa dengan gaya olahpesan, dengan satu perbedaan penting. Dengan olahpesan nyata, pengantrean pesan, broker pesan, atau bus layanan menggunakan AMQP, pesan selalu dikirim secara asinkron dan dikomunikasikan di seluruh proses dan mesin. Ini berguna untuk mengintegrasikan beberapa Konteks yang Dibatasi, layanan mikro, atau bahkan aplikasi yang berbeda. Namun, dengan peristiwa domain, Anda ingin memunculkan peristiwa dari operasi domain yang sedang Anda jalankan, tetapi Anda ingin efek samping apa pun terjadi dalam domain yang sama.

Peristiwa domain dan efek sampingnya (tindakan yang dipicu setelahnya yang dikelola oleh penanganan aktivitas) akan terjadi segera, biasanya dalam proses, dan dalam domain yang sama. Dengan demikian, peristiwa domain dapat menjadi sinkron atau asinkron. Tetapi, peristiwa integrasi harus selalu asinkron.

Peristiwa domain versus peristiwa integrasi

Secara semantik, domain dan peristiwa integrasi adalah hal yang sama: pemberitahuan tentang sesuatu yang baru saja terjadi. Tetapi, penerapan domain dan peristiwa integrasi harus berbeda. Peristiwa domain hanya merupakan pesan yang didorong ke pengirim peristiwa domain, yang dapat diterapkan sebagai mediator dalam memori berdasarkan kontainer IoC atau metode lainnya.

Di sisi lain, tujuan dari peristiwa integrasi adalah untuk menyebarkan transaksi dan pembaruan yang diterapkan ke subsistem tambahan, baik peristiwa integrasi layanan mikro lain, Konteks yang Dibatasi atau bahkan aplikasi eksternal. Oleh karena itu, peristiwa integrasi harus terjadi hanya jika entitas berhasil dipertahankan, jika tidak, maka seluruh operasi seperti tidak pernah terjadi.

Seperti disebutkan sebelumnya, peristiwa integrasi harus didasarkan pada komunikasi asinkron antara beberapa layanan mikro (Konteks yang Dibatasi lainnya) atau bahkan sistem/aplikasi eksternal.

Oleh karena itu, antarmuka bus peristiwa memerlukan beberapa infrastruktur yang memungkinkan komunikasi antar proses dan terdistribusi antara layanan yang mungkin berupa jarak jauh. Hal ini dapat didasarkan pada bus layanan komersial, antrean, database bersama yang digunakan sebagai kotak surat, atau sistem olahpesan berbasis pendorongan terdistribusi dan ideal lainnya.

Peristiwa domain sebagai cara yang diinginkan untuk memicu efek samping di beberapa agregat dalam domain yang sama

Jika menjalankan perintah yang terkait dengan satu instans agregat memerlukan aturan domain tambahan untuk dijalankan pada satu atau beberapa agregat tambahan, Anda harus merancang dan menerapkan efek samping tersebut untuk dipicu oleh peristiwa domain. Seperti yang ditunjukkan pada Gambar 7-14, dan sebagai salah satu kasus penggunaan yang paling penting, peristiwa domain harus digunakan untuk menyebarkan perubahan status di beberapa agregat dalam model domain yang sama.

Diagram showing a domain event controlling data to a Buyer aggregate.

Gambar 7-14. Peristiwa domain untuk menerapkan konsistensi antara beberapa agregat dalam domain yang sama

Gambar 7-14 menunjukkan bagaimana konsistensi antara agregat dicapai oleh peristiwa domain. Saat pengguna memulai pesanan, Agregat Pesanan mengirim peristiwa domain OrderStarted. Peristiwa domain OrderStarted ditangani oleh Agregat Pembeli untuk membuat objek Pembeli di layanan mikro pemesanan, berdasarkan info pengguna asli dari layanan mikro identitas (dengan informasi yang disediakan dalam perintah CreateOrder).

Sebagai alternatif, Anda dapat membuat akar agregat berlangganan untuk peristiwa yang dimunculkan oleh anggota agregatnya (entitas turunan). Misalnya, setiap entitas turunan OrderItem dapat memunculkan peristiwa saat harga item lebih tinggi dari jumlah tertentu, atau saat jumlah item produk terlalu tinggi. Akar agregat kemudian dapat menerima peristiwa tersebut dan melakukan penghitungan atau agregasi global.

Penting untuk dipahami bahwa komunikasi berbasis peristiwa ini tidak diterapkan secara langsung dalam agregat; Anda perlu menerapkan penanganan aktivitas domain.

Menangani peristiwa domain berkaitan dengan aplikasi. Lapisan model domain seharusnya hanya fokus pada logika domain—hal-hal yang akan dipahami oleh ahli domain, bukan infrastruktur aplikasi seperti penangan dan tindakan persistensi efek samping menggunakan repositori. Oleh karena itu, tingkat lapisan aplikasi adalah tempat Anda harus memiliki penanganan aktivitas domain yang memicu tindakan saat peristiwa domain dimunculkan.

Peristiwa domain juga dapat digunakan untuk memicu sejumlah tindakan aplikasi, dan yang lebih penting, harus terbuka untuk meningkatkan jumlah tindakan aplikasi di masa mendatang dengan cara yang terpisah. Misalnya, ketika pesanan dimulai, Anda mungkin ingin menerbitkan peristiwa domain untuk menyebarkan info tersebut ke agregat lain atau bahkan untuk memunculkan tindakan aplikasi seperti pemberitahuan.

Poin utamanya adalah jumlah tindakan terbuka yang akan dijalankan ketika peristiwa domain terjadi. Pada akhirnya, tindakan dan aturan di domain serta aplikasi akan bertambah. Kompleksitas atau jumlah tindakan efek samping ketika sesuatu terjadi akan bertambah, tetapi jika kode Anda digabungkan dengan "lem" (yaitu, membuat objek tertentu dengan new ), maka setiap kali Anda perlu menambahkan tindakan baru, Anda akan juga perlu mengubah kode yang berfungsi dan menguji kode.

Perubahan ini dapat mengakibatkan bug baru dan pendekatan ini juga bertentangan dengan prinsip Open/Closed dari SOLID. Tidak hanya itu, kelas asli yang mengatur operasi akan terus berkembang, yang bertentangan dengan Prinsip Tanggung Jawab Tunggal (SRP).

Di sisi lain, jika Anda menggunakan peristiwa domain, Anda dapat membuat penerapan kecil dan terpisah dengan memisahkan tanggung jawab menggunakan pendekatan ini:

  1. Kirim perintah (misalnya, CreateOrder).
  2. Terima perintah di penangan perintah.
    • Jalankan transaksi agregat tunggal.
    • (Opsional) Munculkan peristiwa domain untuk efek samping (misalnya, OrderStartedDomainEvent).
  3. Tangani peristiwa domain (dalam proses saat ini) yang akan mengeksekusi sejumlah efek samping terbuka dalam beberapa agregat atau tindakan aplikasi. Misalnya:
    • Verifikasi atau buat pembeli dan metode pembayaran.
    • Buat dan kirim peristiwa integrasi terkait ke bus peristiwa untuk menyebarkan status di seluruh layanan mikro atau memicu tindakan eksternal seperti mengirim email ke pembeli.
    • Tangani efek samping lainnya.

Seperti yang ditunjukkan pada Gambar 7-15, mulai dari peristiwa domain yang sama, Anda dapat menangani beberapa tindakan yang terkait dengan agregat lain di domain atau tindakan aplikasi tambahan yang perlu Anda lakukan di seluruh layanan mikro yang terhubung dengan peristiwa integrasi dan bus peristiwa.

Diagram showing a domain event passing data to several event handlers.

Gambar 7-15. Menangani beberapa tindakan per domain

Mungkin ada beberapa penangan untuk peristiwa domain yang sama di Lapisan Aplikasi, satu penangan dapat menyelesaikan konsistensi antara agregat, dan penangan lain dapat menerbitkan peristiwa integrasi, sehingga layanan mikro lain dapat melakukan sesuatu terhadap peristiwa integrasi. Penanganan aktivitas biasanya berada di lapisan aplikasi, karena Anda akan menggunakan objek infrastruktur seperti repositori atau API aplikasi untuk perilaku layanan mikro. Oleh karena itu, penanganan aktivitas mirip dengan penangan perintah, jadi keduanya merupakan bagian dari lapisan aplikasi. Perbedaan penting adalah bahwa perintah harus diproses hanya sekali. Peristiwa domain dapat diproses nol atau n kali, karena dapat diterima oleh banyak penerima atau penanganan aktivitas dengan tujuan berbeda untuk setiap penangan.

Memiliki jumlah penangan terbuka per peristiwa domain memungkinkan Anda menambahkan aturan domain sebanyak yang diperlukan, tanpa memengaruhi kode saat ini. Misalnya, menerapkan aturan bisnis berikut mungkin semudah menambahkan beberapa penanganan aktivitas (atau bahkan hanya satu penanganan aktivitas):

Ketika jumlah total yang dibeli oleh pelanggan di toko, di semua jumlah pesanan, melebihi $6.000, terapkan diskon 10% untuk setiap pesanan baru dan beri tahu pelanggan dengan email tentang diskon tersebut untuk pesanan di masa mendatang.

Menerapkan peristiwa domain

Dalam C#, peristiwa domain hanya merupakan struktur atau kelas penyimpanan data, seperti DTO, dengan semua informasi yang terkait dengan apa yang baru saja terjadi di domain, seperti yang ditunjukkan pada contoh berikut:

public class OrderStartedDomainEvent : INotification
{
    public string UserId { get; }
    public string UserName { get; }
    public int CardTypeId { get; }
    public string CardNumber { get; }
    public string CardSecurityNumber { get; }
    public string CardHolderName { get; }
    public DateTime CardExpiration { get; }
    public Order Order { get; }

    public OrderStartedDomainEvent(Order order, string userId, string userName,
                                   int cardTypeId, string cardNumber,
                                   string cardSecurityNumber, string cardHolderName,
                                   DateTime cardExpiration)
    {
        Order = order;
        UserId = userId;
        UserName = userName;
        CardTypeId = cardTypeId;
        CardNumber = cardNumber;
        CardSecurityNumber = cardSecurityNumber;
        CardHolderName = cardHolderName;
        CardExpiration = cardExpiration;
    }
}

Ini pada dasarnya merupakan kelas yang menyimpan semua data yang terkait dengan peristiwa OrderStarted.

Dalam hal bahasa domain yang ada di mana-mana, karena peristiwa adalah sesuatu yang terjadi di masa lalu, nama kelas dari peristiwa tersebut harus direpresentasikan sebagai kata kerja bentuk lampau, seperti OrderStartedDomainEvent atau OrderShippedDomainEvent. Itulah bagaimana peristiwa domain diimplementasikan dalam pemesanan layanan mikro di eShop.

Seperti disebutkan sebelumnya, karakteristik penting dari peristiwa adalah bahwa karena peristiwa adalah sesuatu yang terjadi di masa lalu, peristiwa tidak boleh diubah. Oleh karena itu, peristiwa harus menjadi kelas yang tidak dapat diubah. Anda dapat melihat di kode sebelumnya bahwa properti berupa baca-saja. Tidak ada cara untuk memperbarui objek, Anda hanya dapat menetapkan nilai saat Anda membuat objek.

Penting untuk digarisbawahi di sini bahwa jika peristiwa domain ditangani secara asinkron, menggunakan antrean yang memerlukan serialisasi dan deserialisasi objek peristiwa, propertinya harus "private set", bukan baca-saja, sehingga pendeserialisasi akan dapat menetapkan nilai pada penghapusan antrean. Hal ini bukan merupakan masalah di layanan mikro Pemesanan, karena pub/sub peristiwa domain diterapkan secara sinkron menggunakan MediatR.

Memunculkan peristiwa domain

Pertanyaan selanjutnya adalah bagaimana cara memunculkan peristiwa domain sehingga mencapai penanganan aktivitas terkait. Anda dapat menggunakan beberapa pendekatan.

Udi Dahan awalnya mengusulkan (misalnya, di beberapa posting terkait, seperti Domain Events – Take 2 ) menggunakan kelas statik untuk mengelola dan memunculkan peristiwa. Ini mungkin termasuk kelas statik bernama DomainEvents yang akan segera memunculkan peristiwa domain saat dipanggil, menggunakan sintaks seperti DomainEvents.Raise(Event myEvent). Jimmy Bogard menulis posting blog (Strengthening your domain: Domain Events) yang menyarankan pendekatan serupa.

Tetapi, ketika kelas peristiwa domain berupa statik, peristiwa domain juga segera dikirim ke penangan. Hal ini membuat pengujian dan penelusuran kesalahan menjadi lebih sulit, karena penanganan aktivitas dengan logika efek samping dijalankan segera setelah peristiwa dimunculkan. Saat Anda menguji dan melakukan penelusuran kesalahan, Anda hanya perlu fokus pada apa yang terjadi di kelas agregat saat ini; Anda tidak ingin tiba-tiba dialihkan ke penanganan aktivitas lain untuk efek samping yang terkait dengan agregat lain atau logika aplikasi. Inilah sebabnya mengapa pendekatan lain telah berkembang, seperti yang dijelaskan di bagian selanjutnya.

Pendekatan yang ditangguhkan untuk memunculkan dan mengirim peristiwa

Daripada langsung mengirim ke penanganan aktivitas domain, pendekatan yang lebih baik adalah menambahkan peristiwa domain ke kumpulan, lalu mengirimkan peristiwa domain tersebut tepat sebelum atau tepat setelah melakukan transaksi (seperti halnya SaveChanges di EF). (Pendekatan ini dijelaskan oleh Jimmy Bogard di posting ini A better domain events pattern.)

Memutuskan apakah Anda mengirim peristiwa domain tepat sebelum atau setelah melakukan transaksi itu penting, karena hal ini menentukan apakah Anda akan menyertakan efek samping sebagai bagian dari transaksi yang sama atau dalam transaksi yang berbeda. Dalam kasus terakhir, Anda perlu berurusan dengan konsistensi akhir di beberapa agregat. Topik ini dibahas di bagian berikutnya.

Pendekatan yang ditangguhkan adalah apa yang digunakan eShop. Pertama, Anda menambahkan peristiwa yang terjadi di entitas Anda ke dalam kumpulan atau daftar peristiwa per entitas. Daftar tersebut harus menjadi bagian dari objek entitas, atau bahkan lebih baik, bagian dari kelas entitas dasar Anda, seperti yang ditunjukkan dalam contoh Kelas dasar entitas berikut:

public abstract class Entity
{
     //...
     private List<INotification> _domainEvents;
     public List<INotification> DomainEvents => _domainEvents;

     public void AddDomainEvent(INotification eventItem)
     {
         _domainEvents = _domainEvents ?? new List<INotification>();
         _domainEvents.Add(eventItem);
     }

     public void RemoveDomainEvent(INotification eventItem)
     {
         _domainEvents?.Remove(eventItem);
     }
     //... Additional code
}

Saat Anda ingin memunculkan peristiwa, Anda cukup menambahkan peristiwa ke kumpulan peristiwa dari kode di metode apa pun dari entitas akar agregat.

Kode berikut, bagian dari Order aggregate-root di eShop, menunjukkan contoh:

var orderStartedDomainEvent = new OrderStartedDomainEvent(this, //Order object
                                                          cardTypeId, cardNumber,
                                                          cardSecurityNumber,
                                                          cardHolderName,
                                                          cardExpiration);
this.AddDomainEvent(orderStartedDomainEvent);

Perhatikan bahwa satu-satunya hal yang dilakukan metode AddDomainEvent adalah menambahkan peristiwa ke daftar. Belum ada peristiwa yang dikirim, dan belum ada penanganan aktivitas yang dipanggil.

Anda sebenarnya perlu mengirimkan peristiwa nanti, saat Anda melakukan transaksi ke database. Jika Anda menggunakan Entity Framework Core, ini berarti dalam metode SaveChanges dari EF DbContext Anda, seperti pada kode berikut:

// EF Core DbContext
public class OrderingContext : DbContext, IUnitOfWork
{
    // ...
    public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        // Dispatch Domain Events collection.
        // Choices:
        // A) Right BEFORE committing data (EF SaveChanges) into the DB. This makes
        // a single transaction including side effects from the domain event
        // handlers that are using the same DbContext with Scope lifetime
        // B) Right AFTER committing data (EF SaveChanges) into the DB. This makes
        // multiple transactions. You will need to handle eventual consistency and
        // compensatory actions in case of failures.
        await _mediator.DispatchDomainEventsAsync(this);

        // After this line runs, all the changes (from the Command Handler and Domain
        // event handlers) performed through the DbContext will be committed
        var result = await base.SaveChangesAsync();
    }
}

Dengan kode ini, Anda mengirimkan peristiwa entitas ke penanganan aktivitas masing-masing peristiwa entitas.

Hasil keseluruhannya adalah Anda telah menghentikan proses memunculkan peristiwa domain (penambahan sederhana ke dalam daftar di memori) dari mengirimkan peristiwa domain ke penanganan aktivitas. Selain itu, bergantung pada jenis pengirim yang Anda gunakan, Anda dapat mengirimkan peristiwa secara sinkron atau asinkron.

Perlu diketahui bahwa batas-batas transaksional berperan penting di sini. Jika unit kerja dan transaksi Anda dapat menjangkau lebih dari satu agregat (seperti saat menggunakan EF Core dan database hubungan), hal ini akan berjalan lancar. Tetapi jika transaksi tidak dapat menjangkau agregat, Anda harus menerapkan langkah-langkah tambahan untuk mencapai konsistensi. Ini adalah alasan lain mengapa ketidaktahuan persistensi tidak universal; ketidaktahuan persistensi bergantung pada sistem penyimpanan yang Anda gunakan.

Transaksi tunggal di seluruh agregat versus konsistensi akhir di seluruh agregat

Pertanyaan tentang apakah akan melakukan satu transaksi di seluruh agregat versus mengandalkan konsistensi akhir di seluruh agregat tersebut adalah pertanyaan yang kontroversial. Banyak pembuat DDD seperti Eric Evans dan Vaughn Vernon menganjurkan aturan bahwa satu transaksi = satu agregat dan karenanya memperdebatkan konsistensi akhir di seluruh agregat. Misalnya, dalam bukunya Domain-Driven Design, Eric Evans mengatakan hal ini:

Aturan apa pun yang mencakup Agregat diharapkan tidak diperbarui setiap saat. Melalui pemrosesan peristiwa, pemrosesan batch, atau mekanisme pembaruan lainnya, dependensi lain dapat diselesaikan dalam waktu tertentu. (halaman 128)

Vaughn Vernon mengatakan yang berikut ini dalam Effective Aggregate Design. Part II: Making Aggregates Work Together:

Jadi, jika menjalankan perintah pada satu instans agregat mengharuskan aturan bisnis tambahan dijalankan pada satu atau lebih agregat, gunakan konsistensi akhir [...] Ada cara praktis untuk mendukung konsistensi akhir dalam model DDD. Metode agregat menerbitkan peristiwa domain yang dikirimkan tepat waktu ke satu atau beberapa pelanggan asinkron.

Dasar pemikiran ini didasarkan pada merangkul transaksi kecil, bukan transaksi yang mencakup banyak agregat atau entitas. Gagasannya adalah bahwa dalam kasus kedua, jumlah kunci database akan menjadi substansial dalam aplikasi skala besar dengan kebutuhan skalabilitas tinggi. Mempertimbangkan fakta bahwa aplikasi yang sangat scalable tidak perlu memiliki konsistensi transaksional instan antara beberapa agregat membantu dengan menerima konsep konsistensi akhir. Perubahan atomik sering tidak diperlukan oleh bisnis, dan merupakan tanggung jawab ahli domain untuk mengatakan apakah operasi tertentu memerlukan transaksi atomik atau tidak. Jika operasi selalu membutuhkan transaksi atomik di antara beberapa agregat, Anda mungkin bertanya apakah agregat Anda harus lebih besar atau tidak dirancang dengan benar.

Tetapi, pengembang dan arsitek lain seperti Jimmy Bogard setuju dengan mencakup satu transaksi di beberapa agregat—tetapi hanya jika agregat tambahan tersebut terkait dengan efek samping untuk perintah asli yang sama. Misalnya, dalam Pola peristiwa domain yang lebih baik, Bogard mengatakan hal ini:

Biasanya, saya ingin efek samping dari peristiwa domain terjadi dalam transaksi logis yang sama, tetapi tidak harus dalam cakupan yang sama untuk meningkatkan peristiwa domain [...] Tepat sebelum kita melakukan transaksi, kita mengirimkan peristiwa ke masing-masing penangan.

Jika Anda mengirimkan peristiwa domain tepat sebelum melakukan transaksi awal, Anda bertujuan agar efek samping dari peristiwa tersebut disertakan dalam transaksi yang sama. Misalnya, jika metode EF DbContext SaveChanges gagal, transaksi akan menggulung balik semua perubahan, termasuk hasil dari operasi efek samping yang diterapkan oleh penanganan aktivitas domain terkait. Ini karena cakupan masa pakai DbContext secara default ditentukan sebagai "tercakup". Oleh karena itu, objek DbContext dibagikan di beberapa objek repositori yang dibuat dalam cakupan atau grafik objek yang sama. Hal ini terjadi bersamaan dengan cakupan HttpRequest saat mengembangkan API Web atau aplikasi MVC.

Sebenarnya, kedua pendekatan (transaksi atomik tunggal dan konsistensi akhir) dapat menjadi benar. Ini sangat bergantung pada domain atau persyaratan bisnis Anda dan apa yang dikatakan ahli domain kepada Anda. Ini juga bergantung pada seberapa scalable layanan yang Anda butuhkan (transaksi yang lebih terperinci memiliki dampak yang lebih kecil terkait dengan kunci database). Ini juga bergantung pada seberapa banyak investasi yang ingin Anda lakukan dalam kode Anda, karena konsistensi akhir membutuhkan kode yang lebih kompleks untuk mendeteksi kemungkinan inkonsistensi di seluruh agregat dan kebutuhan untuk menerapkan tindakan kompensasi. Pertimbangkan bahwa jika Anda menerapkan perubahan pada agregat asli dan setelah itu, ketika peristiwa sedang dikirim, jika ada masalah dan penanganan aktivitas tidak dapat menerapkan efek sampingnya, Anda akan memiliki inkonsistensi antara agregat.

Cara untuk mengizinkan tindakan kompensasi adalah dengan menyimpan peristiwa domain dalam tabel database tambahan sehingga dapat menjadi bagian dari transaksi asli. Setelah itu, Anda dapat memiliki proses batch yang mendeteksi inkonsistensi dan menjalankan tindakan kompensasi dengan membandingkan daftar peristiwa dengan status agregat saat ini. Tindakan kompensasi adalah bagian dari topik kompleks yang memerlukan analisis mendalam dari pihak Anda, termasuk mendiskusikannya dengan pengguna bisnis dan ahli domain.

Bagaimanapun, Anda dapat memilih pendekatan yang Anda butuhkan. Tetapi pendekatan awal yang ditangguhkan—memunculkan kejadian sebelum diterapkan, jadi Anda menggunakan satu transaksi, yang merupakan pendekatan paling sederhana saat menggunakan EF Core dan database hubungan. Pendekatan tersebut lebih mudah diterapkan dan valid dalam banyak kasus bisnis. Ini juga pendekatan yang digunakan dalam pemesanan layanan mikro di eShop.

Tetapi, bagaimana Anda benar-benar mengirimkan peristiwa tersebut ke masing-masing penanganan aktivitas? Apa objek _mediator yang Anda lihat di contoh sebelumnya? Ini ada hubungannya dengan teknik dan artefak yang Anda gunakan untuk memetakan antara peristiwa dan penanganan aktivitas peristiwa.

Pengirim peristiwa domain: memetakan dari peristiwa ke penanganan aktivitas

Setelah Anda dapat mengirim atau menerbitkan peristiwa, Anda memerlukan semacam artefak yang akan menerbitkan peristiwa tersebut, sehingga setiap penangan terkait bisa mendapatkannya dan memproses efek samping berdasarkan peristiwa tersebut.

Salah satu pendekatan adalah sistem olahpesan nyata atau bahkan bus peristiwa, mungkin didasarkan pada bus layanan yang bertentangan dengan peristiwa dalam memori. Tetapi, untuk kasus pertama, olahpesan yang sebenarnya akan berlebihan untuk memproses peristiwa domain, karena Anda hanya perlu memproses peristiwa tersebut dalam proses yang sama (yaitu, dalam domain dan lapisan aplikasi yang sama).

Cara berlangganan peristiwa domain

Saat Anda menggunakan MediatR, setiap penanganan aktivitas harus menggunakan jenis peristiwa yang disediakan pada parameter INotificationHandler generik antarmuka, seperti yang Anda lihat dalam kode berikut:

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler
  : INotificationHandler<OrderStartedDomainEvent>

Berdasarkan hubungan antara peristiwa dan penanganan aktivitas, yang dapat dianggap sebagai langganan, artefak MediatR dapat menemukan semua penanganan aktivitas untuk setiap peristiwa dan memicu masing-masing penanganan aktivitas tersebut.

Cara menangani peristiwa domain

Terakhir, penanganan aktivitas biasanya menerapkan kode lapisan aplikasi yang menggunakan repositori infrastruktur untuk mendapatkan agregat tambahan yang diperlukan dan untuk menjalankan logika domain efek samping. Kode penanganan aktivitas domain berikut di eShop, menunjukkan contoh implementasi.

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler
    : INotificationHandler<OrderStartedDomainEvent>
{
    private readonly ILogger _logger;
    private readonly IBuyerRepository _buyerRepository;
    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;

    public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler(
        ILogger<ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler> logger,
        IBuyerRepository buyerRepository,
        IOrderingIntegrationEventService orderingIntegrationEventService)
    {
        _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));
        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task Handle(
        OrderStartedDomainEvent domainEvent, CancellationToken cancellationToken)
    {
        var cardTypeId = domainEvent.CardTypeId != 0 ? domainEvent.CardTypeId : 1;
        var buyer = await _buyerRepository.FindAsync(domainEvent.UserId);
        var buyerExisted = buyer is not null;

        if (!buyerExisted)
        {
            buyer = new Buyer(domainEvent.UserId, domainEvent.UserName);
        }

        buyer.VerifyOrAddPaymentMethod(
            cardTypeId,
            $"Payment Method on {DateTime.UtcNow}",
            domainEvent.CardNumber,
            domainEvent.CardSecurityNumber,
            domainEvent.CardHolderName,
            domainEvent.CardExpiration,
            domainEvent.Order.Id);

        var buyerUpdated = buyerExisted ?
            _buyerRepository.Update(buyer) :
            _buyerRepository.Add(buyer);

        await _buyerRepository.UnitOfWork
            .SaveEntitiesAsync(cancellationToken);

        var integrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(
            domainEvent.Order.Id, domainEvent.Order.OrderStatus.Name, buyer.Name);
        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);

        OrderingApiTrace.LogOrderBuyerAndPaymentValidatedOrUpdated(
            _logger, buyerUpdated.Id, domainEvent.Order.Id);
    }
}

Kode penanganan aktivitas domain sebelumnya dianggap sebagai kode lapisan aplikasi karena menggunakan repositori infrastruktur, seperti yang dijelaskan di bagian berikutnya pada lapisan infrastruktur-persistensi. Penanganan aktivitas juga dapat menggunakan komponen infrastruktur lainnya.

Peristiwa domain dapat menghasilkan peristiwa integrasi untuk diterbitkan di luar batas layanan mikro

Terakhir, penting untuk disebutkan bahwa terkadang Anda mungkin ingin menyebarkan peristiwa di beberapa layanan mikro. Penyebaran tersebut adalah peristiwa integrasi, dan dapat diterbitkan melalui bus peristiwa dari penanganan aktivitas domain tertentu.

Kesimpulan tentang peristiwa domain

Seperti yang dinyatakan, gunakan peristiwa domain untuk secara eksplisit menerapkan efek samping dari perubahan dalam domain Anda. Untuk menggunakan terminologi DDD, gunakan peristiwa domain untuk secara eksplisit menerapkan efek samping di satu atau beberapa agregat. Selain itu, dan untuk skalabilitas yang lebih baik dan dampak yang lebih kecil pada kunci database, gunakan konsistensi akhir antara agregat dalam domain yang sama.

Aplikasi referensi menggunakan MediatR untuk menyebarkan peristiwa domain secara sinkron di seluruh agregat, dalam satu transaksi. Tetapi, Anda juga dapat menggunakan beberapa penerapan AMQP seperti RabbitMQ atau Azure Service Bus untuk menyebarkan peristiwa domain secara asinkron, menggunakan konsistensi akhir, tetapi, seperti yang disebutkan di atas, Anda harus mempertimbangkan perlunya tindakan kompensasi jika terjadi kegagalan.

Sumber daya tambahan