Di mana kita harus meletakkan validasi untuk model domain

38

Saya masih mencari praktik terbaik untuk validasi model domain. Apakah itu baik untuk meletakkan validasi dalam konstruktor model domain? contoh validasi model domain saya sebagai berikut:

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

Terima kasih atas semua saran Anda.

adisembiring
sumber

Jawaban:

47

Ada artikel yang menarik dari Martin Fowler tentang hal itu yang menyoroti aspek yang cenderung diabaikan oleh kebanyakan orang (termasuk saya):

Tapi satu hal yang saya pikir terus-menerus membuat orang tersandung adalah ketika mereka berpikir validitas objek pada konteks yang independen seperti metode isValid.

Saya pikir jauh lebih berguna untuk menganggap validasi sebagai sesuatu yang terikat pada konteks - biasanya tindakan yang ingin Anda lakukan. Apakah pesanan ini valid untuk diisi, apakah pelanggan ini valid untuk check-in ke hotel. Jadi daripada memiliki metode seperti isValid memiliki metode seperti isValidForCheckIn.

Dari sini berikut bahwa konstruktor tidak boleh melakukan validasi, kecuali mungkin beberapa pemeriksaan kewarasan dasar yang dibagikan oleh semua konteks.

Lagi dari artikel:

Dalam Tentang Wajah, Alan Cooper menganjurkan agar kita tidak membiarkan gagasan kami tentang status yang berlaku mencegah pengguna memasukkan (dan menyimpan) informasi yang tidak lengkap. Saya teringat akan hal ini beberapa hari yang lalu ketika membaca draft buku yang sedang dikerjakan oleh Jimmy Nilsson. Dia menyatakan prinsip bahwa Anda harus selalu dapat menyimpan objek, meskipun memiliki kesalahan di dalamnya. Meskipun saya tidak yakin bahwa ini harus menjadi aturan mutlak, saya pikir orang cenderung mencegah menabung lebih dari yang seharusnya. Memikirkan konteks untuk validasi dapat membantu mencegah hal itu.

Michael Borgwardt
sumber
Syukurlah seseorang mengatakan ini. Bentuk yang memiliki 90% data tetapi tidak akan menyimpan apa pun tidak adil bagi pengguna, yang sering membuat 10% lainnya hanya untuk tidak kehilangan data, sehingga semua validasi yang dilakukan adalah memaksa sistem untuk kehilangan jejak yang 10% dibuat. Masalah serupa dapat terjadi di bagian belakang - katakanlah impor data. Saya menemukan biasanya lebih baik mencoba bekerja dengan benar dengan data yang tidak valid daripada mencoba mencegahnya agar tidak pernah terjadi.
psr
2
@psr Apakah Anda bahkan memerlukan logika back-end jika data Anda tidak bertahan? Anda dapat meninggalkan semua manipulasi di sisi klien jika data Anda tidak memiliki makna pada model bisnis Anda. Akan juga pemborosan sumber daya untuk mengirim pesan bolak-balik (client-server) jika datanya tidak berarti. Jadi, kita kembali ke ide "tidak pernah mengizinkan Anda objek domain untuk masuk dalam keadaan tidak valid!" .
Geo C.
2
Saya bertanya-tanya mengapa begitu banyak suara untuk jawaban ambigu seperti itu. Saat menggunakan DDD, kadang-kadang ada beberapa aturan yang cukup memeriksa apakah beberapa data INT atau dalam kisaran. Misalnya ketika Anda membiarkan pengguna aplikasi memilih beberapa batasan pada produknya (berapa kali seseorang dapat melihat pratinjau produk saya, dan dalam jangka waktu berapa bulan dalam sebulan). Di sini kedua kendala harus int dan salah satunya harus dalam kisaran 0-31. Ini tampaknya validasi format data yang dalam lingkungan non DDD cocok dengan layanan atau pengontrol. Tetapi dalam DDD saya berada di sisi menjaga validaion dalam domain (90% dari itu).
Geo C.
2
Menegakkan lapisan atas untuk tahu terlalu banyak tentang domain untuk menjaganya dalam keadaan yang valid baunya seperti desain yang buruk. Domain haruslah yang menjamin statusnya valid. Bergerak terlalu banyak di pundak lapisan atas dapat membuat domain Anda anemia dan Anda bisa melewati beberapa kendala penting yang dapat merugikan bisnis Anda. Apa yang saya sadari sekarang, generalisasi yang tepat adalah untuk menjaga validasi Anda sedekat mungkin dengan kegigihan Anda, atau sedekat mungkin dengan kode manipulasi data Anda (ketika dimanipulasi untuk mencapai keadaan akhir).
Geo C.
PS Saya tidak mencampur otorisasi (diizinkan untuk melakukan sesuatu), otentikasi (apakah pesan itu berasal dari lokasi yang tepat atau dikirim oleh klien yang tepat, keduanya diidentifikasi dengan kunci api / token / nama pengguna atau apa pun) dengan validasi format atau aturan bisnis. Ketika saya katakan 90% maksud saya aturan bisnis yang kebanyakan dari mereka juga memasukkan validasi format. Validasi format Ofcourse dapat di lapisan atas, tetapi kebanyakan dari mereka akan berada di domain (bahkan format alamat email yang akan divalidasi dalam objek nilai Alamat Email).
Geo C.
6

