Bagaimana memperlakukan validasi referensi antar agregat?


11

Saya sedikit kesulitan dengan referensi antar agregat. Mari kita asumsikan agregat Carmemiliki referensi ke agregat Driver. Referensi ini akan dimodelkan dengan memiliki Car.driverId.

Sekarang masalah saya adalah seberapa jauh saya harus memvalidasi pembuatan Caragregat di CarFactory. Haruskah saya percaya bahwa yang lulus DriverIdmerujuk ke yang sudah ada Driver atau haruskah saya memeriksa invarian itu?

Untuk memeriksa, saya melihat dua kemungkinan:

  • Saya bisa mengubah tanda tangan pabrik mobil untuk menerima entitas pengemudi yang lengkap. Pabrik kemudian akan memilih id dari entitas itu dan membangun mobil dengan itu. Di sini invarian diperiksa secara implisit.
  • Saya dapat memiliki referensi DriverRepositorydalam CarFactorydan secara eksplisit menelepon driverRepository.exists(driverId).

Tapi sekarang saya bertanya-tanya bukankah itu terlalu banyak memeriksa invarian? Saya dapat membayangkan bahwa agregat-agregat itu mungkin hidup dalam konteks terikat yang terpisah, dan sekarang saya akan mencemari BC mobil dengan ketergantungan pada DriverRepository atau entitas Driver dari driver BC.

Juga, jika saya akan berbicara dengan para ahli domain, mereka tidak akan pernah mempertanyakan validitas referensi tersebut. Saya merasa bahwa saya mencemari model domain saya dengan masalah yang tidak terkait. Tetapi sekali lagi, pada titik tertentu input pengguna harus divalidasi.

Jawaban:


6

Saya bisa mengubah tanda tangan pabrik mobil untuk menerima entitas pengemudi yang lengkap. Pabrik kemudian akan memilih id dari entitas itu dan membangun mobil dengan itu. Di sini invarian diperiksa secara implisit.

Pendekatan ini menarik karena Anda mendapatkan cek gratis dan selaras dengan bahasa di mana-mana. A Cartidak didorong oleh a driverId, tetapi oleh a Driver.

Pendekatan ini sebenarnya digunakan oleh Vaughn Vernon dalam konteks terikat sampel Identitas & Akses di mana ia meneruskan Useragregat ke Groupagregat, tetapi Grouphanya memegang tipe nilai GroupMember. Seperti yang Anda lihat ini juga memungkinkan dia untuk memeriksa pemberdayaan pengguna (kami sangat sadar bahwa cek mungkin basi).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

Namun, dengan melewatkan Driverinstance Anda juga membuka diri Anda untuk modifikasi bagian Driverdalam yang tidak disengaja Car. Melewati referensi nilai membuatnya lebih mudah untuk beralasan tentang perubahan dari sudut pandang programmer, tetapi pada saat yang sama, DDD adalah semua tentang Bahasa yang Dapat Dideteksi, jadi mungkin itu sepadan dengan risikonya.

Jika Anda benar-benar dapat menghasilkan nama-nama yang bagus untuk menerapkan Prinsip Segregasi Antarmuka (ISP) maka Anda dapat mengandalkan antarmuka yang tidak memiliki metode perilaku. Anda mungkin bisa juga datang dengan konsep objek nilai yang mewakili referensi driver yang tidak berubah dan yang hanya bisa dipakai dari driver yang ada (misalnya DriverDescriptor driver = driver.descriptor()).

Saya dapat membayangkan bahwa agregat-agregat itu mungkin hidup dalam konteks terikat yang terpisah, dan sekarang saya akan mencemari BC mobil dengan ketergantungan pada DriverRepository atau entitas Driver dari driver BC.

Tidak, sebenarnya tidak. Selalu ada lapisan anti korupsi untuk memastikan bahwa konsep dari satu konteks tidak akan berdarah ke konteks lainnya. Sebenarnya jauh lebih mudah jika Anda memiliki BC yang didedikasikan untuk asosiasi pengemudi mobil karena Anda dapat memodelkan konsep yang ada seperti Cardan Driversecara khusus untuk konteks itu.

Oleh karena itu, Anda mungkin memiliki DriverLookupServicedefinisi dalam BC yang bertanggung jawab untuk mengelola asosiasi pengemudi mobil. Layanan ini dapat memanggil layanan web yang diekspos oleh konteks Manajemen Pengemudi yang mengembalikan Driverinstance yang kemungkinan besar akan menjadi objek nilai dalam konteks ini.

Perhatikan bahwa layanan web tidak selalu merupakan metode integrasi terbaik antara BC. Anda juga bisa mengandalkan pesan di mana misalnya UserCreatedpesan dari konteks Manajemen Driver akan dikonsumsi dalam konteks jarak jauh yang akan menyimpan representasi driver di DB itu sendiri. Kemudian DriverLookupServicedapat menggunakan DB ini dan data pengemudi akan tetap up to date dengan pesan lebih lanjut (misalnya DriverLicenceRevoked).

