DDD - aturan bahwa Entitas tidak dapat mengakses Gudang secara langsung

185

Dalam Domain Driven Design, tampaknya ada banyak dari kesepakatan yang Entitas tidak boleh akses Repositori langsung.

Apakah ini berasal dari buku Eric Evans Domain Driven Design , atau apakah itu datang dari tempat lain?

Di mana ada beberapa penjelasan yang bagus untuk alasan di baliknya?

sunting: Untuk memperjelas: Saya tidak berbicara tentang praktik OO klasik memisahkan akses data ke dalam lapisan terpisah dari logika bisnis - Saya berbicara tentang pengaturan khusus di mana dalam DDD, Entitas tidak seharusnya berbicara dengan data lapisan akses sama sekali (yaitu mereka tidak seharusnya memegang referensi ke objek Repositori)

pembaruan: Saya memberikan hadiah kepada BacceSR karena jawabannya tampak paling dekat, tapi saya masih cukup gelap dalam hal ini. Jika itu prinsip yang begitu penting, pasti ada beberapa artikel bagus tentang itu di suatu tempat online, tentunya?

pembaruan: Maret 2013, suara positif pada pertanyaan menyiratkan ada banyak minat dalam hal ini, dan meskipun ada banyak jawaban, saya masih berpikir ada ruang untuk lebih jika orang memiliki ide tentang ini.

seperti kode
sumber
Lihatlah pertanyaan saya stackoverflow.com/q/8269784/235715 , itu menunjukkan situasi ketika sulit untuk menangkap logika, tanpa Entitas memiliki akses ke repositori. Meskipun saya pikir entitas tidak boleh memiliki akses ke repositori, dan ada solusi untuk situasi saya ketika kode dapat ditulis ulang tanpa referensi repositori, tetapi saat ini saya tidak dapat memikirkannya.
Alex Burtsev
Tidak tahu dari mana asalnya. Pikiranku: Saya pikir kesalahpahaman ini datang dari orang-orang bagaimana tidak mengerti apa itu DDD. Pendekatan ini bukan untuk mengimplementasikan perangkat lunak tetapi untuk mendesainnya (domain .. design). Dulu, kami memiliki arsitek dan pelaksana, tetapi sekarang hanya ada pengembang perangkat lunak. DDD dimaksudkan untuk arsitek. Dan ketika seorang arsitek sedang merancang perangkat lunak, ia membutuhkan beberapa alat atau pola untuk merepresentasikan memori atau basis data bagi para pengembang yang akan mengimplementasikan desain yang disiapkan. Tetapi desain itu sendiri (dari perspektif bisnis) tidak memiliki atau membutuhkan repositori.
berhalak

Jawaban:

47

Ada sedikit kebingungan di sini. Repositori mengakses akar agregat. Akar agregat adalah entitas. Alasan untuk ini adalah pemisahan keprihatinan dan layering yang baik. Ini tidak masuk akal pada proyek-proyek kecil, tetapi jika Anda berada di tim besar Anda ingin mengatakan, "Anda mengakses produk melalui Gudang Produk. Produk adalah akar agregat untuk kumpulan entitas, termasuk objek ProductCatalog. Jika Anda ingin memperbarui ProductCatalog Anda harus pergi melalui ProductRepository. "

Dengan cara ini Anda memiliki pemisahan yang sangat, sangat jelas pada logika bisnis dan di mana hal-hal diperbarui. Anda tidak memiliki anak yang keluar sendiri dan menulis seluruh program ini yang melakukan semua hal rumit ini ke katalog produk dan ketika datang untuk mengintegrasikannya ke proyek hulu, Anda duduk di sana memandangnya dan menyadarinya semua harus dibuang. Ini juga berarti ketika orang bergabung dengan tim, menambahkan fitur baru, mereka tahu ke mana harus pergi dan bagaimana menyusun program.

Tapi tunggu! Repositori juga merujuk ke lapisan persistensi, seperti dalam Pola Repositori. Dalam dunia yang lebih baik Repositori Eric Evans dan Pola Repositori akan memiliki nama yang terpisah, karena mereka cenderung tumpang tindih sedikit. Untuk mendapatkan pola repositori, Anda memiliki kontras dengan cara lain di mana data diakses, dengan bus layanan atau sistem model kejadian. Biasanya ketika Anda sampai ke level ini, definisi Repositori Eric Evans berjalan di sisi jalan dan Anda mulai berbicara tentang konteks yang dibatasi. Setiap konteks terikat pada dasarnya adalah aplikasi sendiri. Anda mungkin memiliki sistem persetujuan yang canggih untuk memasukkan barang ke dalam katalog produk. Dalam desain asli Anda, produk adalah bagian utama tetapi dalam konteks terbatas ini katalog produk. Anda masih dapat mengakses informasi produk dan memperbarui produk melalui bus layanan,

