Sebuah nilai sering masuk ke program sebagai string, angka, atau objek dengan struktur longgar, lalu bergerak melewati beberapa layer. Jika setiap layer harus menanyakan apakah nilai itu kosong, malformed, atau berada di luar rentang yang diizinkan, logika validasi akan menyebar ke seluruh codebase. Sebagian caller mengulang pemeriksaan, sebagian melupakannya, dan sebagian lain diam-diam membuat asumsi yang berbeda.
Alternatif yang berguna adalah memperlakukan input eksternal sebagai representasi yang tidak tepercaya dan mengubahnya pada sebuah boundary menjadi nilai yang merepresentasikan fakta domain. Setelah konversi berhasil, kode downstream dapat mengandalkan jaminan yang diberikan nilai baru tersebut alih-alih berulang kali memvalidasi representasi awal.
Model mentalnya adalah: periksa ketidakpastian di tempat ia masuk, lalu bawa hasil pemeriksaan itu di dalam nilai itu sendiri. Artikel ini menunjukkan bagaimana pendekatan tersebut mengubah kode, jaminan apa yang dapat diberikannya, dan di mana pendekatan ini tidak berlaku.
Mulai dari ketidakpastian, bukan tipe
Misalkan sebuah layanan order menerima jumlah yang diminta. Layer transport memberikannya sebagai teks:
"12"Aplikasi membutuhkan fakta yang lebih kuat: jumlah tersebut harus berupa bilangan bulat dari 1 sampai 100.
Jika string mentah bergerak jauh ke dalam aplikasi, beberapa fungsi mungkin melindungi dirinya secara terpisah:
reserve(quantity_text):
quantity = parse_integer(quantity_text)
if quantity < 1 or quantity > 100:
return error
...
calculate_shipping(quantity_text):
quantity = parse_integer(quantity_text)
if quantity < 1 or quantity > 100:
return error
...Masalahnya bukan sekadar sintaks yang terduplikasi. Setiap fungsi menerima nilai yang maknanya belum terselesaikan. quantity_text bisa berupa "12", "0", "many", atau hal lain. Setiap consumer harus mengingat aturan yang sama sebelum dapat menggunakan nilai itu dengan aman.
Sebagai gantinya, boundary dapat mengonversi input satu kali:
result = OrderQuantity.from_text(quantity_text)
if result is error:
return invalid_request(result.message)
quantity = result.value
reserve(quantity)
calculate_shipping(quantity)OrderQuantity tidak berguna hanya karena memiliki nama khusus domain. Ia berguna jika konstruksinya menegakkan aturan yang menjadi sandaran bagian program lainnya.
Buat konstruksi menetapkan jaminan
Untuk contoh ini, OrderQuantity hanya boleh ada ketika nilai numeriknya berada di antara 1 dan 100, inklusif.
Salah satu sketsa yang netral terhadap bahasa pemrograman terlihat seperti ini:
OrderQuantity.from_text(text):
number = try_parse_integer(text)
if number could not be parsed:
return error("quantity must be a whole number")
if number < 1 or number > 100:
return error("quantity must be between 1 and 100")
return success(OrderQuantity(number))Keputusan desain yang penting adalah caller biasa tidak dapat melewati pemeriksaan ini dan membuat OrderQuantity(0) secara langsung. Mekanisme tepatnya bergantung pada bahasa: private constructor, module boundary, smart constructor, factory function, atau mekanisme enkapsulasi lain dapat menyediakannya.
Setelah konstruksi berhasil, kode downstream dapat bernalar dari premis yang lebih kuat:
calculate_shipping(quantity: OrderQuantity):
if quantity.value <= 10:
return STANDARD_RATE
return BULK_RATEFungsi tersebut tidak lagi memeriksa apakah quantity berupa angka atau bernilai positif. Pertanyaan itu sudah diselesaikan sebelum fungsi menerima nilai tersebut.
Ini tidak berarti nilai tersebut valid secara universal. Artinya, nilai itu memenuhi invariant spesifik yang dijanjikan oleh OrderQuantity. Jika bisnis kemudian hanya mengizinkan maksimal 50 unit untuk suatu produk, aturan terpisah tersebut mungkin tetap membutuhkan informasi yang tidak dimiliki nilai quantity.
Pisahkan error representasi dari aturan domain
Konversi di boundary menjadi lebih jelas ketika dua jenis kegagalan yang berbeda dipisahkan.
Error representasi berarti input tidak dapat diinterpretasikan sebagai jenis nilai yang dibutuhkan. Sebagai contoh, "twelve" tidak dapat diinterpretasikan sebagai quantity integer di bawah kontrak input integer desimal.
Pelanggaran aturan domain berarti representasinya dapat dipahami, tetapi nilainya tidak diizinkan. "0" dapat di-parse sebagai integer, tetapi nol berada di luar rentang quantity order yang diizinkan.
Kedua kegagalan dapat dilaporkan oleh operasi konversi yang sama, tetapi memisahkan konsepnya memperjelas penalaran:
"twelve" -> cannot parse integer
"0" -> integer, but outside 1..100
"12" -> valid OrderQuantityPembedaan ini juga membantu error handling. Sebuah API dapat memetakan kedua kasus ke status yang sama untuk client sambil tetap mencatat informasi diagnostik yang berbeda secara internal. Nilai domain itu sendiri tidak perlu mengingat input tidak valid mana yang ditolak; input tidak valid tidak pernah menjadi nilai tersebut.
Biarkan interface downstream mewajibkan nilai yang lebih kuat
Validasi di boundary hanya memberi manfaat terbatas jika fungsi internal tetap menerima representasi yang lemah.
Bandingkan signature berikut:
reserve(quantity: string)dengan:
reserve(quantity: OrderQuantity)Signature pertama menyisakan pertanyaan bagi setiap caller: string mana yang dapat diterima? Signature kedua menyatakan bahwa caller harus terlebih dahulu memperoleh quantity yang memenuhi invariant domain.
Perubahan itu memindahkan tanggung jawab ke boundary yang disengaja. Kode yang mem-parse HTTP request, mengonsumsi message, membaca file konfigurasi, atau mengimpor record dapat melakukan konversi. Logika inti aplikasi kemudian dapat bekerja pada nilai yang makna dasarnya sudah ditetapkan.
Pola yang sama dapat digunakan untuk banyak konsep kecil:
EmailAddress.from_text(raw)
Percentage.from_number(raw)
VersionNumber.parse(raw)
NonEmptyName.from_text(raw)Contoh tersebut hanya tepat ketika tipe memiliki invariant yang stabil dan bermakna. Membuat wrapper untuk setiap nilai primitive menambah ceremony tanpa selalu memperbaiki desain.
Validasi fakta yang benar-benar dapat dimiliki nilai
Sebuah nilai domain biasanya sebaiknya menegakkan aturan yang dapat diputuskan dari informasi yang dikandungnya dan tetap benar selama masa guna nilai tersebut.
Untuk OrderQuantity, rentang 1 sampai 100 dapat memenuhi deskripsi itu jika merupakan invariant seluruh sistem. Aturan seperti “warehouse ini saat ini memiliki 12 unit tersedia” tidak demikian. Ketersediaan bergantung pada state eksternal yang berubah.
Mencoba menanamkan aturan itu ke dalam konstruksi menghasilkan jaminan yang menyesatkan:
quantity = OrderQuantity.create(10, current_inventory)Walaupun inventory memiliki 10 unit saat konstruksi, order lain dapat menghabiskan stock sesaat kemudian. Nilai tersebut tidak dapat mempertahankan klaim bahwa stock masih tersedia.
Pemisahan yang lebih baik adalah:
quantity = OrderQuantity.from_number(10) // stable quantity invariant
inventory.reserve(product, quantity) // current-state business decisionOperasi pertama menetapkan apa quantity tersebut. Operasi kedua menanyakan apakah suatu tindakan diizinkan dalam kondisi saat ini.
Pembedaan ini mencegah kesalahan umum: memperlakukan setiap aturan validasi bisnis sebagai properti value object. Sebagian aturan menjadi milik operasi karena bergantung pada waktu, entitas lain, permission, atau mutable system state.
Jangan samakan sudah divalidasi dengan tepercaya selamanya
Nilai domain yang sudah divalidasi menghapus satu kelas ketidakpastian. Ia tidak menghapus semua kemungkinan kegagalan.
Pertimbangkan nilai FilePath yang menolak path kosong dan menormalisasi separator. Itu dapat berguna, tetapi tidak membuktikan bahwa file ada, process dapat membacanya, atau file masih akan ada ketika operasi dijalankan. Fakta tersebut bergantung pada environment.
Demikian pula, CustomerId yang valid dapat menjamin format identifier tanpa menjamin bahwa customer dengan identifier tersebut benar-benar ada. Nilai Money yang valid dapat menegakkan aturan currency dan amount tanpa menjamin bahwa account memiliki dana yang cukup.
Pertanyaan praktisnya adalah:
Fakta apa yang menjadi benar ketika nilai ini berhasil dibuat, dan dapatkah program mempertahankan fakta tersebut tanpa berkonsultasi dengan state eksternal yang berubah?
Jika jawabannya presisi, tipe dapat mengomunikasikan jaminan yang berguna. Jika jawabannya kabur, abstraction mungkin menjanjikan lebih dari yang dapat diberikannya.
Tentukan tempat konversi seharusnya dilakukan
Boundary yang tepat adalah titik ketika program memiliki cukup konteks untuk menginterpretasikan input mentah dan sebelum data yang lebih lemah menyebar ke kode yang mengharapkan makna domain.
Untuk web request, titik itu mungkin request mapper yang menghadap aplikasi, bukan HTTP framework itu sendiri. Untuk message consumer, titik itu mungkin adapter yang mengubah decoded message menjadi application command. Untuk file import, titik itu mungkin tahap konversi row-to-domain.
Hindari mendorong kebijakan domain ke generic infrastructure hanya agar validasi dilakukan lebih awal. HTTP library dapat menetapkan bahwa sebuah field secara sintaksis adalah integer, tetapi aturan bahwa order quantity harus paling banyak 100 adalah milik application atau domain policy yang menguasai aturan tersebut.
Jadi, tujuannya bukan “validasi sedini mungkin secara fisik.” Tujuannya adalah menyelesaikan ketidakpastian pada boundary paling awal yang memiliki cukup makna untuk menyelesaikannya dengan benar.
Kembalikan kegagalan secara eksplisit
Konversi dari input yang belum pasti memang sewajarnya dapat gagal. Memperlakukan setiap nilai user yang ditolak sebagai exceptional programming failure dapat mengaburkan fakta tersebut.
API konversi sebaiknya membuat perilaku kegagalannya jelas. Bergantung pada bahasa dan konvensi di sekitarnya, bentuknya dapat berupa result type, error return, optional value ketika diagnosis tidak diperlukan, atau exception yang memang ditujukan untuk konversi input.
Properti pentingnya adalah caller tidak dapat secara tidak sengaja memperlakukan konversi yang gagal sebagai konstruksi yang berhasil.
Sebagai contoh:
result = OrderQuantity.from_text(raw)
match result:
success(quantity) -> submit_order(quantity)
error(problem) -> report(problem)Bentuk ini membuat transisi terlihat jelas: input mentah berada di satu sisi; nilai domain tepercaya hanya ada pada jalur sukses.
Waspadai aturan yang berubah dengan laju berbeda
Sebuah nilai menjadi canggung ketika constructornya mengumpulkan kebijakan yang tidak saling berkaitan.
Misalkan OrderQuantity dimulai dengan batas teknis stabil 1 sampai 100, tetapi kemudian menerima aturan untuk customer tier, promotional period, warehouse capacity, dan product-specific limit. Konstruksi sekarang membutuhkan beberapa service dan potongan konteks:
OrderQuantity.create(
raw,
customer,
product,
warehouse,
promotion,
clock
)Itu adalah tanda peringatan. Nilai tersebut tidak lagi hanya menetapkan invariant miliknya sendiri; ia berubah menjadi decision engine untuk sebuah operasi.
Jaga agar nilai tetap berfokus pada fakta yang intrinsik terhadap nilai itu. Tempatkan aturan kontekstual pada operasi atau policy yang memiliki konteks yang diperlukan:
quantity = OrderQuantity.from_number(raw)
ordering_policy.check(customer, product, quantity)Pemisahan ini juga mempermudah menemukan lokasi perubahan. Perubahan pada arti sebuah quantity memengaruhi nilai. Perubahan pada siapa yang boleh memesan berapa banyak memengaruhi ordering policy.
Ketika pendekatan yang lebih sederhana sudah cukup
Tidak setiap input membutuhkan domain type khusus.
Jika sebuah nilai hanya digunakan sekali, memiliki representasi built-in yang jelas, dan tidak membawa invariant yang dapat digunakan kembali, pemeriksaan lokal dapat lebih jelas. Sebagai contoh, command-line option sekali pakai yang menerima retry count dari 0 sampai 3 mungkin cukup divalidasi di tempat command di-parse.
Nilai khusus menjadi lebih berguna ketika beberapa bagian program bergantung pada invariant yang sama, ketika tertukarnya nilai primitive yang mirip berbiaya mahal, atau ketika pemeriksaan berulang sudah mulai muncul.
Trade-off-nya adalah tambahan kode dan konsep. Constructor, fungsi konversi, dan nama khusus domain membutuhkan maintenance. Gunakan ketika jaminan yang lebih kuat menyederhanakan cukup banyak penalaran downstream sehingga biaya tersebut layak dibayar.
Kesimpulan
Validasi berulang sering menjadi tanda bahwa input yang belum pasti telah bergerak terlalu jauh. Alih-alih meminta setiap consumer melindungi dirinya dari nilai malformed atau out-of-range yang sama, konversikan input mentah pada boundary yang bermakna menjadi nilai domain yang konstruksinya menetapkan invariant yang presisi.
Kuncinya adalah menjaga jaminan tetap sempit dan benar. Validasi representasi serta fakta stabil yang dapat dimiliki nilai. Biarkan aturan yang bergantung pada mutable external state ditangani operasi yang memiliki konteks tersebut. Kemudian buat interface downstream menerima nilai yang lebih kuat agar hasil validasi dibawa melalui program, bukan sekadar diingat melalui konvensi.