Terlepas dari kenyataan, pertanyaan ini agak basi, saya ingin menambahkan sesuatu yang berharga:

Saya ingin setuju dengan @MichaelBorgwardt dan memperluas dengan membawa testability. Dalam "Bekerja Efektif dengan Kode Legacy", Michael Feathers berbicara banyak tentang hambatan untuk pengujian dan salah satu hambatan itu adalah objek "sulit untuk membangun". Membangun objek yang tidak valid harus dimungkinkan, dan seperti yang disarankan Fowler, pemeriksaan validitas tergantung konteks harus dapat mengidentifikasi kondisi tersebut. Jika Anda tidak dapat menemukan cara membangun objek dalam test harness, Anda akan kesulitan menguji kelas Anda.

Mengenai validitas saya suka memikirkan sistem kontrol. Sistem kontrol bekerja dengan secara konstan menganalisis keadaan output dan menerapkan tindakan korektif ketika output menyimpang dari titik setel, ini disebut kontrol loop tertutup. Kontrol loop tertutup pada dasarnya mengharapkan penyimpangan dan bertindak untuk memperbaikinya dan itulah cara dunia nyata bekerja, itulah sebabnya mengapa semua sistem kontrol nyata biasanya menggunakan pengontrol loop tertutup.

Saya pikir menggunakan validasi tergantung konteks dan objek yang mudah dibangun akan membuat sistem Anda lebih mudah untuk digunakan.

Paul
sumber
1
Banyak kali objek hanya tampak sulit dibangun. Misalnya dalam kasus ini Anda bisa mem-bypass konstruktor publik dengan membuat kelas Wrapper yang mewarisi dari kelas yang diuji dan memungkinkan Anda untuk membuat instance objek dasar dalam keadaan tidak valid. Di sinilah menggunakan pengubah akses yang benar di kelas dan konstruktor ikut bermain dan benar-benar dapat merusak pengujian jika digunakan secara tidak benar. Selain itu, menghindari kelas dan metode "yang disegel" kecuali jika sesuai akan membuat kode lebih mudah untuk diuji.
P. Roe
4

Karena saya yakin Anda sudah tahu ...

Dalam pemrograman berorientasi objek, sebuah konstruktor (kadang-kadang disingkat menjadi ctor) di kelas adalah jenis subrutin khusus yang disebut pada penciptaan objek. Ini mempersiapkan objek baru untuk digunakan, sering menerima parameter yang digunakan konstruktor untuk mengatur variabel anggota yang diperlukan saat objek pertama kali dibuat. Itu disebut konstruktor karena itu membangun nilai-nilai anggota data kelas.

Memeriksa validitas data yang diteruskan sebagai parameter c'tor jelas valid di konstruktor - jika tidak, Anda mungkin mengizinkan pembangunan objek yang tidak valid.

Namun (dan ini hanya pendapat saya, tidak dapat menemukan dokumen yang bagus pada saat ini) - jika validasi data memerlukan operasi yang kompleks (seperti operasi async - mungkin validasi berbasis server jika mengembangkan aplikasi desktop), maka lebih baik dimasukkan ke dalam inisialisasi atau fungsi validasi eksplisit semacam dan anggota diatur ke nilai default (seperti null) dalam c'tor.


Juga, hanya sebagai catatan samping saat Anda memasukkannya dalam contoh kode Anda ...

Kecuali jika Anda melakukan validasi lebih lanjut (atau fungsi lainnya) AddOrderLine, saya kemungkinan besar akan mengekspos List<LineItem>sebagai properti daripada Orderbertindak sebagai fasad .

Demian Brecht
sumber
Mengapa mengekspos wadah? Apa bedanya untuk lapisan atas apa wadah itu? Sangat masuk akal untuk memiliki suatu AddLineItemmetode. Bahkan, untuk DDD, ini lebih disukai. Jika List<LineItem>diubah menjadi objek koleksi khusus, maka properti yang terbuka dan segala sesuatu yang bergantung pada List<LineItem>properti dapat berubah, salah, dan terkecuali.
IAbstract
4

Validasi harus dilakukan sesegera mungkin.

Validasi dalam konteks apa pun, baik model Domain atau cara penulisan perangkat lunak lainnya, harus memenuhi tujuan APA yang ingin Anda validasi dan pada level apa Anda saat ini.

