Cara bekerja dengan pengecualian di DDD

gambar

Sebagai bagian dari konferensi DotNext 2018 baru-baru ini, BoF on Domain Driven Design berlangsung. Ini membahas masalah bekerja dengan pengecualian, yang menyebabkan perdebatan sengit, tetapi tidak menerima diskusi rinci, karena itu bukan topik utama.

Selain itu, mempelajari banyak sumber daya, mulai dari pertanyaan tentang stackoverflow dan diakhiri dengan kursus arsitektur berbayar, Anda dapat mengamati bahwa komunitas TI memiliki sikap ambigu terhadap pengecualian dan bagaimana menggunakannya.

Paling sering disebutkan bahwa dengan menggunakan pengecualian, mudah untuk membuat utas eksekusi yang memiliki semantik operator goto , yang berdampak buruk terhadap keterbacaan kode.

Ada berbagai pendapat tentang apakah membuat jenis pengecualian Anda sendiri atau menggunakan yang standar yang disediakan di .NET.

Seseorang melakukan validasi pada pengecualian, dan seseorang di mana-mana menggunakan monad Hasil . Memang benar bahwa Hasil memungkinkan Anda untuk memahami dengan tanda tangan metode apakah eksekusi yang sukses adalah mungkin atau tidak. Tetapi tidak kurang benar bahwa dalam bahasa imperatif (yang mencakup C #), penggunaan Hasil yang luas menyebabkan kode yang sulit dibaca, ditutupi dengan konstruksi bahasa sehingga sulit untuk membuat naskah asli.

Dalam artikel ini saya akan berbicara tentang praktik-praktik yang diadopsi oleh tim kami (singkatnya - kami menggunakan semua pendekatan dan tidak ada satupun yang merupakan dogma).

Kami akan berbicara tentang aplikasi perusahaan yang dibangun berdasarkan ASP.NET MVC + WebAPI. Aplikasi ini dibangun dengan arsitektur bawang merah , berkomunikasi dengan pangkalan data dan pesan broker. Ini menggunakan logging terstruktur ke tumpukan ELK dan pemantauan dikonfigurasi menggunakan Grafana.

Kami akan melihat bekerja dengan pengecualian dari tiga perspektif:

  1. Aturan Pengecualian Umum
  2. Pengecualian, Kesalahan, dan Arsitektur Bawang
  3. Kasus khusus untuk aplikasi Web

Aturan Pengecualian Umum


  1. Pengecualian dan kesalahan bukanlah hal yang sama. Untuk pengecualian, kami menggunakan pengecualian, untuk kesalahan - Hasil.
  2. Pengecualian hanya untuk situasi luar biasa, yang menurut definisi tidak bisa banyak. Jadi semakin sedikit pengecualian, semakin baik.
  3. Penanganan pengecualian harus sedetail mungkin. Seperti yang ditulis Richter dalam karya monumentalnya.
  4. Jika kesalahan harus dikirim ke pengguna dalam bentuk aslinya - gunakan Hasil.
  5. Pengecualian seharusnya tidak meninggalkan batas-batas sistem dalam bentuk aslinya. Ini tidak ramah pengguna dan memberi penyerang cara untuk mengeksplorasi lebih lanjut kemungkinan kelemahan dalam sistem.
  6. Jika pengecualian yang dilemparkan ditangani oleh aplikasi kami, kami menggunakan bukan pengecualian, tetapi Hasil. Implementasi pada pengecualian akan disembunyikan oleh operator goto dan semakin buruk, semakin jauh kode pemrosesan dari kode throw pengecualian. Hasil secara eksplisit menyatakan kemungkinan kesalahan dan hanya memungkinkan pemrosesan "linier" nya.

Pengecualian, Kesalahan, dan Arsitektur Bawang


Di bagian berikut, kami akan mempertimbangkan tanggung jawab dan aturan untuk melempar / menangani pengecualian / kesalahan untuk lapisan berikut:

  • Host aplikasi
  • Infrastruktur
  • Layanan aplikasi
  • Inti domain

Host aplikasi


Apa yang bertanggung jawab

  • Root komposisi , menyesuaikan operasi seluruh aplikasi.
  • Batas interaksi dengan dunia luar adalah pengguna, layanan lain, peluncuran dijadwalkan.

Karena ini adalah tanggung jawab yang cukup kompleks, ada baiknya mereka membatasi diri. Kami memberikan tanggung jawab yang tersisa ke lapisan dalam.

Cara menangani kesalahan dari Hasil

Siaran ke dunia luar, mengonversi ke format yang sesuai (misalnya, dalam respons http).

Bagaimana Hasil Menghasilkan

Tidak mungkin. Lapisan ini tidak mengandung logika, jadi tidak ada tempat untuk menghasilkan kesalahan.

Cara menangani pengecualian

  1. Menyembunyikan detail dan mengonversi ke format yang sesuai untuk dikirim ke dunia luar
  2. Masuk.

Cara melempar pengecualian

Tidak mungkin, lapisan ini adalah yang paling eksternal dan tidak mengandung logika - tidak ada yang melempar pengecualian untuk itu.

Infrastruktur


Apa yang bertanggung jawab

  1. Adaptor ke port , atau hanya untuk mengimplementasikan antarmuka-Domain, memberikan akses ke infrastruktur - layanan pihak ketiga, database, direktori aktif, dll. Lapisan ini harus sebodoh mungkin dan mengandung logika sesedikit mungkin.
  2. Jika perlu, itu bisa bertindak sebagai lapisan Anti-korupsi .

Cara menangani kesalahan dari Hasil

Saya tidak tahu penyedia database dan layanan lain yang berjalan di monad Hasil. Namun, beberapa layanan beroperasi dengan kode pengembalian. Dalam hal ini, kami akan mengonversikannya ke format Hasil yang diperlukan oleh port.

Bagaimana Hasil Menghasilkan

Secara umum, lapisan ini tidak mengandung logika, yang berarti tidak menghasilkan kesalahan. Tetapi jika digunakan sebagai lapisan anti korupsi, berbagai opsi dimungkinkan. Misalnya, parsing pengecualian dari layanan lawas dan mengonversi ke Hasil, pengecualian yang merupakan pesan validasi sederhana.

Cara menangani pengecualian

Dalam kasus umum, lemparkan lebih jauh, jika perlu, setelah mengamankan detailnya. Jika pelabuhan yang diterapkan memungkinkan Hasil untuk kembali dalam kontrak, maka infrastruktur akan dikonversi menjadi Jenis pengecualian yang dapat diproses.

Misalnya, broker pesan yang digunakan dalam proyek melempar pengecualian ketika mencoba mengirim pesan ketika broker tidak tersedia. Lapisan Layanan Aplikasi siap untuk situasi ini dan dapat menanganinya dengan kebijakan Coba Lagi, Pemutus Sirkuit atau rollback data manual.

Dalam hal ini, lapisan Layanan Aplikasi menyatakan kontrak yang mengembalikan Hasil jika terjadi kesalahan. Dan lapisan Infrastruktur mengimplementasikan port ini, mengubah pengecualian dari broker ke Hasil. Secara alami, itu hanya mengubah jenis pengecualian tertentu, dan tidak semua berturut-turut.

Dengan menggunakan pendekatan ini, kami mendapatkan dua keuntungan:

  1. Secara eksplisit menyatakan kemungkinan kesalahan dalam kontrak.
  2. Kami menyingkirkan situasi saat Layanan Aplikasi tahu cara menangani kesalahan, tetapi tidak tahu jenis pengecualian, karena layanan ini disarikan dari pialang pesan tertentu. Untuk membangun blok tangkap di dasar System.Exception berarti menangkap semua jenis pengecualian, dan bukan hanya yang dapat ditangani oleh Layanan Aplikasi.

Cara melempar pengecualian

Tergantung pada spesifikasi sistem.

Sebagai contoh, pernyataan LINQ Single dan First melempar InvalidOperationException ketika meminta data yang tidak ada. Tapi jenis pengecualian ini digunakan di mana-mana di .NET, yang membuatnya mustahil untuk memprosesnya secara terperinci.

Kami di tim mengadopsi praktik membuat ItemNotFoundException kustom dan membuangnya dari lapisan infrastruktur jika data yang diminta tidak ditemukan dan tidak boleh sesuai dengan aturan bisnis.

Jika data yang diminta tidak ditemukan dan ini diizinkan, harus dinyatakan secara eksplisit dalam kontrak pelabuhan. Misalnya, menggunakan Mungkin monad .

Layanan aplikasi


Apa yang bertanggung jawab

  1. Validasi data input.
  2. Orkestrasi dan koordinasi layanan - mulai dan berakhirnya transaksi, implementasi skrip yang didistribusikan, dll.
  3. Unduh objek domain dan data eksternal melalui port ke Infrastruktur, panggilan perintah berikutnya dalam Domain Core.

Cara menangani kesalahan dari Hasil

Kesalahan dari inti domain diterjemahkan ke dalam dunia luar tidak berubah. Kesalahan dari Infrastruktur dapat ditangani melalui Retry, kebijakan Circuit Breaker atau disiarkan ke luar.

Bagaimana Hasil Menghasilkan

Dapat menerapkan validasi sebagai Hasil.

Dapat menghasilkan notifikasi keberhasilan sebagian operasi. Misalnya, pesan ke pengguna seperti "Pesanan Anda telah berhasil ditempatkan, tetapi terjadi kesalahan saat memverifikasi alamat pengiriman. Seorang spesialis akan segera menghubungi Anda untuk mengklarifikasi detail pengiriman. "

Cara menangani pengecualian

Dengan asumsi bahwa pengecualian infrastruktur yang dapat ditangani aplikasi telah dikonversi oleh lapisan Infrastruktur ke Hasil, ia tidak menanganinya sama sekali.

Cara melempar pengecualian

Secara umum, tidak mungkin. Tetapi ada opsi batas yang dijelaskan di bagian akhir artikel.

Inti domain


Apa yang bertanggung jawab

Penerapan logika bisnis, "inti" sistem dan makna utama keberadaannya.

Cara menangani kesalahan dari Hasil

Karena layer bersifat internal dan kesalahan hanya dimungkinkan dari objek di domain yang sama, pemrosesan dikurangi menjadi aturan bisnis atau terjemahan kesalahan ke atas dalam bentuk aslinya.

Bagaimana Hasil Menghasilkan

Jika Anda melanggar aturan bisnis yang dikemas dalam Core Domain dan tidak dicakup oleh validasi data input di tingkat Layanan Aplikasi. Secara umum, di lapisan ini, Hasil paling sering digunakan.

Cara menangani pengecualian

Tidak mungkin. Pengecualian infrastruktur telah diproses oleh lapisan Infrastruktur, data telah tiba terstruktur, lengkap dan divalidasi berkat lapisan Layanan Aplikasi. Karenanya, semua pengecualian yang mungkin terbang akan menjadi pengecualian.

Cara melempar pengecualian

Biasanya aturan umum berfungsi di sini: semakin sedikit pengecualian - semakin baik.

Tetapi apakah Anda pernah mengalami situasi ketika Anda menulis kode dan memahami bahwa dalam kondisi tertentu dapat melakukan bisnis yang mengerikan? Misalnya, untuk menghapus uang dua kali atau untuk merusak data sehingga kami tidak akan mengumpulkan tulang nanti.

Sebagai aturan, kita berbicara tentang mengeksekusi perintah yang tidak dapat diterima untuk keadaan objek saat ini.

Tentu saja, tombol yang sesuai pada UI seharusnya tidak terlihat dalam keadaan ini. Kita seharusnya tidak menerima perintah dari bus di negara bagian ini. Semua ini benar asalkan lapisan luar dan sistem melakukan fungsinya secara normal . Tetapi dalam Domain Core kita tidak boleh tahu tentang keberadaan lapisan eksternal dan percaya pada kebenaran pekerjaan mereka, kita harus melindungi invarian dari sistem.

Beberapa cek dapat ditempatkan di Layanan Aplikasi di tingkat validasi. Tapi ini bisa berubah menjadi pemrograman defensif , yang dalam kasus ekstrim mengarah ke hal berikut:

  1. Enkapsulasi melemah, karena invarian tertentu harus diverifikasi pada lapisan luar.
  2. Pengetahuan tentang area subjek "mengalir" ke lapisan luar, pemeriksaan dapat digandakan oleh kedua lapisan.
  3. Memvalidasi pelaksanaan perintah dari lapisan eksternal bisa lebih kompleks dan kurang dapat diandalkan daripada memverifikasi bahwa objek domain tidak dapat menjalankan perintah dalam keadaan saat ini.

Juga, jika kita menempatkan pemeriksaan seperti itu di lapisan validasi, maka kita harus memberi tahu pengguna alasan untuk kesalahan tersebut. Mengingat bahwa kami berbicara tentang operasi yang tidak dapat dilakukan sama sekali dalam kondisi saat ini, kami berisiko berada dalam salah satu dari dua situasi:

  • Kami memberi pengguna biasa pesan yang tidak ia pahami sama sekali dan akan tetap mendukung, sama seperti dengan pesan "Terjadi kesalahan tak terduga".
  • Kami memberi tahu penjahat itu dalam bentuk yang cukup jelas mengapa ia tidak dapat melakukan operasi yang ingin ia lakukan dan ia dapat mencari solusi lain.

Namun kembali ke topik utama artikel. Dengan semua indikasi, situasi yang dibahas luar biasa. Itu seharusnya tidak pernah terjadi, tetapi jika itu terjadi, itu akan buruk.

Paling logis dalam situasi ini untuk melempar pengecualian, menjanjikan rincian yang diperlukan, mengembalikan kepada pengguna kesalahan dari bentuk umum "Operasi tidak layak", mengatur pemantauan untuk jenis kesalahan ini dan berharap bahwa kita tidak akan pernah melihatnya.

Jenis atau tipe pengecualian apa yang digunakan dalam kasus ini? Secara logis, ini harus menjadi jenis pengecualian yang terpisah, sehingga kita dapat membedakannya dari yang lain dan agar tidak secara tidak sengaja ditangkap oleh penanganan pengecualian dari lapisan luar. Kami juga tidak memerlukan hierarki atau banyak pengecualian, intinya sama - sesuatu yang tidak dapat diterima telah terjadi. Dalam proyek kami, kami membuat tipe CorruptedInvariantException untuk ini, dan menggunakannya dalam situasi yang sesuai.

Kasus khusus untuk aplikasi Web


Perbedaan yang signifikan antara aplikasi web dari yang lain (desktop, daemon dan layanan windows, dll.) Adalah interaksi dengan dunia luar dalam bentuk operasi jangka pendek (pemrosesan permintaan HTTP), setelah itu aplikasi segera “lupa” apa yang terjadi.

Selain itu, setelah memproses permintaan, respons selalu dihasilkan. Jika operasi yang dilakukan oleh kode kami tidak mengembalikan data, platform akan tetap mengembalikan respons yang berisi kode status. Jika operasi dibatalkan dengan pengecualian, platform masih akan mengembalikan respons yang berisi kode status yang sesuai.

Untuk menerapkan perilaku ini, pemrosesan permintaan dalam platform Web dibangun dalam bentuk pipa. Pertama, permintaan diproses secara berurutan (permintaan), dan kemudian tanggapan disiapkan.

Kita dapat menggunakan middleware, filter tindakan, http handler atau filter ISAPI (tergantung pada platform) dan berintegrasi ke dalam pipa ini di setiap tahap. Dan pada tahap mana pun dari pemrosesan permintaan, kami dapat mengganggu pemrosesan dan pipa akan melanjutkan untuk membentuk respons.

Sebagai aturan, kami tidak lagi mengimplementasikan bagian bisnis aplikasi dalam arsitektur pipa, tetapi menulis kode yang melakukan operasi secara berurutan. Dan dengan pendekatan ini, sedikit lebih sulit untuk mengimplementasikan skenario ketika kami menghentikan eksekusi permintaan dan segera melanjutkan ke pembentukan respon.

Apa hubungan semua ini dengan penanganan pengecualian, Anda bertanya?

Faktanya adalah bahwa aturan untuk bekerja dengan pengecualian yang dijelaskan dalam bagian artikel sebelumnya tidak cocok dengan skenario ini.

Pengecualian itu buruk untuk digunakan karena itu adalah semantik goto.

Meluasnya penggunaan Hasil mengarah pada fakta bahwa kita menyeretnya (Hasil) di semua lapisan aplikasi, dan ketika membentuk respons, kita perlu menguraikan Hasil entah bagaimana untuk memahami kode status yang akan dikembalikan. Disarankan juga untuk menggeneralisasi dan mendorong kode parsing ini ke Middleware atau ActionFilter, yang menjadi petualangan tersendiri. Artinya, Hasil tidak jauh lebih baik daripada pengecualian.

Apa yang harus dilakukan dalam situasi seperti itu?

Jangan membangun yang absolut. Kami menetapkan aturan untuk keuntungan kami sendiri, bukan untuk kerugian.

Jika Anda ingin membatalkan operasi karena kelanjutannya tidak mungkin, maka melempar pengecualian tidak akan memiliki semantik goto. Kami mengarahkan eksekusi ke pintu keluar, dan bukan ke blok kode bisnis lain.

Jika alasan gangguan penting untuk menentukan kode status yang diinginkan, maka jenis pengecualian khusus dapat digunakan.

Sebelumnya, kami menyebutkan dua jenis kustom yang kami gunakan: ItemNotFoundException (mentransformasikan ke 404) dan CorruptedInvariant (mentransformasikan ke 500).

Jika Anda memeriksa hak-hak pengguna, karena mereka tidak jatuh pada role model atau klaim s, maka diperbolehkan untuk membuat custom ForbiddenException kustom (Kode status 403).

Dan akhirnya, validasi. Kami masih tidak dapat melakukan apa pun hingga pengguna memodifikasi permintaannya, semantik ini dijelaskan oleh kode 422 . Jadi kami menghentikan operasi dan mengirim permintaan langsung ke pintu keluar. Ini juga bisa dilakukan dengan menggunakan pengecualian. Misalnya, pustaka FluentValidation sudah memiliki tipe pengecualian bawaan yang meneruskan ke klien semua detail yang diperlukan untuk menampilkan dengan jelas kepada pengguna apa yang salah dengan permintaan tersebut.

Itu saja. Bagaimana Anda bekerja dengan pengecualian?

Source: https://habr.com/ru/post/id431804/


All Articles