Kembali ke pertanyaan awal Anda. Jika Anda mengakses repositori dari dalam suatu entitas itu berarti entitas tersebut benar-benar bukan entitas bisnis tetapi mungkin sesuatu yang harus ada di lapisan layanan. Ini karena entitas adalah objek bisnis dan harus memperhatikan dirinya sendiri sebanyak DSL (bahasa spesifik domain) sebanyak mungkin. Hanya memiliki informasi bisnis di lapisan ini. Jika Anda memecahkan masalah kinerja, Anda harus mencari di tempat lain karena hanya informasi bisnis yang ada di sini. Jika tiba-tiba, Anda memiliki masalah aplikasi di sini, Anda membuatnya sangat sulit untuk memperpanjang dan memelihara aplikasi, yang sebenarnya adalah jantung dari DDD: membuat perangkat lunak yang dapat dirawat.

Tanggapan terhadap Komentar 1 : Benar, pertanyaan yang bagus. Jadi tidak semua validasi terjadi di lapisan domain. Sharp memiliki atribut "DomainSignature" yang melakukan apa yang Anda inginkan. Ini adalah ketekunan yang disadari, tetapi menjadi atribut membuat lapisan domain tetap bersih. Ini memastikan bahwa Anda tidak memiliki entitas duplikat dengan, dalam contoh Anda nama yang sama.

Tetapi mari kita bicara tentang aturan validasi yang lebih rumit. Katakanlah Anda adalah Amazon.com. Pernahkah Anda memesan sesuatu dengan kartu kredit kedaluwarsa? Saya punya, di mana saya belum memperbarui kartu dan membeli sesuatu. Ia menerima pesanan dan UI memberi tahu saya bahwa semuanya sangat bagus. Sekitar 15 menit kemudian, saya akan menerima email yang mengatakan ada masalah dengan pesanan saya, kartu kredit saya tidak valid. Apa yang terjadi di sini adalah bahwa, idealnya, ada beberapa validasi regex di lapisan domain. Apakah ini nomor kartu kredit yang benar? Jika ya, tetap ada pesanan. Namun, ada validasi tambahan di lapisan tugas aplikasi, di mana layanan eksternal diminta untuk melihat apakah pembayaran dapat dilakukan pada kartu kredit. Jika tidak, jangan mengirim barang, tunda pesanan, dan tunggu pelanggan.

Jangan takut untuk membuat objek validasi pada lapisan layanan yang dapat mengakses repositori. Jauhkan dari lapisan domain.

kertosis
sumber
15
Terima kasih. Tetapi saya harus berusaha untuk mendapatkan sebanyak mungkin logika bisnis ke dalam entitas (dan pabrik serta spesifikasi yang terkait dan sebagainya), bukan? Tetapi jika tidak ada dari mereka yang diizinkan mengambil data melalui Gudang, bagaimana saya harus menulis logika bisnis (yang cukup rumit)? Misalnya: Pengguna ruang obrolan tidak diizinkan mengubah nama mereka menjadi nama yang sudah digunakan orang lain. Saya ingin aturan itu dibangun oleh entitas ChatUser, tetapi itu tidak mudah dilakukan jika Anda tidak dapat menekan repositori dari sana. Jadi apa yang harus aku lakukan?
codeulike
Respons saya lebih besar dari yang dibolehkan oleh kotak komentar, lihat hasil edit.
kertosis
6
Entitas Anda harus tahu cara melindungi diri dari bahaya. Ini termasuk memastikan tidak bisa masuk ke keadaan tidak valid. Apa yang Anda gambarkan dengan pengguna ruang obrolan adalah logika bisnis yang berada di TAMBAHAN dengan logika entitas harus tetap valid. Logika bisnis seperti apa yang benar-benar Anda inginkan dalam layanan Chatroom, bukan entitas ChatUser.
Alec
9
Terima kasih, Alec. Itu cara yang jelas untuk mengekspresikannya. Tetapi bagi saya tampaknya bahwa Evans 'aturan emas yang berfokus pada domain' semua logika bisnis harus masuk dalam lapisan domain 'bertentangan dengan aturan' entitas tidak boleh mengakses repositori '. Saya dapat hidup dengan itu jika saya mengerti mengapa demikian, tetapi saya tidak dapat menemukan penjelasan yang bagus di internet tentang mengapa entitas tidak boleh mengakses repositori. Evans sepertinya tidak menyebutkannya secara eksplisit. Dari mana asalnya? Jika Anda dapat memposting jawaban yang menunjuk pada beberapa literatur yang bagus, Anda mungkin dapat memberikan hadiah 50pt untuk diri Anda sendiri :)
codeulike
4
"itu tidak masuk akal pada hal kecil" Ini adalah kesalahan besar yang dilakukan tim ... itu adalah proyek kecil, karena itu aku bisa melakukan ini dan itu ... berhenti berpikir seperti itu. Banyak proyek kecil yang kami kerjakan, akhirnya menjadi besar, karena persyaratan bisnis. Jika Anda melakukan sesuatu yang kecil atau besar, lakukan dengan benar.
MeTitus
35

