Bagaimana tepatnya seharusnya Perintah CQRS divalidasi dan ditransformasikan ke objek domain?

22

Saya telah mengadaptasi CQRS 1 orang miskin untuk beberapa waktu sekarang karena saya suka fleksibilitasnya untuk memiliki data granular di satu toko data, memberikan kemungkinan besar untuk analisis dan dengan demikian meningkatkan nilai bisnis dan ketika diperlukan yang lain untuk bacaan yang berisi data yang didenormalkan untuk meningkatkan kinerja .

Tetapi sayangnya sejak awal saya telah bergumul dengan masalah di mana tepatnya saya harus menempatkan logika bisnis dalam arsitektur jenis ini.

Dari apa yang saya mengerti, perintah adalah sarana untuk mengomunikasikan niat dan tidak memiliki ikatan dengan domain dengan sendirinya. Mereka pada dasarnya adalah data (bodoh - jika Anda mau) objek transfer. Ini untuk membuat perintah mudah ditransfer antara berbagai teknologi. Hal yang sama berlaku untuk acara sebagai tanggapan terhadap acara yang berhasil diselesaikan.

Dalam aplikasi DDD yang khas, logika bisnis berada di dalam entitas, objek nilai, akar agregat, mereka kaya akan data maupun perilaku. Tetapi perintah bukanlah objek domain sehingga tidak boleh terbatas pada representasi data domain, karena itu terlalu banyak membebani mereka.

Jadi pertanyaan sebenarnya adalah: Di mana tepatnya logikanya?

Saya menemukan bahwa saya cenderung menghadapi perjuangan ini paling sering ketika mencoba untuk membangun agregat yang cukup rumit yang menetapkan beberapa aturan tentang kombinasi nilai-nilainya. Juga, ketika memodelkan objek domain saya suka mengikuti paradigma gagal-cepat , tahu kapan objek mencapai metode itu dalam keadaan valid.

Katakanlah agregat Carmenggunakan dua komponen:

  • Transmission,
  • Engine.

Baik Transmissiondan Engineobjek nilai direpresentasikan sebagai tipe super dan memiliki sub tipe sesuai, Automaticdan Manualtransmisi, atau Petroldan Electricmesin masing-masing.

Dalam domain ini, tinggal sendiri yang berhasil dibuat Transmission, baik itu Automaticatau Manual, atau salah satu dari jenis Engineitu sepenuhnya baik-baik saja. Tetapi Caragregat memperkenalkan beberapa aturan baru, hanya berlaku ketika Transmissiondan Engineobjek digunakan dalam konteks yang sama. Yaitu:

  • Ketika mobil menggunakan Electricmesin, satu-satunya jenis transmisi yang diizinkan adalah Automatic.
  • Ketika mobil menggunakan Petrolmesin, ia mungkin memiliki salah satu tipe Transmission.

Saya bisa menangkap pelanggaran kombinasi komponen ini pada tingkat membuat perintah, tetapi seperti yang telah saya nyatakan sebelumnya, dari apa yang saya pahami yang tidak boleh dilakukan karena perintah itu kemudian akan berisi logika bisnis yang harus dibatasi pada lapisan domain.

Salah satu opsi adalah untuk memindahkan validasi logika bisnis ini ke perintah validator itu sendiri, tetapi ini tampaknya juga tidak benar. Rasanya seperti saya akan mendekonstruksi perintah, memeriksa propertinya diambil menggunakan getter dan membandingkannya dengan validator dan memeriksa hasilnya. Itu menjerit seperti pelanggaran terhadap hukum Demeter bagi saya.

Membuang opsi validasi yang disebutkan karena sepertinya tidak layak, sepertinya seseorang harus menggunakan perintah dan membangun agregat darinya. Tetapi di mana logika ini seharusnya ada? Haruskah itu berada di dalam penangan perintah yang bertanggung jawab untuk menangani perintah konkret? Atau haruskah itu mungkin dalam validator perintah (saya juga tidak suka pendekatan ini)?

