Apakah ada cara yang elegan untuk memeriksa batasan unik pada atribut objek domain tanpa memindahkan logika bisnis ke lapisan layanan?

10

Saya telah mengadaptasi desain berbasis domain selama sekitar 8 tahun sekarang dan bahkan setelah bertahun-tahun, masih ada satu hal, yang telah mengganggu saya. Itu memeriksa catatan unik dalam penyimpanan data terhadap objek domain.

Pada September 2013 Martin Fowler menyebutkan prinsip TellDon'tAsk , yang, jika mungkin, harus diterapkan ke semua objek domain, yang kemudian harus mengembalikan pesan, bagaimana operasi berjalan (dalam desain berorientasi objek ini sebagian besar dilakukan melalui pengecualian, ketika operasi tidak berhasil).

Proyek saya biasanya dibagi menjadi banyak bagian, di mana dua di antaranya adalah Domain (berisi aturan bisnis dan tidak ada yang lain, domain tersebut benar-benar tidak peduli) dan Layanan. Layanan yang mengetahui tentang lapisan repositori yang digunakan untuk data CRUD.

Karena keunikan atribut yang dimiliki suatu objek adalah aturan domain / bisnis, itu harus panjang untuk modul domain, jadi aturannya persis di tempat seharusnya.

Untuk dapat memeriksa keunikan catatan, Anda perlu meminta dataset saat ini, biasanya database, untuk mencari tahu, apakah catatan lain dengan katakanlah Namesudah ada.

Mempertimbangkan lapisan domain adalah kegigihan dan tidak tahu bagaimana cara mengambil data tetapi hanya bagaimana melakukan operasi pada mereka, itu tidak dapat benar-benar menyentuh repositori itu sendiri.

Desain yang saya adaptasi terlihat seperti ini:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

Hal ini menyebabkan layanan menentukan aturan domain daripada lapisan domain itu sendiri dan Anda memiliki aturan yang tersebar di beberapa bagian kode Anda.

Saya menyebutkan prinsip TellDon'tAsk, karena seperti yang Anda lihat dengan jelas, layanan ini menawarkan tindakan (baik menyimpan Productatau melempar pengecualian), tetapi di dalam metode ini Anda mengoperasikan objek dengan menggunakan pendekatan prosedural.

Solusi yang jelas adalah membuat Domain.ProductCollectionkelas dengan Add(Domain.Product)metode melempar ProductWithSKUAlreadyExistsException, tetapi kurang dalam kinerja banyak, karena Anda akan perlu mendapatkan semua Produk dari penyimpanan data untuk mengetahui dalam kode, apakah suatu Produk sudah memiliki SKU yang sama sebagai Produk yang ingin Anda tambahkan.

Bagaimana kalian memecahkan masalah khusus ini? Ini bukan masalah sebenarnya, saya telah memiliki lapisan layanan yang mewakili aturan domain tertentu selama bertahun-tahun. Lapisan layanan biasanya juga melayani operasi domain yang lebih kompleks, saya hanya ingin tahu apakah Anda telah menemukan solusi yang lebih baik, lebih terpusat, selama karir Anda.

Andy
sumber
Mengapa menarik seluruh daftar nama ketika Anda hanya bisa meminta satu dan mendasarkan logika pada apakah atau tidak ditemukan? Anda memberi tahu lapisan ketekunan apa yang harus dilakukan dan tidak bagaimana melakukannya selama ada kesepakatan dengan antarmuka.
JeffO
1
Saat menggunakan istilah Layanan, kita harus berusaha lebih jernih, seperti perbedaan antara layanan domain di lapisan domain dan layanan aplikasi di lapisan aplikasi. Lihat gorodinski.com/blog/2012/04/14/…
Erik Eidt
Artikel Excelent, @ErikEidt, terima kasih. Saya kira desain saya tidak salah kalau begitu, jika saya bisa mempercayai Tn. Gorodinski, dia cukup banyak mengatakan hal yang sama: Solusi yang lebih baik adalah meminta layanan aplikasi mengambil informasi yang dibutuhkan oleh suatu entitas, secara efektif mengatur lingkungan eksekusi, dan menyediakan ke entitas, dan memiliki keberatan yang sama terhadap menyuntikkan repositori ke dalam model Domain langsung seperti saya, sebagian besar melalui melanggar SRP.
Andy
Kutipan dari referensi "katakan, jangan tanya": "Tapi secara pribadi, saya tidak menggunakan kirim-jangan-tanya."
radarbob

Jawaban:

7