Saya tidak bisa memberi tahu Anda pendekatan mana yang lebih baik untuk domain Anda, tetapi mudah-mudahan ini akan memberi Anda wawasan yang cukup untuk membuat keputusan.


3

Cara Anda mengajukan pertanyaan (dan mengusulkan dua alternatif) seolah-olah satu-satunya kekhawatiran adalah bahwa driverId masih berlaku pada saat mobil dibuat.

Namun, Anda juga harus khawatir bahwa pengemudi yang terkait dengan driverId tidak dihapus sebelum mobil dihapus atau diberikan driver lain (dan mungkin juga bahwa pengemudi tidak ditugaskan ke mobil lain (ini jika domain membatasi pengemudi hanya untuk dikaitkan dengan satu mobil)).

Saya menyarankan agar alih-alih validasi, Anda mengalokasikan (yang akan mencakup validasi kehadiran). Anda kemudian akan melarang penghapusan saat masih dialokasikan, sehingga menjaga terhadap kondisi ras data basi selama konstruksi, serta masalah jangka panjang lainnya. (Perhatikan bahwa alokasi memvalidasi dan menandai yang dialokasikan, dan beroperasi secara atomik.)

Btw, saya setuju dengan @PriceJones bahwa hubungan antara mobil dan pengemudi mungkin merupakan tanggung jawab yang terpisah dari mobil atau pengemudi. Asosiasi semacam ini hanya akan tumbuh dalam kompleksitas dari waktu ke waktu, karena kedengarannya seperti masalah penjadwalan (driver, mobil, slot waktu / jendela, pengganti, dll ...) Bahkan jika lebih seperti masalah registrasi, orang mungkin ingin historis pendaftaran serta pendaftaran saat ini. Dengan demikian, itu mungkin layak BC sendiri.

Anda dapat memberikan skema alokasi (seperti jumlah boolean atau referensi) dalam BC dari entitas agregat yang dialokasikan, atau dalam BC terpisah, katakanlah, yang bertanggung jawab untuk membuat hubungan antara mobil & pengemudi. Jika Anda melakukan yang pertama, Anda dapat mengizinkan operasi penghapusan (valid) yang dikeluarkan untuk mobil atau pengemudi BC; jika Anda melakukan yang terakhir, Anda harus mencegah penghapusan dari mobil & driver BC dan alih-alih mengirimkannya melalui penjadwal asosiasi mobil & pengemudi.

Anda juga dapat membagi beberapa tanggung jawab alokasi antara BC sebagai berikut. Car & driver BC masing-masing menyediakan skema "alokasi" yang memvalidasi dan menetapkan boolean yang dialokasikan dengan BC itu; ketika alokasi boolean mereka ditetapkan, BC mencegah penghapusan entitas yang sesuai. (Dan sistemnya diatur sehingga BC pengemudi & mobil hanya mengizinkan alokasi dan deallokasi dari penjadwalan asosiasi pengemudi / pengemudi BC.)

Penjadwalan car & driver BC kemudian menyimpan kalender pengemudi yang terkait dengan mobil untuk beberapa periode / jangka waktu, sekarang dan masa depan, dan memberitahukan BC lain tentang deallokasi hanya pada penggunaan terakhir dari mobil atau pengemudi yang dijadwalkan.


Sebagai solusi yang lebih radikal, Anda dapat memperlakukan mobil & pengemudi BC sebagai pabrik catatan sejarah yang hanya ditambahkan, meninggalkan kepemilikan pada penjadwal asosiasi mobil / pengemudi. Mobil BC dapat menghasilkan mobil baru, lengkap dengan semua detail mobil, bersama dengan VIN-nya. Kepemilikan mobil ditangani oleh penjadwal asosiasi mobil / pengemudi. Bahkan jika asosiasi mobil / pengemudi dihapus, dan mobil itu sendiri dihancurkan, catatan mobil masih ada dalam mobil BC menurut definisi, dan kita dapat menggunakan BC mobil untuk mencari data historis; sementara asosiasi / kepemilikan mobil / pengemudi (masa lalu, sekarang, dan berpotensi dijadwalkan di masa depan) sedang ditangani oleh BC lain.


2

Mari kita asumsikan Mobil agregat memiliki referensi ke Driver agregat. Referensi ini akan dimodelkan dengan memiliki Car.driverId.

Yup, itu cara yang tepat untuk memasangkan satu agregat ke agregat lain.

jika saya akan berbicara dengan pakar domain, mereka tidak akan pernah mempertanyakan validitas referensi tersebut

Bukan pertanyaan yang tepat untuk diajukan ke pakar domain Anda. Coba "berapa biaya untuk bisnis jika pengemudi tidak ada?"

Saya mungkin tidak akan menggunakan DriverRepository untuk memeriksa driverId. Sebagai gantinya, saya akan menggunakan layanan domain untuk melakukannya. Saya pikir itu melakukan pekerjaan yang lebih baik untuk mengekspresikan maksud - di bawah selimut, layanan domain masih memeriksa sistem catatan.

Jadi sesuatu seperti itu

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