Pada awalnya, saya yakin bahwa beberapa entitas saya dapat mengakses repositori (mis. Pemuatan malas tanpa ORM). Kemudian saya sampai pada kesimpulan bahwa saya tidak boleh dan bahwa saya bisa menemukan cara alternatif:

  1. Kita harus mengetahui niat kita dalam permintaan dan apa yang kita inginkan dari domain, oleh karena itu kita dapat melakukan panggilan repositori sebelum membuat atau memohon perilaku Agregat. Ini juga membantu menghindari masalah dalam memori yang tidak konsisten dan perlunya pemuatan yang malas (lihat artikel ini ). Baunya adalah bahwa Anda tidak dapat membuat instance dalam memori dari entitas Anda lagi tanpa khawatir tentang akses data.
  2. CQS (Command Query Separation) dapat membantu mengurangi kebutuhan ingin memanggil repositori untuk hal-hal di entitas kita.
  3. Kita dapat menggunakan spesifikasi untuk merangkum dan mengomunikasikan kebutuhan logika domain dan meneruskannya ke repositori sebagai gantinya (layanan dapat mengatur hal-hal ini untuk kita). Spesifikasi dapat berasal dari entitas yang bertugas mempertahankan invarian itu. Repositori akan menginterpretasikan bagian dari spesifikasi ke dalam implementasi kueri itu sendiri dan menerapkan aturan dari spesifikasi pada hasil kueri. Ini bertujuan untuk menjaga logika domain di lapisan domain. Ini juga melayani Bahasa Ubiquitous dan komunikasi yang lebih baik. Bayangkan mengatakan "spesifikasi pesanan yang terlambat" dibandingkan dengan mengatakan "filter pesanan dari tbl_order di mana ditempatkan_at kurang dari 30 menit sebelum sysdate" (lihat jawaban ini ).
  4. Ini membuat alasan tentang perilaku entitas menjadi lebih sulit karena Prinsip Tanggung Jawab Tunggal dilanggar. Jika Anda perlu mengatasi masalah penyimpanan / kegigihan, Anda tahu ke mana harus pergi dan ke mana tidak pergi.
  5. Ini menghindari bahaya memberikan entitas akses dua arah ke negara global (melalui repositori dan layanan domain). Anda juga tidak ingin melanggar batas transaksi Anda.

Vernon Vaughn dalam buku merah Implementing Domain-Driven Design mengacu pada masalah ini di dua tempat yang saya ketahui (catatan: buku ini sepenuhnya didukung oleh Evans seperti yang dapat Anda baca dalam kata pengantar). Dalam Bab 7 tentang Layanan, ia menggunakan layanan domain dan spesifikasi untuk mengatasi kebutuhan agregat untuk menggunakan repositori dan agregat lain untuk menentukan apakah pengguna diautentikasi. Dia dikutip mengatakan:

Sebagai patokan, kita harus mencoba menghindari penggunaan Repositori (12) dari dalam Agregat, jika memungkinkan.

Vernon, Vaughn (2013-02-06). Menerapkan Desain Berbasis Domain (Kindle Location 6089). Pendidikan Pearson. Edisi menyalakan.

Dan di Bab 10 tentang Agregat, di bagian berjudul "Navigasi Model" katanya (tepat setelah dia merekomendasikan penggunaan ID unik global untuk referensi akar agregat lainnya):

Referensi berdasarkan identitas tidak sepenuhnya mencegah navigasi melalui model. Beberapa akan menggunakan Repositori (12) dari dalam Agregat untuk pencarian. Teknik ini disebut Model Domain Terputus, dan itu sebenarnya bentuk pemuatan malas. Namun, ada pendekatan yang disarankan berbeda: Gunakan Layanan Repositori atau Domain (7) untuk mencari objek dependen sebelum memanggil perilaku Agregat. Layanan Aplikasi klien dapat mengontrol ini, kemudian mengirim ke Agregat:

Dia menunjukkan contoh ini dalam kode:

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

Dia selanjutnya juga menyebutkan solusi lain tentang bagaimana layanan domain dapat digunakan dalam metode perintah Agregat bersama dengan pengiriman ganda . (Saya tidak bisa merekomendasikan cukup seberapa bermanfaat untuk membaca bukunya. Setelah Anda lelah dari mencari-cari di internet, membayar lebih dari uang yang layak dan membaca buku.)

Saya kemudian berdiskusi dengan Marco Pivetta @Ocramius yang selalu ramah yang menunjukkan kepada saya sedikit kode untuk mengeluarkan spesifikasi dari domain dan menggunakannya:

1) Ini tidak dianjurkan:

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2) Dalam layanan domain, ini bagus:

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}
prograhammer
sumber
1
Pertanyaan: Kami selalu diajarkan untuk tidak membuat objek dalam keadaan tidak valid atau tidak konsisten. Ketika Anda memuat pengguna dari repositori, dan kemudian Anda menelepon getFriends()sebelum melakukan hal lain, itu akan kosong atau malas dimuat. Jika kosong, maka objek ini bohong dan dalam kondisi tidak valid. Ada pemikiran tentang ini?
Jimbo
Repositori memanggil Domain untuk membuat instance baru. Anda tidak mendapatkan instance dari Pengguna tanpa melalui Domain. Masalah yang dijawab oleh jawaban ini adalah kebalikannya. Di mana Domain mereferensikan Repositori, dan ini harus dihindari.
Pemrogram
28

