Merancang lapisan persistensi infrastruktur

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.

Komponen persistensi data menyediakan akses ke data yang dihosting dalam batas layanan mikro (yaitu, database layanan mikro). Mereka berisi implementasi aktual komponen seperti repositori dan kelas Unit of Work, seperti objek Entity Framework (EF) DbContext kustom. EF DbContext mengimplementasikan repositori dan pola Unit Kerja.

Pola Repositori

Pola Repositori adalah pola Desain Berbasis Domain yang dimaksudkan untuk menjaga kekhawatiran persistensi di luar model domain sistem. Satu atau beberapa abstraksi persistensi - antarmuka - didefinisikan dalam model domain, dan abstraksi ini memiliki implementasi dalam bentuk adaptor khusus persistensi yang ditentukan di tempat lain dalam aplikasi.

Implementasi repositori adalah kelas yang merangkum logika yang diperlukan untuk mengakses sumber data. Mereka mempusatkan fungsionalitas akses data umum, memberikan pemeliharaan yang lebih baik dan memisahkan infrastruktur atau teknologi yang digunakan untuk mengakses database dari model domain. Jika Anda menggunakan Object-Relational Mapper (ORM) seperti Entity Framework, kode yang harus diimplementasikan disederhanakan, berkat LINQ dan pengetikan yang kuat. Ini memungkinkan Anda berfokus pada logika persistensi data dan bukan pada pipa akses data.

Pola repositori adalah cara yang terbukti baik untuk bekerja dengan sumber data. Dalam buku Patterns of Enterprise Application Architecture, Martin Fowler menjelaskan repositori sebagai berikut:

Repositori melakukan tugas perantara antara lapisan model domain dan pemetaan data, bertindak dengan cara yang sama dengan sekumpulan objek domain dalam memori. Objek klien secara deklaratif membangun kueri dan mengirimkannya ke repositori untuk mendapatkan jawaban. Secara konseptual, repositori merangkum sekumpulan objek yang disimpan dalam database dan operasi yang dapat dilakukan di sana, menyediakan jalan yang lebih dekat dengan lapisan persistensi. Repositori, juga, mendukung tujuan pemisahan, dengan jelas dan dalam satu arah, dependensi antara domain kerja dan alokasi atau pemetaan data.

Tentukan satu repositori per agregat

Untuk setiap agregat atau root agregat, Anda harus membuat satu kelas repositori. Anda mungkin dapat memanfaatkan C# Generics untuk mengurangi jumlah total kelas konkret yang perlu Anda pertahankan (seperti yang ditunjukkan nanti dalam bab ini). Dalam layanan mikro berdasarkan pola Domain-Driven Design (DDD), satu-satunya saluran yang harus Anda gunakan untuk memperbarui database adalah repositori. Ini karena mereka memiliki hubungan satu-ke-satu dengan root agregat, yang mengontrol invarian agregat dan konsistensi transaksional. Anda dapat mengkueri database melalui saluran lain (karena Anda bisa mengikuti pendekatan CQRS berikut), karena kueri tidak mengubah status database. Namun, area transaksional (yaitu, pembaruan) harus selalu dikontrol oleh repositori dan root agregat.

Pada dasarnya, repositori memungkinkan Anda mengisi data dalam memori yang berasal dari database dalam bentuk entitas domain. Setelah entitas berada dalam memori, entitas dapat diubah dan kemudian dipertahankan kembali ke database melalui transaksi.

Seperti disebutkan sebelumnya, jika Anda menggunakan pola arsitektur CQS/CQRS, kueri awal dilakukan oleh kueri sisi di luar model domain, yang dilakukan oleh pernyataan SQL sederhana menggunakan Dapper. Pendekatan ini jauh lebih fleksibel daripada repositori karena Anda dapat mengkueri dan menggabungkan tabel apa pun yang Anda butuhkan, dan kueri ini tidak dibatasi oleh aturan dari agregat. Data tersebut masuk ke lapisan presentasi atau aplikasi klien.

Jika pengguna membuat perubahan, data yang akan diperbarui berasal dari aplikasi klien atau lapisan presentasi ke lapisan aplikasi (seperti layanan WEB API). Saat Anda menerima perintah dalam penanganan perintah, Anda menggunakan repositori untuk mendapatkan data yang ingin Anda perbarui dari database. Anda memperbaruinya dalam memori dengan data yang diteruskan dengan perintah, kemudian Anda menambahkan atau memperbarui data (entitas domain) dalam database melalui transaksi.