Mempertimbangkan lapisan domain adalah kegigihan dan tidak tahu bagaimana cara mengambil data tetapi hanya bagaimana melakukan operasi pada mereka, itu tidak dapat benar-benar menyentuh repositori itu sendiri.

Saya tidak setuju dengan bagian ini. Terutama kalimat terakhir.

Meskipun benar bahwa domain harus tetap bodoh, ia tahu bahwa ada "Koleksi entitas domain". Dan bahwa ada aturan domain yang menyangkut koleksi ini secara keseluruhan. Keunikan menjadi salah satunya. Dan karena implementasi logika aktual sangat bergantung pada mode persistensi spesifik, harus ada semacam abstraksi dalam domain yang menentukan kebutuhan untuk logika ini.

Jadi sesederhana membuat antarmuka yang dapat meminta jika nama sudah ada, yang kemudian diimplementasikan di penyimpanan data Anda dan dipanggil oleh siapa pun yang perlu tahu apakah nama itu unik.

Dan saya ingin menekankan bahwa repositori adalah layanan DOMAIN. Mereka adalah abstraksi di sekitar kegigihan. Ini adalah implementasi repositori yang harus dipisahkan dari domain. Sama sekali tidak ada yang salah dengan entitas domain yang memanggil layanan domain. Tidak ada yang salah dengan satu entitas dapat menggunakan repositori untuk mengambil entitas lain atau mengambil beberapa informasi tertentu, yang tidak dapat dengan mudah disimpan dalam memori. Ini adalah alasan mengapa Repositori adalah konsep kunci dalam buku Evans .

Euforia
sumber
Terima kasih atas masukannya. Bisakah repositori benar-benar mewakili yang Domain.ProductCollectionsaya pikirkan, mengingat mereka bertanggung jawab untuk mengambil objek dari lapisan Domain?
Andy
@ DavidPacker Saya tidak begitu mengerti apa yang Anda maksud. Karena seharusnya tidak perlu menyimpan semua item dalam memori. Implementasi metode "DoesNameExist" harus (paling mungkin) query SQL di sisi penyimpanan data.
Euforia
Apa yang dimaksud adalah, alih-alih menyimpan data dalam koleksi di memori, ketika saya ingin mengetahui semua Produk yang saya tidak perlu mendapatkannya Domain.ProductCollectiontetapi meminta repositori sebagai gantinya, sama dengan bertanya, apakah Domain.ProductCollectionberisi Produk dengan melewati SKU, kali ini, sekali lagi, meminta repositori sebagai gantinya (ini sebenarnya adalah contoh), yang alih-alih mengulangi produk-produk yang dimuat sebelumnya meminta basis data yang mendasarinya. Saya tidak bermaksud menyimpan semua Produk dalam memori kecuali saya harus, melakukannya akan menjadi omong kosong.
Andy
Yang mengarah ke pertanyaan lain, haruskah repositori tahu, apakah atribut harus unik? Saya selalu menerapkan repositori sebagai komponen yang cukup bodoh, menyimpan apa yang Anda lewati untuk disimpan dan mencoba untuk mengambil data berdasarkan kondisi yang disahkan dan memasukkan keputusan ke dalam layanan.
Andy
@ Davidvider Yah, "pengetahuan domain" ada di antarmuka. Antarmuka mengatakan "domain membutuhkan fungsionalitas ini", dan penyimpanan data kemudian menyediakan fungsionalitas itu. Jika Anda memiliki IProductRepositoryantarmuka DoesNameExist, jelas apa yang harus dilakukan metode ini. Tetapi memastikan bahwa Produk tidak dapat memiliki nama yang sama dengan produk yang ada harus berada di domain di suatu tempat.
Euforia
4

Anda perlu membaca Greg Young pada set validasi .

Jawaban singkat: sebelum Anda melangkah terlalu jauh ke sarang tikus, Anda harus memastikan bahwa Anda memahami nilai persyaratan dari perspektif bisnis. Betapa mahalnya, untuk mendeteksi dan mengurangi duplikasi itu, daripada mencegahnya?

Masalah dengan persyaratan "keunikan" adalah, yah, sangat sering ada alasan mendasar mengapa orang menginginkannya - Yves Reynhout

Jawaban yang lebih panjang: Saya telah melihat menu kemungkinan, tetapi mereka semua memiliki pengorbanan.

Anda dapat memeriksa duplikat sebelum mengirim perintah ke domain. Ini dapat dilakukan di klien, atau di layanan (contoh Anda menunjukkan teknik). Jika Anda tidak puas dengan logika yang bocor dari lapisan domain, Anda dapat mencapai hasil yang sama dengan a DomainService.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