Ini pertanyaan yang sangat bagus. Saya akan menantikan beberapa diskusi tentang ini. Tapi saya pikir itu disebutkan dalam beberapa buku DDD dan Jimmy nilssons dan Eric Evans. Saya kira itu juga terlihat melalui contoh bagaimana menggunakan pola reposistory.

TAPI mari kita bahas. Saya pikir pemikiran yang sangat valid adalah mengapa entitas harus tahu tentang bagaimana bertahan entitas lain? Penting dengan DDD adalah bahwa setiap entitas memiliki tanggung jawab untuk mengelola "lingkup pengetahuan" sendiri dan tidak boleh tahu apa-apa tentang cara membaca atau menulis entitas lain. Tentu Anda mungkin bisa saja menambahkan antarmuka repositori ke Entity A untuk membaca Entities B. Tetapi risikonya adalah Anda mengekspos pengetahuan tentang cara bertahan B. Akankah entitas A juga melakukan validasi pada B sebelum mempertahankan B ke db?

Seperti yang Anda lihat entitas A dapat lebih terlibat ke dalam siklus hidup entitas B dan yang dapat menambah kompleksitas pada model.

Saya kira (tanpa contoh) bahwa pengujian unit akan lebih kompleks.

Tapi saya yakin akan selalu ada skenario di mana Anda tergoda untuk menggunakan repositori melalui entitas. Anda harus melihat setiap skenario untuk membuat penilaian yang valid. Pro dan kontra. Tetapi solusi repositori-entitas menurut saya dimulai dengan banyak Kontra. Ini harus menjadi skenario yang sangat istimewa dengan Pro yang menyeimbangkan ....

Magnus Backeus
sumber
1
Poin yang bagus. Model-domain sekolah yang lama mungkin akan meminta Entity B bertanggung jawab untuk memvalidasi dirinya sendiri sebelum ia tetap bertahan, saya kira. Apakah Anda yakin Evans menyebutkan Entitas tidak menggunakan Gudang? Saya sedang membaca buku dan belum menyebutkannya ...
codeulike
Yah saya membaca buku beberapa tahun yang lalu (well 3 ...) dan ingatan saya gagal. Saya tidak dapat mengingat apakah dia benar-benar mengucapkannya TETAPI namun saya percaya dia menggambarkan ini melalui contoh-contoh. Anda juga dapat menemukan interpretasi komunitas dari contoh Cargo-nya (dari bukunya) di dddsamplenet.codeplex.com . Unduh proyek kode (lihat proyek Vanilla - ini adalah contoh dari buku). Anda akan menemukan bahwa repositori hanya digunakan di lapisan Aplikasi untuk mengakses entitas domain.
Magnus Backeus
1
Mengunduh contoh DDD SmartCA dari buku p2p.wrox.com/... Anda akan melihat pendekatan lain (meskipun ini adalah klien windows RIA) di mana repositori digunakan dalam layanan (tidak ada yang aneh di sini) tetapi layanan digunakan di dalam entites. Ini adalah sesuatu yang tidak akan saya lakukan TAPI saya seorang pria aplikasi webb. Mengingat skenario untuk aplikasi SmartCA di mana Anda harus dapat bekerja offline, mungkin desain ddd akan terlihat berbeda.
Magnus Backeus
Contoh SmartCA terdengar menarik, di bab mana itu? (unduhan kode diatur oleh bab)
codeulike
1
@codeulike Saat ini saya sedang merancang dan mengimplementasikan kerangka kerja menggunakan konsep ddd. Kadang-kadang melakukan validasi perlu mengakses database dan meng-query-nya (contoh: meminta beberapa kolom untuk memeriksa indeks unik). Sehubungan dengan ini dan fakta bahwa query harus ditulis dalam lapisan repositori. Ternyata entitas domain perlu memiliki referensi ke antarmuka repositori mereka di lapisan model domain untuk menempatkan validasi sepenuhnya di lapisan model domain. Jadi apakah akhirnya entitas domain memiliki akses ke repositori?
Karamafrooz
13

Mengapa memisahkan akses data?

Dari buku ini, saya pikir dua halaman pertama dari bab Model Driven Design memberikan beberapa alasan mengapa Anda ingin mengaburkan rincian implementasi teknis dari penerapan model domain.

  • Anda ingin menjaga hubungan yang erat antara model domain dan kode
  • Memisahkan masalah teknis membantu membuktikan model ini praktis untuk implementasi
  • Anda ingin bahasa di mana-mana untuk menembus hingga ke desain sistem

Tampaknya ini semua untuk tujuan menghindari "model analisis" terpisah yang menjadi terpisah dari implementasi sistem yang sebenarnya.

Dari apa yang saya mengerti tentang buku ini, dikatakan "model analisis" ini dapat dirancang tanpa mempertimbangkan implementasi perangkat lunak. Setelah pengembang mencoba menerapkan model yang dipahami oleh sisi bisnis, mereka membentuk abstraksi mereka sendiri karena kebutuhan, menyebabkan dinding dalam komunikasi dan pemahaman.

Di arah lain, pengembang yang memperkenalkan terlalu banyak masalah teknis ke dalam model domain dapat menyebabkan kesenjangan ini juga.