Saat ini saya menggunakan perintah dan membuat agregat darinya di dalam penangan perintah yang bertanggung jawab. Tetapi ketika saya melakukan ini, haruskah saya memiliki validator perintah itu tidak akan mengandung apa-apa, karena jika CreateCarperintah itu ada maka akan berisi komponen yang saya tahu valid pada kasus-kasus terpisah tetapi agregat mungkin mengatakan berbeda.


Mari kita bayangkan skenario berbeda yang memadukan proses validasi yang berbeda - membuat pengguna baru menggunakan CreateUserperintah.

Perintah berisi Iddari pengguna yang akan dibuat dan mereka Email.

Sistem menyatakan aturan berikut untuk alamat email pengguna:

  • harus unik,
  • tidak boleh kosong,
  • harus memiliki paling banyak 100 karakter (panjang maksimal kolom db).

Dalam hal ini, walaupun memiliki email unik adalah aturan bisnis, mengeceknya secara agregat sangat tidak masuk akal, karena saya perlu memuat seluruh rangkaian email saat ini di sistem ke memori dan memeriksa email di perintah terhadap agregat ( Eeeek! Sesuatu, sesuatu, kinerja.). Karena itu, saya akan memindahkan pemeriksaan ini ke validator perintah, yang akan menggunakan UserRepositorydependensi dan menggunakan repositori untuk memeriksa apakah pengguna dengan email yang ada dalam perintah sudah ada.

Ketika sampai pada hal ini, tiba-tiba masuk akal untuk memasukkan dua aturan email lainnya ke dalam validator perintah juga. Tapi saya merasa aturan harus benar-benar hadir dalam Useragregat dan bahwa validator perintah hanya harus memeriksa tentang keunikan dan jika validasi berhasil saya harus melanjutkan untuk membuat Useragregat dalam CreateUserCommandHandlerdan meneruskannya ke repositori untuk disimpan.

Saya merasa seperti ini karena metode penyimpanan repositori cenderung menerima agregat yang memastikan bahwa setelah agregat disahkan semua invarian dipenuhi. Ketika logika (mis. Ketidak-kekosongan) hanya hadir dalam validasi perintah itu sendiri programmer lain dapat sepenuhnya melewatkan validasi ini dan memanggil metode save UserRepositorydengan Userobjek secara langsung yang dapat menyebabkan kesalahan database fatal, karena email mungkin memiliki sudah terlalu lama.

Bagaimana Anda secara pribadi menangani validasi dan transformasi yang kompleks ini? Saya sebagian besar senang dengan solusi saya, tetapi saya merasa saya perlu penegasan bahwa ide dan pendekatan saya tidak sepenuhnya bodoh untuk cukup senang dengan pilihan. Saya sepenuhnya terbuka untuk pendekatan yang sama sekali berbeda. Jika Anda memiliki sesuatu yang secara pribadi telah Anda coba dan bekerja dengan sangat baik untuk Anda, saya akan senang melihat solusi Anda.


1 Bekerja sebagai pengembang PHP yang bertanggung jawab untuk menciptakan sistem RESTful interpretasi saya terhadap CQRS sedikit menyimpang dari pendekatan pemrosesan async-command standar , seperti kadang-kadang mengembalikan hasil dari perintah karena kebutuhan memproses perintah secara serempak.