Penting untuk menekankan lagi bahwa Anda hanya boleh menentukan satu repositori untuk setiap root agregat, seperti yang ditunjukkan pada Gambar 7-17. Untuk mencapai tujuan root agregat untuk memelihara konsistensi transaksional antara semua objek dalam agregat, Anda tidak boleh membuat repositori untuk setiap tabel dalam database.

Diagram showing relationships of domain and other infrastructure.

Gambar 7-17. Hubungan antara repositori, agregat, dan tabel database

Diagram di atas menunjukkan hubungan antara lapisan Domain dan Infrastruktur: Buyer Agregat tergantung pada IBuyerRepository dan Order Aggregate tergantung pada antarmuka IOrderRepository, antarmuka ini diimplementasikan di lapisan Infrastruktur oleh repositori yang sesuai yang bergantung pada UnitOfWork, juga diimplementasikan di sana, yang mengakses tabel di tingkat Data.

Menerapkan satu root agregat per repositori

Sangat penting untuk mengimplementasikan desain repositori Anda agar desain memberlakukan aturan bahwa hanya root agregat yang harus memiliki repositori. Anda dapat membuat jenis repositori generik atau dasar yang membatasi jenis entitas yang bekerja dengannya untuk memastikan mereka memiliki antarmuka penanda IAggregateRoot.

Dengan demikian, setiap kelas repositori yang diterapkan pada lapisan infrastruktur mengimplementasikan kontrak atau antarmukanya sendiri, seperti yang ditunjukkan dalam kode berikut:

namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
    public class OrderRepository : IOrderRepository
    {
      // ...
    }
}

Setiap antarmuka repositori tertentu mengimplementasikan antarmuka IRepository generik:

public interface IOrderRepository : IRepository<Order>
{
    Order Add(Order order);
    // ...
}

Namun, cara yang lebih baik untuk memastikan kode memberlakukan konvensi bahwa setiap repositori terkait dengan satu agregat adalah dengan menerapkan jenis repositori generik. Dengan begitu, secara eksplisit Anda menggunakan repositori untuk menargetkan agregat tertentu. Hal tersebut dapat dengan mudah dilakukan dengan menerapkan antarmuka dasar generik IRepository, seperti dalam kode berikut:

public interface IRepository<T> where T : IAggregateRoot
{
    //....
}

Pola Repositori memudahkan pengujian logika aplikasi Anda

Pola Repositori memungkinkan Anda untuk dengan mudah menguji aplikasi Anda dengan pengujian unit. Ingat bahwa pengujian unit hanya menguji kode Anda, bukan infrastruktur Anda, sehingga abstraksi repositori memudahkan Anda mencapai tujuan tersebut.

Seperti yang disebutkan di bagian sebelumnya, sebaiknya Anda menentukan dan menempatkan antarmuka repositori di lapisan model domain sehingga lapisan aplikasi, seperti layanan mikro API Web Anda, tidak bergantung langsung pada lapisan infrastruktur tempat Anda menerapkan kelas repositori aktual. Dengan melakukan hal ini dan menggunakan Injeksi Dependensi di pengontrol Web API Anda, Anda dapat menerapkan repositori tiruan yang mengembalikan data palsu alih-alih data dari database. Pendekatan yang dipisahkan ini memungkinkan Anda membuat dan menjalankan pengujian unit yang berfokus pada logika aplikasi Anda tanpa memerlukan konektivitas ke database.

Koneksi ke database dapat gagal dan, yang lebih penting, menjalankan ratusan pengujian terhadap database adalah hal yang buruk dilakukan karena dua alasan. Pertama, prosesnya bisa memakan waktu lama karena banyaknya tes. Kedua, rekaman database mungkin berubah dan berdampak pada hasil pengujian Anda, terutama jika pengujian Anda berjalan secara paralel, sehingga mungkin tidak konsisten. Pengujian unit biasanya dapat berjalan secara paralel; pengujian integrasi mungkin tidak mendukung eksekusi paralel tergantung pada implementasinya. Pengujian terhadap database bukanlah pengujian unit tetapi merupakan pengujian integrasi. Anda harus menjalankan banyak pengujian unit yang berjalan cepat, tetapi lebih sedikit pengujian integrasi terhadap database.

Dalam hal pemisahan kekhawatiran untuk pengujian unit, logika Anda beroperasi pada entitas domain dalam memori. Hal ini mengasumsikan bahwa kelas repositori telah mengirimkannya. Setelah logika Anda memodifikasi entitas domain, logika mengasumsikan kelas repositori akan menyimpannya dengan benar. Poin penting di sini adalah untuk membuat pengujian unit terhadap model domain Anda dan logika domainnya. Root agregat adalah batas konsistensi utama dalam DDD.