Jadi Anda dapat mempertimbangkan bahwa mempraktikkan pemisahan masalah seperti kegigihan dapat membantu melindungi terhadap desain ini dan model analisis yang berbeda. Jika dirasa perlu untuk memperkenalkan hal-hal seperti ketekunan ke dalam model maka itu adalah bendera merah. Mungkin modelnya tidak praktis untuk implementasi.

Mengutip:

"Model tunggal mengurangi kemungkinan kesalahan, karena desain sekarang merupakan hasil langsung dari model yang dipertimbangkan dengan hati-hati. Desain, dan bahkan kode itu sendiri, memiliki komunikasi model."

Cara saya menafsirkan ini, jika Anda berakhir dengan lebih banyak baris kode berurusan dengan hal-hal seperti akses database, Anda kehilangan komunikasi itu.

Jika kebutuhan untuk mengakses database adalah untuk hal-hal seperti memeriksa keunikan, lihat:

Udi Dahan: kesalahan terbesar yang dilakukan tim saat menerapkan DDD

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

di bawah "Semua aturan tidak dibuat sama"

dan

Menggunakan Pola Model Domain

http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119

di bawah "Skenario untuk Tidak Menggunakan Model Domain", yang menyentuh subjek yang sama.

Cara memisahkan akses data

Memuat data melalui antarmuka

"Lapisan akses data" telah diabstraksi melalui antarmuka, yang Anda panggil untuk mengambil data yang diperlukan:

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

Pro: Antarmuka memisahkan kode pipa "akses data", memungkinkan Anda untuk tetap menulis tes. Akses data dapat ditangani berdasarkan kasus per kasus memungkinkan kinerja yang lebih baik daripada strategi generik.

Cons: Kode panggilan harus menganggap apa yang telah dimuat dan apa yang belum.

Katakanlah GetOrderLines mengembalikan objek OrderLine dengan properti ProductInfo nol karena alasan kinerja. Pengembang harus memiliki pengetahuan mendalam tentang kode di balik antarmuka.

Saya sudah mencoba metode ini di sistem nyata. Anda akhirnya mengubah ruang lingkup apa yang dimuat sepanjang waktu dalam upaya untuk memperbaiki masalah kinerja. Anda akhirnya mengintip di belakang antarmuka untuk melihat kode akses data untuk melihat apa yang sedang dan tidak dimuat.

Sekarang, pemisahan kekhawatiran harus memungkinkan pengembang untuk fokus pada satu aspek kode pada satu waktu, sebanyak mungkin. Teknik antarmuka menghapus BAGAIMANA data ini dimuat, tetapi tidak BAGAIMANA BANYAK data dimuat, KAPAN dimuat, dan DI MANA itu dimuat.

Kesimpulan: Pemisahan yang sangat rendah!

Pemuatan Malas

Data dimuat sesuai permintaan. Panggilan untuk memuat data disembunyikan di dalam objek grafik itu sendiri, di mana mengakses properti dapat menyebabkan kueri sql untuk dieksekusi sebelum mengembalikan hasilnya.

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

Pro: 'KAPAN, DI MANA, dan BAGAIMANA' dari akses data disembunyikan dari pengembang yang berfokus pada logika domain. Tidak ada kode dalam agregat yang berhubungan dengan memuat data. Jumlah data yang dimuat dapat menjadi jumlah persis yang dibutuhkan oleh kode.

Cons: Ketika Anda terkena masalah kinerja, sulit untuk memperbaiki ketika Anda memiliki solusi "satu ukuran cocok untuk semua" generik. Pemuatan malas dapat menyebabkan kinerja yang lebih buruk secara keseluruhan, dan menerapkan pemuatan malas mungkin sulit.

Role Interface / Eager Fetching

Setiap use case dibuat eksplisit melalui Role Interface yang diimplementasikan oleh kelas agregat, yang memungkinkan strategi pemuatan data ditangani per use case.

Strategi pengambilan mungkin terlihat seperti ini:

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

Maka agregat Anda dapat terlihat seperti:

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

Strategi BillOrderFetching digunakan untuk membangun agregat, dan kemudian agregat melakukan tugasnya.

Pros: Memungkinkan untuk kode khusus per kasus penggunaan, memungkinkan untuk kinerja yang optimal. Sejalan dengan Prinsip Segregasi Antarmuka . Tidak ada persyaratan kode yang rumit. Tes unit gabungan tidak harus meniru strategi pemuatan. Strategi pemuatan umum dapat digunakan untuk sebagian besar kasus (mis. Strategi "muat semua") dan strategi pemuatan khusus dapat diterapkan bila perlu.

Cons: Pengembang masih harus menyesuaikan / meninjau strategi pengambilan setelah mengubah kode domain.

Dengan pendekatan strategi pengambilan Anda mungkin masih menemukan diri Anda mengubah kode pengambilan kustom untuk perubahan aturan bisnis. Ini bukan pemisahan keprihatinan yang sempurna tetapi akan berakhir lebih dapat dipertahankan dan lebih baik daripada opsi pertama. Strategi pengambilan memang merangkum CARA, KAPAN dan DI MANA data dimuat. Ini memiliki pemisahan keprihatinan yang lebih baik, tanpa kehilangan fleksibilitas seperti satu ukuran cocok untuk semua pendekatan pemuatan malas.