Andy
sumber
Saya butuh beberapa contoh kode. seperti apa objek perintah Anda dan di mana Anda membuatnya?
Ewan
@ Ewan, saya akan menambahkan sampel kode hari ini atau besok. Berangkat untuk perjalanan dalam beberapa menit.
Andy
Menjadi seorang programmer PHP, saya sarankan untuk melihat implementasi CQRS + ES saya: github.com/xprt64/cqrs-es
Constantin Galbenu
@ConstantinGALBENU Jika kita menganggap interpretasi Greg Young tentang CQRS benar (yang seharusnya kita lakukan) maka pemahaman Anda tentang CQRS salah - atau setidaknya implementasi PHP Anda. Perintah tidak akan ditangani oleh agregat secara langsung. Perintah harus ditangani oleh penangan perintah yang dapat menghasilkan perubahan agregat yang kemudian menghasilkan peristiwa yang akan digunakan untuk replikasi negara.
Andy
Saya pikir interpretasi kami tidak berbeda. Anda hanya perlu menggali lebih dalam DDD (pada tingkat taktis Agregat) atau membuka mata Anda lebih lebar. Setidaknya ada dua gaya menerapkan CQRS. Saya menggunakan salah satunya. Implementasi saya lebih menyerupai model Aktor dan membuat lapisan Aplikasi sangat tipis, yang selalu merupakan hal yang baik. Saya mengamati bahwa ada banyak duplikasi kode di dalam layanan aplikasi tersebut dan memutuskan untuk menggantinya dengan a CommandDispatcher.
Constantin Galbenu

Jawaban:

22

Jawaban berikut adalah dalam konteks gaya CQRS yang dipromosikan oleh cqrs.nu di mana perintah tiba langsung pada agregat. Dalam gaya arsitektural ini, layanan aplikasi digantikan oleh komponen infrastruktur ( CommandDispatcher ) yang mengidentifikasi agregat, memuatnya, mengirimkannya perintah dan kemudian melanjutkan agregat (sebagai serangkaian acara jika Sumber pemasukan digunakan).

Jadi pertanyaan sebenarnya adalah: Di mana tepatnya logikanya?

Ada beberapa jenis logika (validasi). Gagasan umum adalah untuk mengeksekusi logika sedini mungkin - gagal cepat jika Anda mau. Jadi, situasinya adalah sebagai berikut:

  • struktur objek perintah itu sendiri; konstruktor perintah memiliki beberapa bidang wajib yang harus ada untuk perintah yang akan dibuat; ini adalah validasi pertama dan tercepat; ini jelas terkandung dalam perintah.
  • validasi bidang level rendah, seperti kekosongan beberapa bidang (seperti nama pengguna) atau format (alamat email yang valid). Validasi semacam ini harus terkandung di dalam perintah itu sendiri, di dalam konstruktor. Ada gaya lain memiliki isValidmetode tetapi ini tampaknya tidak ada gunanya bagi saya karena seseorang harus ingat untuk memanggil metode ini padahal sebenarnya perintah instantiasi yang berhasil sudah cukup.
  • terpisah command validators, kelas yang memiliki tanggung jawab untuk memvalidasi perintah. Saya menggunakan validasi semacam ini ketika saya perlu memeriksa informasi dari beberapa agregat atau sumber eksternal. Anda bisa menggunakan ini untuk memeriksa keunikan nama pengguna. Command validatorsdapat memiliki dependensi yang disuntikkan, seperti repositori. Perlu diingat bahwa validasi ini pada akhirnya konsisten dengan agregat (yaitu ketika pengguna dibuat, pengguna lain dengan nama pengguna yang sama dapat dibuat sementara itu)! Juga, jangan mencoba untuk menempatkan logika di sini yang seharusnya berada di dalam agregat! Validator perintah berbeda dari para manajer Sagas / Proses yang menghasilkan perintah berdasarkan peristiwa.
  • metode agregat yang menerima dan memproses perintah. Ini adalah (jenis) validasi terakhir yang terjadi. Agregat mengekstrak data dari perintah dan menggunakan beberapa logika bisnis inti yang diterimanya (melakukan perubahan pada statusnya) atau menolaknya. Logika ini diperiksa secara konsisten dan kuat. Ini adalah garis pertahanan terakhir. Dalam contoh Anda, aturannya When a car uses Electric engine the only allowed transmission type is Automaticharus diperiksa di sini.