Repositori yang diterapkan di eShopOnContainers mengandalkan implementasi DbContext EF Core dari pola Repositori dan Unit of Work menggunakan pelacak perubahannya, sehingga mereka tidak menduplikasi fungsionalitas ini.

Perbedaan antara pola Repositori dan pola kelas Akses Data (kelas DAL) warisan

Objek DAL umum secara langsung melakukan operasi akses data dan persistensi terhadap penyimpanan, seringkali pada tingkat satu tabel dan baris. Operasi CRUD sederhana yang diterapkan dengan sekumpulan kelas DAL sering kali tidak mendukung transaksi (meskipun ini tidak selalu terjadi). Sebagian besar pendekatan kelas DAL menggunakan abstraksi minimal, yang mengakibatkan konektor yang ketat antara aplikasi atau kelas Business Logic Layer (BLL) yang memanggil objek DAL.

Saat menggunakan repositori, detail implementasi persistensi dienkapsulasi jauh dari model domain. Penggunaan abstraksi memberikan kemudahan memperluas perilaku melalui pola seperti Dekorator atau Proksi. Misalnya, masalah pemotongan silang seperti penembolokan, pengelogan, dan penanganan kesalahan semuanya dapat diterapkan menggunakan pola ini daripada dikodekan secara permanen dalam kode akses data itu sendiri. Ini juga sepele untuk mendukung beberapa adaptor repositori yang dapat digunakan di lingkungan yang berbeda, dari pengembangan lokal hingga lingkungan penahapan bersama hingga produksi.

Unit Kerja Pelaksana

Unit kerja mengacu pada satu transaksi yang melibatkan beberapa operasi penyisipan, pembaruan, atau penghapusan. Dalam istilah sederhana, hal ini berarti bahwa untuk tindakan pengguna tertentu, seperti pendaftaran di situs web, semua operasi sisipan, pembaruan, dan penghapusan ditangani dalam satu transaksi. Ini lebih efisien daripada menangani beberapa operasi database dengan cara yang lebih cerewet.

Sejumlah operasi persistensi ini akan dilakukan nanti dalam satu tindakan ketika kode Anda dari lapisan aplikasi memerintahkannya. Keputusan tentang menerapkan perubahan dalam memori ke penyimpanan database aktual biasanya didasarkan pada pola Unit of Work. Dalam EF, pola Unit Kerja diimplementasikan oleh DbContext dan dijalankan ketika panggilan dilakukan ke SaveChanges.

Dalam banyak kasus, pola atau cara menerapkan operasi terhadap penyimpanan ini dapat meningkatkan performa aplikasi dan mengurangi kemungkinan inkonsistensi. Pola ini juga mengurangi pemblokiran transaksi dalam tabel database, karena semua operasi yang dimaksudkan dilakukan sebagai bagian dari satu transaksi. Hal ini lebih efisien dibandingkan dengan mengeksekusi banyak operasi terisolasi terhadap database. Oleh karena itu, ORM yang dipilih dapat mengoptimalkan eksekusi terhadap database dengan mengelompokkan beberapa tindakan pembaruan dalam transaksi yang sama, dibandingkan dengan banyak eksekusi transaksi kecil dan terpisah.

Pola Unit Kerja dapat diimplementasikan dengan atau tanpa menggunakan pola Repositori.

Repositori tidak seharusnya bersifat wajib

Repositori kustom berguna karena alasan yang dikutip sebelumnya, dan pendekatan tersebut digunakan untuk memesan layanan mikro di eShopOnContainers. Namun, ini bukan merupakan pola penting untuk diterapkan dalam desain DDD atau bahkan dalam pengembangan .NET umum.

Misalnya, Jimmy Bogard, saat memberikan umpan balik langsung untuk panduan ini, mengatakan hal-hal berikut:

Ini mungkin akan menjadi umpan balik terbesar saya. Saya benar-benar tidak menyukai repositori, terutama karena mereka menyembunyikan detail penting dari mekanisme persistensi yang mendasarinya. Itu sebabnya saya juga memilih MediatR untuk perintah. Saya dapat menggunakan kekuatan penuh lapisan persistensi, dan mendorong semua perilaku domain tersebut ke root agregat saya. Saya biasanya tidak ingin meniru repositori saya - saya masih harus menguji integrasi itu dengan hal yang sebenarnya. Memilih CQRS berarti bahwa kita sebenarnya tidak membutuhkan repositori lagi.

Repositori mungkin berguna, tetapi tidak penting untuk desain DDD Anda dengan cara pola Agregat dan model domain yang kaya. Oleh karena itu, Anda dapat menggunakan pola Repositori atau tidak, sesuai keinginan Anda.

Sumber daya tambahan

Pola repositori

Pola Unit of Work