ttg
sumber
Terima kasih, saya akan memeriksa tautannya. Tetapi dalam jawaban Anda apakah Anda membingungkan 'pemisahan kekhawatiran' dengan 'tidak ada akses sama sekali'? Tentu saja kebanyakan orang akan setuju bahwa lapisan kegigihan harus tetap terpisah dari lapisan tempat Entitas berada. Tetapi itu berbeda dengan mengatakan 'entitas tidak boleh bahkan dapat melihat lapisan kegigihan, bahkan melalui implementasi agnostik yang sangat umum. antarmuka '.
kode sandi seperti
Memuat data melalui antarmuka atau tidak, Anda masih khawatir dengan memuat data saat menerapkan aturan bisnis. Saya setuju bahwa banyak orang masih menyebut pemisahan kekhawatiran ini, mungkin prinsip tanggung jawab tunggal akan menjadi istilah yang lebih baik untuk digunakan.
ttg
1
Tidak yakin bagaimana cara menguraikan komentar terakhir Anda, tetapi saya pikir Anda menyarankan agar data tidak dimuat saat memproses aturan bisnis? Saya melihat itu akan membuat aturan 'lebih murni'. Tetapi banyak jenis aturan bisnis akan perlu merujuk ke data lain - apakah Anda menyarankan bahwa itu harus dimuat di muka oleh objek yang terpisah?
codeulike
@codeulike: Saya telah memperbarui jawaban saya. Anda masih dapat memuat data selama aturan bisnis jika Anda merasa harus melakukannya, tetapi itu tidak memerlukan penambahan baris kode akses data ke dalam model domain Anda (mis. Pemuatan malas). Dalam model domain yang telah saya rancang, data umumnya hanya dimuat di muka seperti yang Anda katakan. Saya telah menemukan bahwa menjalankan aturan bisnis biasanya tidak memerlukan jumlah data yang berlebihan.
ttg
12

Pertanyaan yang sangat bagus. Saya berada di jalur penemuan yang sama, dan sebagian besar jawaban di internet tampaknya membawa banyak masalah karena mereka memberikan solusi.

Jadi (dengan risiko menulis sesuatu yang saya tidak setuju dengan satu tahun dari sekarang) di sini adalah penemuan saya sejauh ini.

Pertama-tama, kami menyukai model domain yang kaya , yang memberi kami kemampuan menemukan yang tinggi (dari apa yang dapat kami lakukan dengan agregat) dan keterbacaan (panggilan metode ekspresif).

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

Kami ingin mencapai ini tanpa menyuntikkan layanan apa pun ke konstruktor entitas, karena:

  • Pengenalan perilaku baru (yang menggunakan layanan baru) dapat menyebabkan perubahan konstruktor, yang berarti perubahan mempengaruhi setiap baris yang membuat entitas !
  • Ini layanan bukan merupakan bagian dari model , tapi konstruktor injeksi akan menyarankan bahwa mereka.
  • Seringkali sebuah layanan (bahkan antarmuka) adalah detail implementasi dan bukan bagian dari domain. Model domain akan memiliki ketergantungan yang menghadap ke luar .
  • Ini bisa membingungkan mengapa entitas tidak bisa ada tanpa dependensi ini. (Layanan catatan kredit, katamu? Aku bahkan tidak akan melakukan apa pun dengan catatan kredit ...)
  • Itu akan membuat instantiate sulit, sehingga sulit untuk diuji .
  • Masalahnya menyebar dengan mudah, karena entitas lain yang mengandung ini akan mendapatkan dependensi yang sama - yang pada mereka mungkin terlihat seperti dependensi yang sangat tidak wajar .

Lalu, bagaimana kita bisa melakukan ini? Kesimpulan saya sejauh ini adalah bahwa dependensi metode dan pengiriman ganda memberikan solusi yang layak.

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()sekarang membutuhkan layanan yang bertanggung jawab untuk membuat catatan kredit. Ini menggunakan pengiriman ganda , sepenuhnya membongkar pekerjaan ke layanan yang bertanggung jawab, sambil mempertahankan kemampuan penemuan dari Invoiceentitas.

SetStatus()sekarang memiliki ketergantungan sederhana pada pencatat, yang jelas akan melakukan bagian dari pekerjaan .

Untuk yang terakhir, untuk membuat segalanya lebih mudah pada kode klien, kita mungkin malah login melalui IInvoiceService. Lagi pula, pembuatan faktur tampaknya cukup intrinsik dengan faktur. Seperti single IInvoiceServicemembantu menghindari kebutuhan untuk semua jenis layanan mini untuk berbagai operasi. Kelemahannya adalah menjadi tidak jelas apa tepatnya layanan yang akan dilakukan . Bahkan mungkin mulai terlihat seperti pengiriman ganda, sementara sebagian besar pekerjaan masih dilakukan dengan SetStatus()sendirinya.

Kami masih bisa memberi nama parameter 'logger', dengan harapan dapat mengungkapkan maksud kami. Tampaknya agak lemah.