Anda sebenarnya menanyakan domain tentang driverId di sejumlah titik berbeda

  • Dari klien, sebelum mengirim perintah
  • Dalam aplikasi, sebelum meneruskan perintah ke model
  • Dalam model domain, selama pemrosesan perintah

Setiap atau semua pemeriksaan ini dapat mengurangi kesalahan dalam input pengguna. Tetapi mereka semua bekerja dari data basi; agregat lainnya dapat berubah segera setelah kami mengajukan pertanyaan. Jadi selalu ada beberapa bahaya negatif palsu / positif.

  • Dalam laporan pengecualian, jalankan setelah perintah selesai

Di sini, Anda masih bekerja dengan data basi (agregat mungkin menjalankan perintah saat Anda menjalankan laporan, Anda mungkin tidak dapat melihat tulisan terbaru untuk semua agregat). Tetapi pemeriksaan antara agregat tidak akan pernah sempurna (Car.create (driver: 7) berjalan pada saat yang sama dengan Driver.delete (driver: 7)) Jadi ini memberi Anda lapisan pertahanan ekstra terhadap risiko.


1
Driver.deleteseharusnya tidak ada. Saya tidak pernah benar-benar melihat domain tempat agregat dihancurkan. Dengan menjaga AR di sekitar Anda tidak pernah bisa berakhir dengan anak yatim.
plalx

1

Mungkin bisa membantu untuk bertanya: Apakah Anda yakin mobil dibangun dengan pengemudi? Saya belum pernah mendengar tentang mobil yang terdiri dari seorang pengemudi di dunia nyata. Alasan mengapa pertanyaan ini penting adalah karena ini mungkin mengarahkan Anda ke arah pembuatan mobil dan pengemudi secara mandiri dan kemudian menciptakan beberapa mekanisme eksternal yang menetapkan pengemudi ke sebuah mobil. Mobil dapat eksis tanpa referensi pengemudi dan tetap menjadi mobil yang valid.

Jika mobil benar-benar harus memiliki pengemudi dalam konteks Anda, maka Anda mungkin ingin mempertimbangkan pola pembangun. Pola ini akan bertanggung jawab untuk memastikan mobil dibangun dengan driver yang ada. Pabrik-pabrik akan melayani mobil dan pengemudi yang divalidasi secara independen, tetapi pembangun akan memastikan mobil memiliki referensi yang dibutuhkan sebelum melayani mobil.


Saya berpikir tentang hubungan mobil / pengemudi juga - tetapi memperkenalkan agregat DriverAssignment hanya bergerak yang referensi perlu divalidasi.
VoiceOfUnreason

1

Tapi sekarang saya bertanya-tanya bukankah itu terlalu banyak memeriksa invarian?

Aku pikir begitu. Mengambil DriverId yang diberikan dari DB mengembalikan set kosong jika tidak ada. Jadi memeriksa hasil kembali membuat bertanya apakah itu ada (dan kemudian mengambil) tidak perlu.

Maka desain kelas membuatnya tidak perlu juga

  • Jika ada persyaratan "mobil yang diparkir mungkin atau mungkin tidak memiliki sopir"
  • Jika objek Driver memerlukan a DriverIddan diatur dalam konstruktor.
  • Jika Carkebutuhan hanya itu DriverId, punya Driver.Idrajin rajin. Tidak ada setter.

Repositori bukan tempat untuk aturan bisnis

  • A Carpeduli jika memiliki Driver(atau ID-nya setidaknya). A Driverpeduli jika ia memiliki DriverId. The Repositorypeduli tentang integritas data dan tidak peduli tentang mobil driver-kurang.
  • DB akan memiliki aturan integritas data. Kunci non-nol, batasan bukan-nol, dll. Tetapi integritas data adalah tentang skema data / tabel, bukan aturan bisnis. Kami memiliki hubungan simbiosis yang sangat berkorelasi dalam kasus ini tetapi jangan mencampur keduanya.
  • Fakta bahwa a DriverIdadalah hal domain bisnis ditangani di kelas yang sesuai.

Pemisahan Kekhawatiran Kekerasan

... terjadi ketika Repository.DriverIdExists()mengajukan pertanyaan.

Bangun objek domain. Jika tidak Drivermaka mungkin objek DriverInfo(hanya DriverIddan Name, katakanlah). Itu DriverIddivalidasi setelah konstruksi. Itu harus ada, dan menjadi tipe yang tepat, dan apa pun yang lainnya. Maka itu adalah masalah desain kelas klien bagaimana menangani driver / driverId yang tidak ada.

Mungkin Carbaik-baik saja tanpa sopir sampai Anda menelepon Car.Drive(). Dalam hal Carobjek tentu saja memastikan keadaannya sendiri. Tidak bisa mengemudi tanpa Driver- well, belum cukup.

Memisahkan properti dari kelasnya buruk

Tentu, miliki Car.DriverIdjika Anda mau. Tetapi seharusnya terlihat seperti ini:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

Bukan ini:

public class Car {
    public int DriverId {get; protected set;}
}

Sekarang yang Carharus berurusan dengan semua DriverIdmasalah validitas - pelanggaran prinsip tanggung jawab tunggal; dan kode redundan mungkin.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.