Berdasarkan pertanyaan Anda, saya kira jawabannya adalah dengan memecah validasi.

  1. Validasi properti memeriksa apakah nilai untuk properti itu sudah benar, misalnya ketika rentang antara 1-10 dikeluarkan.

  2. Validasi objek menjamin bahwa semua properti pada objek tersebut valid dalam hubungannya satu sama lain. misalnya BeginDate sebelum EndDate. Misalkan Anda membaca nilai dari penyimpanan data dan BeginDate dan EndDate diinisialisasi ke DateTime.Min secara default. Saat mengatur BeginDate, tidak ada alasan untuk menegakkan aturan "harus sebelum EndDate", karena ini tidak berlaku YET. Aturan ini harus diperiksa SETELAH semua properti telah ditetapkan. Ini bisa disebut pada level root agregat

  3. Validasi juga harus dilakukan sebelumnya pada entitas agregat (atau akar agregat). Objek Pesanan dapat berisi data yang valid dan begitu juga dengan OrderLines. Tetapi kemudian aturan bisnis menyatakan bahwa tidak ada pesanan mungkin lebih dari $ 1.000. Bagaimana Anda akan menegakkan aturan ini dalam beberapa kasus, ini diizinkan. Anda tidak bisa hanya menambahkan properti "jangan validasi jumlah" karena ini akan menyebabkan penyalahgunaan (cepat atau lambat, bahkan mungkin Anda, hanya untuk menyingkirkan "permintaan jahat" ini).

  4. selanjutnya ada validasi di lapisan presentasi. Apakah Anda benar-benar akan mengirim objek melalui jaringan, mengetahui itu akan gagal? Atau akankah Anda mengampuni pengguna burdon ini dan memberitahukannya segera setelah ia memasukkan nilai yang tidak valid. mis. sebagian besar waktu lingkungan DEV Anda akan lebih lambat daripada produksi. Apakah Anda ingin menunggu selama 30 detik sebelum Anda diberi tahu "Anda lupa bidang ini LAGI selama uji coba LAINNYA", terutama ketika ada bug produksi yang harus diperbaiki dengan atasan yang menghembuskan leher Anda?

  5. Validasi pada tingkat persistensi seharusnya sedekat mungkin dengan validasi nilai properti. Ini akan membantu mencegah pengecualian dengan membaca kesalahan "nol" atau "nilai tidak valid" saat menggunakan pemetaan dari segala jenis atau pembaca data lama biasa. Menggunakan prosedur yang tersimpan tidak menyelesaikan masalah ini, tetapi mengharuskan untuk menulis logika pembatalan yang sama LAGI dan menjalankannya LAGI. Dan prosedur tersimpan adalah domain admin DB, jadi jangan coba-coba melakukan pekerjaannya juga (atau lebih buruk lagi mengganggunya dengan "seluk memetik dia tidak dibayar".

jadi untuk mengatakannya dengan beberapa kata terkenal "itu tergantung", tetapi setidaknya sekarang Anda tahu MENGAPA itu tergantung.

Saya berharap saya dapat menempatkan semua ini di satu tempat, tetapi sayangnya, ini tidak dapat dilakukan. Melakukan ini akan menempatkan ketergantungan pada "objek Dewa" yang berisi SEMUA validasi untuk SEMUA lapisan. Anda tidak ingin melewati jalan gelap itu.

Karena alasan ini saya hanya melempar pengecualian validasi ke tingkat properti. Semua level lain yang saya gunakan ValidationResult dengan metode IsValid untuk mengumpulkan semua "aturan yang rusak" dan meneruskannya kepada pengguna dalam satu AggregateException tunggal.

Ketika menyebarkan tumpukan panggilan, saya kemudian mengumpulkan ini lagi di AggregateExceptions sampai saya mencapai lapisan presentasi. Lapisan layanan dapat membuang pengecualian ini langsung ke klien dalam kasus WCF sebagai FaultException.

Ini memungkinkan saya untuk mengambil pengecualian dan membaginya untuk menunjukkan kesalahan individu pada setiap kontrol input atau meratakannya dan menunjukkannya dalam satu daftar. Pilihan ada padamu.

inilah mengapa saya juga menyebutkan validasi presentasi, untuk membuat hubungan arus pendek sebanyak mungkin.

Jika Anda bertanya-tanya mengapa saya juga memiliki validasi pada tingkat agregasi (atau tingkat layanan jika Anda suka), itu karena saya tidak memiliki bola kristal yang memberitahu saya siapa yang akan menggunakan layanan saya di masa depan. Anda akan cukup kesulitan menemukan kesalahan Anda sendiri untuk mencegah orang lain membuat kesalahan Anda :) dengan memasukkan data yang tidak valid. Jika Anda mengelola aplikasi A, tetapi aplikasi B memberi makan beberapa data menggunakan layanan Anda. Tebak siapa yang pertama mereka tanyakan ketika ada bug? Administrator aplikasi B dengan senang hati akan memberi tahu pengguna "tidak ada kesalahan di akhir saya, saya hanya memasukkan data".

Wesley Kenis
sumber