Sebagai gantinya, saya akan memilih untuk meminta IInvoiceLogger(seperti yang sudah kita lakukan dalam contoh kode) dan telah IInvoiceServicemengimplementasikan antarmuka itu. Kode klien hanya dapat menggunakan tunggal IInvoiceServiceuntuk semua Invoicemetode yang meminta 'mini-service' yang sangat khusus, faktur-intrinsik, sementara metode tanda tangan masih membuat sangat jelas apa yang mereka minta.

Saya perhatikan bahwa saya belum membahas repositori dengan jelas. Logger adalah atau menggunakan repositori, tetapi izinkan saya juga memberikan contoh yang lebih eksplisit. Kita dapat menggunakan pendekatan yang sama, jika repositori dibutuhkan hanya dalam satu atau dua metode.

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

Bahkan, ini memberikan alternatif untuk beban malas yang selalu menyusahkan .

Pembaruan: Saya telah meninggalkan teks di bawah ini untuk tujuan historis, tetapi saya sarankan untuk menghindari muatan malas 100%.

Untuk benar, beban malas berbasis properti, saya tidak sedang menggunakan konstruktor injeksi, tapi dengan cara ketekunan-bodoh.

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

Di satu sisi, repositori yang memuat Invoicedari database dapat memiliki akses gratis ke fungsi yang akan memuat catatan kredit yang sesuai, dan menyuntikkan fungsi itu ke dalam Invoice.

Di sisi lain, kode yang membuat yang sebenarnya baru Invoice hanya akan melewati fungsi yang mengembalikan daftar kosong:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(Sebuah kebiasaan ILazy<out T>bisa menyingkirkan kita dari para pemeran jelek IEnumerable, tapi itu akan menyulitkan diskusi.)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

Saya akan senang mendengar pendapat, preferensi, dan peningkatan Anda!

Timo
sumber
3

Bagi saya ini tampaknya praktik umum terkait OOD yang baik daripada khusus untuk DDD.

Alasan yang bisa saya pikirkan adalah:

  • Pemisahan masalah (Entitas harus dipisahkan dari cara mereka bertahan. Karena mungkin ada beberapa strategi di mana entitas yang sama akan bertahan tergantung pada skenario penggunaan)
  • Secara logis, entitas dapat dilihat pada level di bawah level di mana repositori beroperasi. Komponen level bawah seharusnya tidak memiliki pengetahuan tentang komponen level yang lebih tinggi. Oleh karena itu entri tidak boleh memiliki pengetahuan tentang Gudang.
pengguna1502505
sumber
2

Vernon Vaughn memberikan solusi:

Gunakan repositori atau layanan domain untuk mencari objek dependen sebelum memanggil perilaku agregat. Layanan aplikasi klien dapat mengontrol ini.

Alireza Rahmani Khalili
sumber
Tapi bukan dari Entitas.
ssmith
Dari Vernon Vaughn IDDD sumber: Kalender kelas publik meluas EventSourcedRootEntity {... Kalender publikEntry scheduleCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz
periksa makalahnya @Teimuraz
Alireza Rahmani Khalili
1

Saya belajar kode pemrograman berorientasi objek sebelum semua lapisan gebrakan terpisah ini muncul, dan objek / kelas pertama saya DID memetakan langsung ke database.

Akhirnya, saya menambahkan lapisan perantara karena saya harus bermigrasi ke server database lain. Saya telah melihat / mendengar tentang skenario yang sama beberapa kali.

Saya pikir memisahkan akses data (alias "Repositori") dari logika bisnis Anda, adalah salah satu dari hal-hal itu, yang telah diciptakan kembali beberapa kali, kata buku Desain Domain Driven, membuatnya banyak "noise".

Saat ini saya menggunakan 3 lapisan (GUI, Logika, Akses Data), seperti yang dilakukan banyak pengembang, karena tekniknya bagus.

Memisahkan data, menjadi Repositorylayer (alias Data Accesslayer), dapat dilihat sebagai teknik pemrograman yang baik, bukan hanya aturan, untuk diikuti.

Seperti banyak metodologi, Anda mungkin ingin memulai, dengan TIDAK diimplementasikan, dan pada akhirnya, memperbarui program Anda, begitu Anda memahaminya.

Quote: Iliad tidak sepenuhnya ditemukan oleh Homer, Carmina Burana tidak sepenuhnya ditemukan oleh Carl Orff, dan dalam kedua kasus, orang yang membuat karya orang lain, semuanya bekerja, mendapat pujian ;-)

umlcat
sumber
1
Terima kasih, tapi saya tidak bertanya tentang memisahkan akses data dari logika bisnis - itu adalah hal yang sangat jelas bahwa ada perjanjian yang sangat luas. Saya bertanya tentang mengapa dalam arsitektur DDD seperti S # arp, Entitas tidak diperbolehkan untuk bahkan 'berbicara' ke lapisan akses data. Ini pengaturan yang menarik yang saya belum bisa menemukan banyak diskusi tentang.
kode
0

Apakah ini berasal dari buku Eric Evans Domain Driven Design, atau apakah itu datang dari tempat lain?