Tentu saja, dilakukan dengan cara ini implementasi DeduplicationService akan perlu tahu sesuatu tentang cara mencari skus yang ada. Jadi sementara itu mendorong beberapa pekerjaan kembali ke domain, Anda masih dihadapkan dengan masalah dasar yang sama (membutuhkan jawaban untuk validasi yang ditetapkan, masalah dengan kondisi balapan).

Anda dapat melakukan validasi di lapisan kegigihan Anda sendiri. Database relasional sangat bagus dalam menetapkan validasi. Beri batasan keunikan pada kolom sku produk Anda, dan Anda siap melakukannya. Aplikasi hanya menyimpan produk ke dalam repositori, dan Anda mendapatkan pelanggaran kendala yang muncul kembali jika ada masalah. Jadi kode aplikasi terlihat bagus, dan kondisi balapan Anda dieliminasi, tetapi Anda memiliki "domain" peraturan bocor.

Anda dapat membuat agregat terpisah di domain Anda yang mewakili set skus yang dikenal. Saya dapat memikirkan dua variasi di sini.

Salah satunya adalah seperti ProductCatalog; produk ada di tempat lain, tetapi hubungan antara produk dan skus dikelola oleh katalog yang menjamin keunikan sku. Bukan berarti ini menyiratkan bahwa produk tidak memiliki skus; skus ditugaskan oleh ProductCatalog (jika Anda perlu skus menjadi unik, Anda mencapainya dengan hanya memiliki agregat ProductCatalog tunggal). Tinjau bahasa di mana-mana dengan pakar domain Anda - jika hal seperti itu ada, ini bisa menjadi pendekatan yang tepat.

Alternatif adalah sesuatu yang lebih mirip layanan reservasi sku. Mekanisme dasarnya sama: sebuah agregat tahu tentang semua skus, sehingga dapat mencegah pengenalan duplikat. Tetapi mekanismenya sedikit berbeda: Anda memperoleh sewa sku sebelum menugaskannya ke suatu produk; saat membuat produk, Anda menyerahkannya ke sku. Masih ada kondisi balapan dalam permainan (agregat yang berbeda, karena itu transaksi berbeda), tapi rasanya berbeda. Kelemahan nyata di sini adalah bahwa Anda memproyeksikan ke dalam model domain layanan leasing tanpa benar-benar memiliki pembenaran dalam bahasa domain.

Anda dapat menarik semua entitas produk menjadi satu agregat - yaitu, katalog produk yang dijelaskan di atas. Anda benar-benar mendapatkan keunikan skus ketika Anda melakukan ini, tetapi biayanya adalah pertengkaran tambahan, memodifikasi produk apa pun benar-benar berarti memodifikasi seluruh katalog.

Saya tidak suka kebutuhan untuk menarik semua SKU produk dari database untuk melakukan operasi di-memori.

Mungkin Anda tidak perlu melakukannya. Jika Anda menguji sku dengan filter Bloom , Anda dapat menemukan banyak skus unik tanpa memuat set sama sekali.

Jika use case Anda memungkinkan Anda untuk arbitrer tentang skus mana yang Anda tolak, Anda dapat menyadap semua positif palsu (bukan masalah besar jika Anda mengizinkan klien untuk menguji skus yang mereka usulkan sebelum mengirimkan perintah). Itu akan memungkinkan Anda untuk menghindari memuat set ke memori.

(Jika Anda ingin lebih menerima, Anda dapat mempertimbangkan untuk malas memuat skus jika terjadi kecocokan dalam filter bloom; Anda masih berisiko mengambil semua skus ke dalam memori kadang-kadang, tetapi seharusnya tidak menjadi kasus umum jika Anda mengizinkan kode klien untuk memeriksa perintah untuk kesalahan sebelum mengirim).

VoiceOfUnreason
sumber
Saya tidak suka kebutuhan untuk menarik semua SKU produk dari database untuk melakukan operasi di-memori. Ini solusi jelas yang saya sarankan dalam pertanyaan awal dan mempertanyakan kinerjanya. Juga, IMO, bergantung pada batasan dalam DB Anda, untuk bertanggung jawab atas keunikan, adalah buruk. Jika Anda beralih ke mesin database baru dan entah bagaimana kendala unik hilang selama transformasi, Anda telah melanggar kode, karena informasi database yang ada di sana sudah tidak ada lagi. Pokoknya terima kasih atas tautannya, baca menarik.
Andy
Mungkin filter Bloom (lihat edit).
VoiceOfUnreason