Saya merasa seperti ini karena metode penyimpanan repositori cenderung menerima agregat yang memastikan bahwa setelah agregat disahkan semua invarian dipenuhi. Ketika logika (mis. Kekosongan) hanya hadir dalam validasi perintah itu sendiri programmer lain dapat sepenuhnya melewatkan validasi ini dan memanggil metode save di UserRepository dengan objek Pengguna secara langsung yang dapat menyebabkan kesalahan database fatal, karena email mungkin sudah terlalu lama.

Menggunakan teknik di atas tidak ada yang dapat membuat perintah yang tidak valid atau memotong logika di dalam agregat. Validator perintah secara otomatis dimuat + dipanggil oleh CommandDispatchersehingga tidak ada yang dapat mengirim perintah langsung ke agregat. Seseorang dapat memanggil metode pada agregat yang melewati perintah tetapi tidak dapat bertahan dari perubahan sehingga tidak ada gunanya / tidak berbahaya untuk melakukannya.

Bekerja sebagai pengembang PHP yang bertanggung jawab untuk membuat sistem RESTful, interpretasi saya terhadap CQRS sedikit menyimpang dari pendekatan pemrosesan async-command standar, seperti kadang-kadang mengembalikan hasil dari perintah karena kebutuhan memproses perintah secara serempak.

Saya juga seorang programmer PHP dan saya tidak mengembalikan apa pun dari penangan perintah saya (metode agregat dalam bentuk handleSomeCommand). Namun, saya cukup sering mengembalikan informasi ke klien / browser di HTTP response, misalnya ID dari agregat root yang baru dibuat atau sesuatu dari model-baca tetapi saya tidak pernah mengembalikan (benar - benar tidak pernah ) apa pun dari metode perintah agregat saya. Fakta sederhana bahwa perintah itu diterima (dan diproses - kita berbicara tentang pemrosesan PHP sinkron, kan ?!) sudah cukup.

Kami mengembalikan sesuatu ke browser (dan masih mengerjakan buku CQRS ) karena CQRS bukan arsitektur tingkat tinggi .

Contoh cara kerja validator perintah:

Jalur komando melalui validator perintah dalam perjalanannya ke Agregat

Constantin Galbenu
sumber
Sehubungan dengan strategi validasi Anda, poin nomor dua melompat ke arah saya sebagai tempat di mana logika akan sering diduplikasi. Tentunya orang ingin agregat Pengguna memvalidasi email yang tidak kosong dan terbentuk dengan baik juga tidak? Ini menjadi jelas ketika kami memperkenalkan perintah ChangeEmail.
slide sisi raja
@ king-side-slide tidak jika Anda memiliki EmailAddressobjek nilai yang memvalidasi dirinya sendiri.
Constantin Galbenu
Itu sepenuhnya benar. Seseorang dapat merangkum sebuah EmailAddressuntuk mengurangi duplikasi. Namun yang lebih penting, dengan melakukan itu Anda juga akan memindahkan logika dari perintah Anda ke domain Anda. Perlu dicatat bahwa ini bisa terlalu jauh. Seringkali pengetahuan yang serupa (objek bernilai) mungkin memiliki persyaratan validasi yang berbeda tergantung pada siapa yang menggunakannya. EmailAddressadalah contoh yang nyaman karena seluruh konsepsi nilai ini memiliki persyaratan validasi global.
slide sisi raja
Demikian pula, gagasan "validator perintah" tampaknya tidak perlu. Tujuannya bukan untuk mencegah perintah yang tidak valid dibuat dan dikirim. Tujuannya adalah untuk mencegah mereka mengeksekusi. Misalnya, saya bisa meneruskan data apa pun yang saya inginkan dengan URL. Jika tidak valid, sistem menolak permintaan saya. Perintah masih dibuat dan dikirim. Jika suatu perintah membutuhkan beberapa agregat untuk validasi (yaitu kumpulan Pengguna untuk memeriksa keunikan email), layanan domain lebih sesuai. Objek seperti "x validator" sering merupakan tanda dari model anemia di mana data dipisahkan dari perilaku.
slide sisi raja
1
@ king-side-slide Contoh konkretnya adalah UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Anda dapat melihat bahwa ini adalah domain terpisah dari Pesanan sehingga tidak dapat divalidasi oleh OrderAggregate itu sendiri.
Constantin Galbenu
6