Itu barang lama. Buku Eric hanya membuatnya sedikit lebih ramai.

Di mana ada beberapa penjelasan yang bagus untuk alasan di baliknya?

Alasannya sederhana - pikiran manusia menjadi lemah ketika dihadapkan dengan berbagai konteks yang samar-samar terkait. Mereka mengarah pada ambiguitas (Amerika di Amerika Selatan / Utara berarti Amerika Selatan / Utara), ambiguitas mengarah pada pemetaan informasi yang konstan setiap kali pikiran "menyentuhnya" dan itu disimpulkan sebagai produktivitas dan kesalahan yang buruk.

Logika bisnis harus tercermin sejelas mungkin. Kunci asing, normalisasi, pemetaan relasional objek berasal dari domain yang sama sekali berbeda - hal-hal tersebut bersifat teknis, terkait komputer.

Dalam analogi: jika Anda belajar cara menulis, Anda tidak harus terbebani dengan pemahaman di mana pena dibuat, mengapa tinta menempel di kertas, ketika kertas ditemukan dan apa saja penemuan Cina terkenal lainnya.

sunting: Untuk memperjelas: Saya tidak berbicara tentang praktik OO klasik memisahkan akses data ke dalam lapisan terpisah dari logika bisnis - Saya berbicara tentang pengaturan khusus di mana dalam DDD, Entitas tidak seharusnya berbicara dengan data lapisan akses sama sekali (yaitu mereka tidak seharusnya memegang referensi ke objek Repositori)

Alasannya masih sama dengan yang saya sebutkan di atas. Ini dia hanya selangkah lebih maju. Mengapa entitas harus secara persisten tidak tahu apa-apa jika mereka bisa (setidaknya dekat) sepenuhnya? Berkurangnya kekhawatiran yang tidak berhubungan dengan domain yang dimiliki oleh model kami - semakin banyak ruang bernafas yang dimiliki pikiran kami ketika harus menafsirkannya kembali.

Arnis Lapsa
sumber
Baik. Jadi, bagaimana Entity yang benar-benar tidak tahu apa-apa menerapkan Business Logic jika bahkan tidak diizinkan untuk berbicara dengan layer persistence? Apa yang dilakukan ketika perlu melihat nilai dalam entitas lain yang berubah-ubah?
kode sandi
Jika entitas Anda perlu melihat nilai dalam entitas lain yang sewenang-wenang, Anda mungkin memiliki beberapa masalah desain. Mungkin mempertimbangkan untuk memecah kelas sehingga mereka lebih kohesif.
cdaq
0

Untuk mengutip Carolina Lilientahl, "Pola harus mencegah siklus" https://www.youtube.com/watch?v=eJjadzMRQAk , di mana ia merujuk pada dependensi siklus antar kelas. Dalam hal repositori di dalam agregat, ada godaan untuk membuat dependensi siklik dari pengaturan navigasi objek sebagai satu-satunya alasan. Pola yang disebutkan di atas oleh pemrogram, yang direkomendasikan oleh Vernon Vaughn, di mana agregat lain direferensikan oleh id alih-alih instance root, (apakah ada nama untuk pola ini?) Menyarankan alternatif yang mungkin memandu ke solusi lain.

Contoh ketergantungan siklik antar kelas (pengakuan):

(Waktu0): Dua kelas, Sampel dan Sumur, merujuk satu sama lain (ketergantungan siklik). Sumur mengacu pada Sampel, dan Sampel merujuk kembali ke Sumur, karena nyaman (kadang-kadang perulangan sampel, terkadang perulangan semua sumur di piring). Saya tidak bisa membayangkan kasus di mana Sampel tidak akan merujuk kembali ke Sumur tempat ia ditempatkan.

(Time1): Setahun kemudian, banyak kasus penggunaan diimplementasikan .... dan sekarang ada kasus di mana Sampel tidak boleh merujuk kembali ke Sumur yang ditempatkan. Terdapat pelat sementara dalam langkah kerja. Di sini sebuah sumur mengacu pada sampel, yang pada gilirannya merujuk ke sebuah sumur di piring lain. Karena itu, perilaku aneh kadang-kadang terjadi ketika seseorang mencoba menerapkan fitur baru. Butuh waktu untuk menembus.

Saya juga terbantu oleh artikel ini yang disebutkan di atas tentang aspek negatif dari pemuatan malas.

Edvard Englund
sumber
-1

Di dunia ideal, DDD mengusulkan agar Entitas tidak boleh memiliki referensi ke lapisan data. tapi kita tidak hidup di dunia ideal. Domain mungkin perlu merujuk ke objek domain lain untuk logika bisnis yang dengannya mereka mungkin tidak memiliki ketergantungan. Adalah logis bagi entitas untuk merujuk ke lapisan repositori untuk tujuan hanya baca, untuk mengambil nilai.

vsingh
sumber
Tidak, ini memperkenalkan penggabungan yang tidak perlu ke entitas, melanggar SRP dan Pemisahan Kekhawatiran, dan menyulitkan deserialisasi entitas dari kegigihan (karena proses deserialisasi sekarang juga harus menyuntikkan layanan / repositori yang entitas dapatkan).
ssmith