Satu premis mendasar dari DDD adalah bahwa model domain memvalidasi diri mereka sendiri. Ini adalah konsep penting karena mengangkat domain Anda sebagai pihak yang bertanggung jawab untuk memastikan aturan bisnis Anda ditegakkan. Itu juga membuat model domain Anda sebagai fokus pengembangan.

Sistem CQRS (seperti yang Anda tunjukkan dengan benar) adalah detail implementasi yang mewakili sub-domain umum yang mengimplementasikan mekanisme kohesifnya sendiri. Model Anda seharusnya tidak bergantung pada infrastruktur CQRS apa pun untuk berperilaku sesuai dengan aturan bisnis Anda. Tujuan DDD adalah untuk memodelkan perilaku suatu sistem sedemikian rupa sehingga hasilnya adalah abstraksi yang berguna dari persyaratan fungsional domain bisnis inti Anda. Memindahkan setiap bagian dari perilaku ini dari model Anda, betapapun menggoda, mengurangi integritas dan kohesi model Anda (dan membuatnya kurang berguna).

Cukup dengan memperluas contoh Anda untuk memasukkan ChangeEmailperintah, kami dapat dengan sempurna menggambarkan mengapa Anda tidak ingin ada logika bisnis Anda dalam infrastruktur perintah Anda karena Anda perlu menduplikasi aturan Anda:

  • email tidak boleh kosong
  • email tidak boleh lebih dari 100 karakter
  • email harus unik

Jadi sekarang kita bisa yakin bahwa logika kita perlu berada di domain kita, mari kita atasi masalah "di mana". Dua aturan pertama dapat dengan mudah diterapkan pada Useragregat kita , tetapi aturan terakhir itu sedikit lebih bernuansa; yang membutuhkan pengetahuan lebih lanjut untuk mendapatkan wawasan yang lebih dalam. Di permukaan, sepertinya aturan ini berlaku untuk a User, tetapi sebenarnya tidak. "Keunikan" email berlaku untuk koleksi Users(sesuai dengan cakupan tertentu).

Ah ha! Dengan pemikiran itu, menjadi sangat jelas bahwa Anda UserRepository(koleksi in-memory Anda Users) mungkin menjadi kandidat yang lebih baik untuk menegakkan invarian ini. Metode "save" kemungkinan merupakan tempat paling masuk akal untuk memasukkan cek (tempat Anda dapat memberikan UserEmailAlreadyExistspengecualian). Atau, sebuah domain UserServicedapat dibuat bertanggung jawab untuk membuat yang baru Usersdan memperbarui atributnya.

Gagal cepat adalah pendekatan yang baik, tetapi hanya bisa dilakukan di mana dan kapan itu cocok dengan sisa model. Mungkin sangat menggoda untuk memeriksa parameter pada metode layanan aplikasi (atau perintah) sebelum memproses lebih lanjut dalam upaya untuk menangkap kegagalan ketika Anda (pengembang) tahu panggilan akan gagal di suatu tempat lebih dalam dalam proses. Tetapi dengan melakukan hal itu, Anda akan memiliki duplikat (dan kebocoran) pengetahuan dengan cara yang kemungkinan akan membutuhkan lebih dari satu pembaruan pada kode ketika aturan bisnis berubah.

slide sisi raja
sumber
2
Saya setuju dengan ini. Bacaan saya sampai sekarang (tanpa CQRS) memberi tahu saya bahwa validasi harus selalu masuk dalam model domain untuk melindungi invarian. Sekarang saya membaca CQRS, ia memberitahu saya untuk meletakkan validasi di objek Command. Ini tampaknya kontra intuitif. Apakah Anda mengetahui contoh-contoh misalnya pada GitHub di mana validasi dimasukkan ke dalam Model Domain alih-alih Perintah? +1.
w0051977