Desain instan

Orang belajar arsitektur dari buku-buku lama yang ditulis untuk Jawa. Buku-buku itu bagus, tetapi mereka memberikan solusi untuk masalah waktu itu dengan instrumen waktu itu. Waktu telah berubah, C # lebih mirip dengan Scala cahaya dari Jawa, dan ada beberapa buku bagus yang baru.

Pada artikel ini, kita akan memeriksa kriteria untuk kode yang baik dan kode yang buruk, bagaimana dan apa yang harus diukur. Kami akan melihat gambaran umum tugas dan pendekatan, kami akan menganalisis pro dan kontra. Pada akhirnya akan ada rekomendasi dan praktik terbaik untuk merancang aplikasi web.

Artikel ini adalah transkrip laporan saya dari konferensi DotNext 2018 Moscow. Selain teks, ada video dan tautan ke slide di bawah potongan.



Halaman slide dan laporan di situs .
Tentang saya secara singkat: Saya dari Kazan, saya bekerja untuk High Tech Group. Kami sedang mengembangkan perangkat lunak untuk bisnis. Baru-baru ini, saya telah mengajar kursus di Universitas Federal Kazan yang disebut Pengembangan Perangkat Lunak Korporat. Dari waktu ke waktu saya masih menulis artikel tentang Habr tentang praktik rekayasa, tentang pengembangan perangkat lunak perusahaan.

Seperti yang mungkin sudah Anda duga, hari ini saya akan berbicara tentang pengembangan perangkat lunak perusahaan, yaitu, bagaimana menyusun aplikasi web modern:

  • kriteria
  • sejarah singkat perkembangan pemikiran arsitektural (apa, apa yang telah menjadi, apa masalahnya);
  • ikhtisar kelemahan arsitektur engah klasik
  • keputusan
  • analisis implementasi langkah demi langkah tanpa merinci
  • hasil.

Kriteria


Kami merumuskan kriteria. Saya benar-benar tidak suka ketika berbicara tentang desain dengan gaya "kung fu saya lebih kuat dari kung fu Anda". Bisnis pada prinsipnya memiliki satu kriteria khusus yang disebut uang. Semua orang tahu bahwa waktu adalah uang, jadi kedua komponen ini paling sering paling penting.



Jadi, kriterianya. Pada prinsipnya, bisnis paling sering bertanya kepada kami "sebanyak mungkin fitur per unit waktu", tetapi dengan satu peringatan - fitur ini seharusnya berfungsi. Dan langkah pertama di mana itu mungkin rusak adalah review kode. Artinya, tampaknya programmer mengatakan: "Saya akan melakukannya dalam tiga jam." Tiga jam berlalu, ulasan masuk ke kode, dan pemimpin tim berkata: "Oh, tidak, ulangi." Ada tiga lagi - dan berapa banyak iterasi yang ditinjau oleh kode, sehingga Anda perlu mengalikan tiga jam.

Poin berikutnya adalah kembali dari tahap tes penerimaan. Hal yang sama. Jika fitur tidak bekerja, maka itu tidak dilakukan, tiga jam ini membentang selama seminggu, dua - well, seperti biasa. Kriteria terakhir adalah jumlah regresi dan bug, yang, meskipun telah diuji dan diterima, melalui produksi. Ini juga sangat buruk. Ada satu masalah dengan kriteria ini. Sulit dilacak, karena hubungan antara fakta bahwa kita mendorong sesuatu ke dalam repositori dan fakta bahwa sesuatu pecah setelah dua minggu bisa jadi sulit dilacak. Tapi, bagaimanapun, itu mungkin.

Pengembangan arsitektur


Sekali waktu, ketika programmer baru mulai menulis program, masih belum ada arsitektur, dan semua orang melakukan semua yang mereka suka.



Karena itu, kami mendapatkan gaya arsitektur yang demikian. Ini disebut "kode mie" di sini, mereka mengatakan "kode spageti" di luar negeri. Semuanya terhubung dengan segalanya: kita mengubah sesuatu pada titik A - rusak pada titik B, benar-benar mustahil untuk memahami apa yang terhubung dengan apa. Tentu saja, para programmer dengan cepat menyadari bahwa ini tidak akan berhasil, dan beberapa struktur harus dilakukan, dan memutuskan bahwa beberapa layer akan membantu kami. Sekarang, jika Anda membayangkan bahwa daging cincang adalah kode, dan lasagna adalah lapisan seperti itu, inilah ilustrasi arsitektur berlapis. Daging cincang tetap dicincang, tetapi sekarang daging cincang dari lapisan No. 1 tidak bisa pergi dan berbicara dengan daging cincang dari lapisan No. 2. Kami memberi kode beberapa bentuk: bahkan dalam gambar Anda dapat melihat bahwa memanjat lebih berbingkai.



Semua orang mungkin akrab dengan arsitektur berlapis klasik : ada UI, ada logika bisnis, dan ada lapisan Akses Data. Masih ada segala macam layanan, fasad dan lapisan, dinamai untuk arsitek yang keluar dari perusahaan, mungkin ada jumlah yang tidak terbatas.



Tahap selanjutnya adalah arsitektur bawang yang disebut. Tampaknya ada perbedaan besar: sebelumnya ada kotak kecil, dan di sini ada lingkaran. Tampaknya sangat berbeda.



Tidak juga. Seluruh perbedaannya adalah bahwa di suatu tempat pada saat itu prinsip-prinsip SOLID dirumuskan, dan ternyata dalam bawang klasik ada masalah dengan inversi ketergantungan, karena kode domain abstrak untuk beberapa alasan tergantung pada implementasi, pada Akses Data, jadi kami memutuskan untuk menggunakan Akses Data , dan memiliki Akses Data tergantung pada domain.



Di sini saya berlatih menggambar dan menggambar arsitektur bawang, tetapi tidak secara klasik dengan "cincin". Saya mendapatkan sesuatu di antara poligon dan lingkaran. Saya melakukan ini hanya untuk menunjukkan bahwa jika Anda menemukan kata "bawang", "heksagonal" atau "port dan adaptor" - ini semua adalah satu dan sama. Intinya adalah bahwa domain ada di tengah, itu dibungkus layanan, mereka bisa menjadi layanan domain atau aplikasi, yang Anda inginkan. Dan dunia luar dalam bentuk UI, tes dan infrastruktur tempat DAL pindah ke - mereka berkomunikasi dengan domain melalui lapisan layanan ini.

Contoh sederhana. Pembaruan email


Mari kita lihat bagaimana kasus penggunaan yang sederhana akan terlihat dalam paradigma seperti itu - memperbarui alamat email pengguna.



Kami perlu mengirim permintaan, memvalidasi, memperbarui nilai dalam database, mengirim pemberitahuan ke email baru: "Semuanya beres, Anda mengubah email, kami tahu semuanya baik-baik saja", dan membalas browser "200" - semuanya baik-baik saja.



Kode mungkin terlihat seperti ini. Di sini kita memiliki validasi ASP.NET MVC standar, ada ORM untuk membaca dan memperbarui data, dan ada beberapa jenis pengirim email yang mengirimkan pemberitahuan. Sepertinya semuanya baik-baik saja, kan? Satu peringatan - di dunia yang ideal.

Di dunia nyata, situasinya sedikit berbeda. Intinya adalah untuk menambahkan otorisasi, pengecekan kesalahan, pemformatan, masuk dan profil. Ini semua tidak ada hubungannya dengan use case kami, tetapi semua harus demikian. Dan sepotong kecil kode itu menjadi besar dan menakutkan: dengan banyak sarang, dengan banyak kode, dengan fakta bahwa itu sulit dibaca, dan yang paling penting, bahwa ada lebih banyak kode infrastruktur daripada kode domain.



"Di mana layanannya?" - katamu. Saya menulis semua logika ke pengendali. Tentu saja, ini masalah, sekarang saya akan menambahkan layanan, dan semuanya akan baik-baik saja.



Kami menambahkan layanan, dan itu benar-benar menjadi lebih baik, karena alih-alih memakai alas kaki yang besar, kami mendapat satu baris kecil yang indah.

Apakah sudah membaik? Sudah menjadi! Dan sekarang kita dapat menggunakan kembali metode ini di pengontrol yang berbeda. Hasilnya jelas. Mari kita lihat implementasi dari metode ini.



Tapi di sini semuanya tidak begitu baik. Kode ini masih di sini. Kami baru saja mentransfer hal yang sama ke layanan. Kami memutuskan untuk tidak memecahkan masalah, tetapi hanya untuk menyamarkannya dan memindahkannya ke tempat lain. Itu saja.



Selain itu, beberapa pertanyaan lain muncul. Haruskah kita melakukan validasi di controller atau di sini? Yah, semacam suka, di controller. Dan jika Anda perlu pergi ke database dan melihat bahwa ada ID atau tidak ada pengguna lain dengan email seperti itu? Hmm, kalau begitu dalam layanan. Tetapi penanganan kesalahan di sini? Penanganan kesalahan ini mungkin ada di sini, dan penanganan kesalahan yang akan merespons ke browser di pengontrol. Dan metode SaveChanges, apakah ada dalam layanan atau Anda perlu mentransfernya ke controller? Mungkin begitu dan begitu, karena jika satu layanan dipanggil, lebih logis untuk memanggil layanan, dan jika Anda memiliki tiga metode layanan di controller yang perlu Anda panggil, maka Anda perlu memanggilnya di luar layanan ini sehingga transaksi itu adalah satu. Refleksi ini menunjukkan bahwa mungkin lapisan tidak menyelesaikan masalah.



Dan ide ini terpikir oleh lebih dari satu orang. Jika Anda google, setidaknya tiga dari suami terhormat ini menulis tentang hal yang sama. Dari atas ke bawah: Stephen .NET Junkie (sayangnya, saya tidak tahu nama belakangnya, karena dia tidak muncul di mana pun di Internet), penulis wadah IoC Simple Injector . Berikutnya Jimmy Bogard adalah penulis AutoMapper . Dan di bawah ini adalah Scott Vlashin, penulis F # untuk kesenangan dan keuntungan .



Semua orang ini berbicara tentang hal yang sama dan menyarankan untuk membangun aplikasi bukan berdasarkan lapisan, tetapi berdasarkan kasus penggunaan, yaitu, persyaratan yang diminta oleh bisnis. Dengan demikian, use case dalam C # dapat ditentukan menggunakan antarmuka IHandler. Ini memiliki nilai input, ada nilai output dan ada metode itu sendiri yang benar-benar mengeksekusi use case ini.



Dan di dalam metode ini bisa ada model domain, atau beberapa model denormalized untuk membaca, mungkin dengan Dapper atau dengan Pencarian Elastis, jika Anda perlu mencari sesuatu, dan mungkin Anda memiliki Legacy -sistem dengan prosedur tersimpan - tidak ada masalah, serta permintaan jaringan - baik, secara umum, apa pun yang mungkin Anda butuhkan di sana. Tetapi jika tidak ada lapisan, apa yang harus dilakukan?



Untuk memulai, mari kita hilangkan UserService. Kami menghapus metode dan membuat kelas. Dan kami akan menghapusnya, dan kami akan menghapusnya lagi. Dan kemudian ambil dan hapus kelas.



Mari kita berpikir, apakah kelas-kelas ini setara atau tidak? Kelas GetUser mengembalikan data dan tidak mengubah apa pun di server. Ini, misalnya, tentang permintaan "Beri saya ID pengguna." Kelas UpdateEmail dan BanUser mengembalikan hasil operasi dan mengubah status. Misalnya, ketika kami memberi tahu server: "Silakan ubah status, Anda perlu mengubah sesuatu."



Mari kita lihat protokol HTTP. Ada metode GET, yang, sesuai dengan spesifikasi protokol HTTP, harus mengembalikan data dan tidak mengubah keadaan server.



Dan ada metode lain yang dapat mengubah status server dan mengembalikan hasil operasi.



Paradigma CQRS tampaknya dirancang khusus untuk protokol HTTP. Kueri adalah operasi GET, dan perintah adalah PUT, POST, HAPUS - tidak perlu menemukan apa pun.



Kami mendefinisikan kembali Handler kami dan mendefinisikan antarmuka tambahan. IQueryHandler, yang hanya berbeda dalam hal kita menggantungkan kendala bahwa jenis nilai input adalah IQuery. IQuery adalah antarmuka penanda, tidak ada di dalamnya kecuali generik ini. Kita memerlukan generik untuk menempatkan batasan di QueryHandler, dan sekarang, mendeklarasikan QueryHandler, kita tidak bisa lewat di sana bukan di Query, tetapi melewati objek Query di sana, kita tahu nilai baliknya. Ini nyaman jika Anda hanya memiliki satu antarmuka, sehingga Anda tidak perlu mencari implementasinya dalam kode, dan sekali lagi agar tidak berantakan. Anda menulis IQueryHandler, menulis implementasi di sana, dan di TOut Anda tidak bisa mengganti tipe nilai pengembalian yang lain. Itu tidak mengkompilasi. Dengan demikian, Anda dapat langsung melihat nilai input mana yang sesuai dengan data input mana.



Situasinya sangat mirip untuk CommandHandler dengan satu pengecualian: generik ini diperlukan untuk satu trik lagi, yang akan kita lihat sedikit lebih jauh.

Implementasi Handler


Penangan, kami mengumumkan, apa implementasi mereka?



Apakah ada masalah? Sepertinya ada yang gagal.

Dekorator bergegas untuk menyelamatkan


Tapi itu tidak membantu, karena kita masih di tengah jalan, kita harus menyelesaikan sedikit lagi, dan kali ini kita perlu menggunakan pola dekorator , yaitu fitur tata letak yang indah. Dekorator dapat dibungkus dekorator, dibungkus dekorator, dibungkus dekorator - terus sampai Anda bosan.



Maka semuanya akan terlihat seperti ini: ada input Dto, memasuki dekorator pertama, kedua, ketiga, kemudian kita masuk ke Handler dan juga keluar, pergi melalui semua dekorator dan mengembalikan Dto di browser. Kami mendeklarasikan kelas dasar abstrak untuk kemudian mewarisi, tubuh Handler diteruskan ke konstruktor, dan kami mendeklarasikan metode Handle abstrak, di mana logika dekorator tambahan akan digantung.



Sekarang dengan bantuan dekorator Anda dapat membangun saluran pipa secara keseluruhan. Mari kita mulai dengan tim. Apa yang kita miliki Nilai input, validasi, verifikasi hak akses, logika itu sendiri, beberapa peristiwa yang terjadi sebagai akibat dari logika ini, dan nilai pengembalian.



Mari kita mulai dengan validasi. Kami mendeklarasikan dekorator. IEnumerable dari validator tipe T masuk ke dalam konstruktor dekorator ini. Kami menjalankan semuanya, memeriksa apakah validasi gagal dan tipe kembalinya IEnumerable<validationresult> , maka kita dapat mengembalikannya karena jenisnya cocok. Dan jika itu adalah Hander lain, maka Anda harus melemparkan Pengecualian, karena tidak ada hasil di sini, jenis nilai pengembalian lainnya.



Langkah selanjutnya adalah Keamanan. Kami juga mendeklarasikan dekorator, membuat metode CheckPermission, dan memverifikasi. Jika tiba-tiba terjadi kesalahan, semuanya, kami tidak melanjutkan. Sekarang, setelah kami menyelesaikan semua pemeriksaan dan yakin semuanya baik-baik saja, kami dapat memenuhi logika kami.

Obsesi dengan primitif


Sebelum menunjukkan implementasi logika, saya ingin memulai sedikit lebih awal, yaitu dengan nilai input yang datang ke sana.



Sekarang, jika kita memilih kelas seperti itu, maka yang paling sering terlihat seperti ini. Setidaknya kode yang saya lihat dalam pekerjaan sehari-hari.



Agar validasi berfungsi, kami menambahkan beberapa atribut di sini yang memberi tahu Anda seperti apa validasinya. Ini akan membantu dari sudut pandang struktur data, tetapi tidak akan membantu dengan validasi seperti memeriksa nilai dalam database. Ini hanya EmailAddress, tidak jelas caranya, di mana memeriksa bagaimana menggunakan atribut ini untuk pergi ke database. Alih-alih atribut, Anda bisa pergi ke tipe khusus, maka masalah ini akan terpecahkan.



Alih-alih int primitif, kami mendeklarasikan tipe Id yang memiliki generik yang merupakan entitas tertentu dengan kunci int. Dan kami meneruskan entitas ini ke konstruktor, atau meneruskan Id-nya, tetapi pada saat yang sama kami harus melewati fungsi yang oleh Id dapat diambil dan dikembalikan, periksa apakah ada nol di sana atau tidak nol.



Kami melakukan hal yang sama dengan Email. Konversikan semua email ke garis bawah sehingga semuanya terlihat sama untuk kita. Selanjutnya, kita mengambil atribut Email, menyatakannya sebagai statis untuk kompatibilitas dengan validasi ASP.NET, dan di sini kita sebut saja. Artinya, ini juga bisa dilakukan. Agar infrastruktur ASP.NET dapat menangkap semua ini, Anda harus sedikit mengubah serialisasi dan / atau ModelBinding. Tidak ada banyak kode di sana, relatif sederhana, jadi saya tidak akan berhenti di situ.



Setelah perubahan ini, alih-alih tipe primitif, tipe khusus muncul di sini: Id dan Email. Dan setelah ModelBinder ini dan deserializer yang diperbarui berhasil, kami tahu pasti bahwa nilai-nilai ini benar, termasuk bahwa nilai-nilai tersebut ada di database. "Invarian"



Poin berikutnya yang ingin saya bahas adalah keadaan invarian di kelas, karena cukup sering model anemia digunakan, di mana hanya ada kelas, banyak pengambil-penentu, sama sekali tidak jelas bagaimana mereka harus bekerja sama. Kami bekerja dengan logika bisnis yang kompleks, jadi penting bagi kami bahwa kode tersebut mendokumentasikan sendiri. Sebaliknya, lebih baik untuk mendeklarasikan konstruktor nyata bersama dengan kosong untuk ORM, itu dapat dinyatakan dilindungi sehingga pemrogram dalam kode aplikasi mereka tidak bisa menyebutnya, dan ORM bisa. Di sini kita melewati bukan tipe primitif, tetapi tipe Email, sudah benar dengan benar, jika itu nol, kita masih membuang Pengecualian. Anda dapat menggunakan beberapa Fody, PostSharp, tetapi C # 8 akan segera hadir. Dengan demikian, akan ada jenis referensi yang tidak dapat dibatalkan, dan lebih baik menunggu dukungannya dalam bahasa tersebut. Saat berikutnya, jika kita ingin mengubah nama dan nama keluarga, kemungkinan besar kita ingin mengubahnya bersama, jadi harus ada metode publik yang tepat yang mengubahnya bersama-sama.



Dalam metode publik ini, kami juga memverifikasi bahwa panjang garis-garis ini cocok dengan apa yang kami gunakan dalam database. Dan jika ada sesuatu yang salah, maka hentikan eksekusi. Di sini saya menggunakan trik yang sama. Saya mendeklarasikan atribut khusus dan memanggilnya dalam kode aplikasi.



Selain itu, atribut tersebut dapat digunakan kembali dalam Dto. Sekarang, jika saya ingin mengubah nama dan nama keluarga, saya mungkin memiliki perintah perubahan seperti itu. Apakah perlu menambahkan konstruktor khusus di sini? Tampaknya sepadan. Ini akan menjadi lebih baik, tidak ada yang akan mengubah nilai-nilai ini, tidak akan merusaknya, mereka akan benar.



Sebenarnya tidak juga. Faktanya adalah Dto bukan objek sama sekali. Ini adalah kamus tempat kami menaruh data deserialisasi. Yaitu, mereka berpura-pura menjadi objek, tentu saja, tetapi mereka hanya memiliki satu tanggung jawab - harus serial dan deserialized. Jika kami mencoba melawan struktur ini, kami akan mulai mengumumkan beberapa ModelBinders dengan desainer, untuk melakukan sesuatu seperti ini sangat melelahkan, dan, yang paling penting, itu akan pecah dengan rilis baru kerangka kerja baru. Semua ini dijelaskan dengan baik oleh Mark Simon dalam artikel "Di perbatasan program tidak berorientasi objek" , jika menarik, lebih baik membaca posnya, di sana dijelaskan secara rinci.



Singkatnya, kita memiliki dunia eksternal yang kotor, kita memberi tanda pada input, mengubahnya menjadi model bersih kita, dan kemudian mentransfer semuanya kembali ke serialisasi, ke browser, lagi ke dunia eksternal yang kotor.

Handler


Setelah semua perubahan ini dilakukan, bagaimana Hander akan terlihat seperti di sini?



Saya menulis dua baris di sini untuk membuatnya lebih mudah dibaca, tetapi secara umum dapat ditulis dalam satu. Datanya benar, karena kami memiliki sistem tipe, ada validasi, yaitu data yang diperkuat beton, Anda tidak perlu memeriksanya lagi. Pengguna seperti itu juga ada, tidak ada pengguna lain dengan email yang sibuk, semuanya bisa dilakukan. Namun, masih belum ada panggilan ke metode SaveChanges, tidak ada pemberitahuan dan tidak ada log dan profiler, kan? Kami melanjutkan.

Acara


Peristiwa domain.



Mungkin pertama kali konsep ini dipopulerkan oleh Udi Dahan dalam postingannya "Acara Domain - Keselamatan" . Di sana, ia menyarankan hanya mendeklarasikan kelas statis dengan metode Naikkan dan melempar peristiwa seperti itu. Beberapa saat kemudian, Jimmy Bogard mengusulkan implementasi yang lebih baik, disebut "Pola peristiwa domain yang lebih baik" .



Saya akan menunjukkan serialisasi Bogard dengan satu perubahan kecil, tetapi yang penting. Alih-alih melempar peristiwa, kita dapat mendeklarasikan beberapa daftar, dan di tempat-tempat di mana semacam reaksi harus terjadi, langsung di dalam entitas untuk menyimpan peristiwa ini. Dalam hal ini, pengambil email ini juga merupakan kelas Pengguna, dan kelas ini, properti ini tidak berpura-pura menjadi properti dengan pengambil dan setter otomatis, tetapi benar-benar menambahkan sesuatu pada ini. Artinya, ini adalah enkapsulasi nyata, bukan senonoh. Saat berubah, kami memeriksa apakah emailnya berbeda dan mengadakan acara. Acara ini belum mencapai tempat, kami hanya memilikinya di daftar internal entitas.



Selanjutnya, pada saat kita akan memanggil metode SaveChanges, kita mengambil ChangeTracker, melihat apakah ada entitas yang mengimplementasikan antarmuka, apakah mereka memiliki peristiwa domain. Dan jika ada, mari kita ambil semua acara domain ini dan mengirimkannya ke operator yang tahu apa yang harus dilakukan dengan mereka.

Pelaksanaan dispatcher ini adalah topik untuk diskusi lain, ada beberapa kesulitan dengan pengiriman ganda di C #, tetapi ini juga dilakukan. Dengan pendekatan ini, ada keuntungan lain yang tidak jelas. Sekarang, jika kita memiliki dua pengembang, satu dapat menulis kode yang mengubah email ini, dan yang lain dapat melakukan modul notifikasi. Mereka sama sekali tidak terhubung satu sama lain, mereka menulis kode yang berbeda, mereka hanya terhubung pada tingkat acara domain ini dari satu kelas Dto. Pengembang pertama hanya membuang kelas ini di beberapa titik, yang kedua meresponsnya dan tahu bahwa itu perlu dikirim melalui email, SMS, push notification ke telepon dan semua juta notifikasi lainnya, dengan mempertimbangkan preferensi pengguna yang biasanya terjadi.



Inilah poin terkecil, tetapi penting. Artikel Jimmy menggunakan kelebihan metode SaveChanges, dan lebih baik tidak melakukannya. Dan lebih baik melakukannya di dekorator, karena jika kita membebani metode SaveChanges dan kita membutuhkan dbContext di Handler, kita akan mendapatkan dependensi melingkar. Anda dapat bekerja dengan ini, tetapi solusinya sedikit kurang nyaman dan sedikit kurang indah. Karena itu, jika pipa dibangun di atas dekorator, maka saya tidak melihat alasan untuk melakukannya secara berbeda.

Penebangan dan Pembuatan Profil




Sarang kode tetap ada, tetapi pada contoh awal kita pertama kali menggunakan MiniProfiler, lalu coba tangkap, lalu jika. Total ada tiga tingkat bersarang, sekarang masing-masing tingkat ini bersarang di dekorator sendiri. Dan di dalam dekorator, yang bertanggung jawab untuk pembuatan profil, kami hanya memiliki satu tingkat sarang, kode dibaca dengan sempurna. Selain itu, jelas bahwa dalam dekorator ini hanya ada satu tanggung jawab. Jika dekorator bertanggung jawab untuk login, maka ia hanya akan login, jika untuk profiling, masing-masing, hanya profil, yang lainnya ada di tempat lain.

Tanggapan


Setelah seluruh pipeline bekerja, kita hanya bisa mengambil Dto dan mengirimkannya ke browser lebih lanjut, membuat serialisasi JSON.



Tetapi satu hal kecil lagi, hal semacam itu yang kadang-kadang dilupakan: pada setiap tahap, Pengecualian dapat terjadi di sini, dan sebenarnya Anda perlu untuk menanganinya.



Saya tidak bisa tidak menyebutkan Scott Vlashin dan laporannya "pemrograman berorientasi kereta api" di sini lagi. Mengapa Laporan asli sepenuhnya dikhususkan untuk bekerja dengan kesalahan dalam bahasa F #, cara mengatur aliran sedikit berbeda dan mengapa pendekatan seperti itu lebih baik daripada menggunakan Exception'ov. Dalam F #, ini benar-benar bekerja dengan sangat baik, karena F # adalah bahasa fungsional, dan Scott menggunakan fungsionalitas bahasa fungsional.



Karena, mungkin, sebagian besar dari Anda masih menulis dalam C #, jika Anda menulis analog dalam C # , maka pendekatan ini akan terlihat seperti ini. Alih-alih melempar pengecualian, kami mendeklarasikan kelas Hasil yang memiliki cabang yang sukses dan cabang yang tidak berhasil. Dengan demikian, dua desainer. Kelas hanya bisa dalam satu negara. Kelas ini adalah kasus khusus dari jenis persatuan, persatuan terdiskriminasi dari F #, tetapi ditulis ulang dalam C #, karena tidak ada dukungan bawaan di C #.



Alih-alih menyatakan getter publik bahwa seseorang mungkin tidak memeriksa nol dalam kode, Pencocokan Pola digunakan. Sekali lagi, dalam F # itu akan menjadi bahasa Pencocokan Pola bawaan, dalam C # kita harus menulis metode terpisah di mana kita akan melewati satu fungsi yang tahu apa yang harus dilakukan dengan hasil operasi yang sukses, bagaimana mengubahnya lebih jauh ke bawah rantai, dan dengan kesalahan. Yaitu, tidak masalah cabang mana yang bekerja untuk kita, kita harus memberikan ini ke satu hasil yang dikembalikan. Dalam F #, ini semua bekerja dengan sangat baik, karena ada komposisi fungsional, baik, dan segala sesuatu yang telah saya daftarkan. Di .NET, ini bekerja sedikit lebih buruk, karena segera setelah Anda memiliki lebih dari satu Hasil, tetapi banyak - dan hampir setiap metode dapat gagal karena satu alasan atau yang lain - hampir semua jenis fungsi yang dihasilkan menjadi jenis Hasil, dan Anda memerlukannya sebagai untuk menggabungkan sesuatu.



Cara termudah untuk menggabungkan mereka adalah dengan menggunakan LINQ , karena sebenarnya LINQ tidak hanya bekerja dengan IEnumerable, jika Anda mendefinisikan kembali metode SelectMany dan Select dengan cara yang benar, maka kompiler C # akan melihat bahwa Anda dapat menggunakan sintaks LINQ untuk jenis ini. Secara umum, ternyata kertas kalkir dengan notasi Haskell atau dengan Ekspresi Komputasi yang sama dalam F #. Bagaimana ini harus dibaca? Di sini kita memiliki tiga hasil operasi, dan jika semuanya baik-baik saja di ketiga kasus, maka ambil hasil ini r1 + r2 + r3 dan tambahkan. Jenis nilai yang dihasilkan juga akan menjadi Hasil, tetapi Hasil baru, yang kami nyatakan dalam Pilih. Secara umum, ini bahkan merupakan pendekatan yang berfungsi, jika bukan hanya satu tetapi.



Untuk semua pengembang lain, segera setelah Anda mulai menulis kode seperti itu di C #, Anda mulai terlihat seperti ini. “Ini adalah Pengecualian menakutkan yang buruk, jangan tulis mereka! Mereka jahat! Lebih baik tulis kode yang tidak ada yang mengerti dan tidak bisa men-debug! "



C # bukan F #, ini agak berbeda, tidak ada konsep yang berbeda atas dasar ini dilakukan, dan ketika kami mencoba menarik burung hantu di dunia seperti itu, ternyata yang paling tidak, tidak biasa.



Sebagai gantinya, Anda dapat menggunakan alat normal bawaan yang didokumentasikan, yang diketahui semua orang dan tidak akan menyebabkan disonansi kognitif di antara pengembang. ASP.NET memiliki Pengecualian Handler global.



Kami tahu bahwa jika ada masalah dengan validasi, Anda harus mengembalikan kode 400 atau 422 (Entitas yang tidak dapat diproses). Jika ada masalah dengan otentikasi dan otorisasi, ada 401 dan 403. Jika terjadi kesalahan, maka ada yang salah. Dan jika ada yang tidak beres dan Anda ingin memberi tahu pengguna apa tepatnya, tentukan jenis Pengecualian Anda, katakan bahwa itu adalah IHasUserMessage, nyatakan Pengambil pesan di antarmuka ini dan cukup periksa: jika antarmuka ini diimplementasikan, maka Anda dapat menerima pesan dari Exception dan berikan JSON kepada pengguna. Jika antarmuka ini tidak diimplementasikan, itu berarti ada semacam kesalahan sistem, dan kami hanya memberi tahu pengguna bahwa ada kesalahan, kami sudah melakukannya, kita semua tahu - yah, seperti biasa.

Pipa permintaan


Kami menyimpulkan ini dengan tim dan melihat apa yang kami miliki di Read-stack. Adapun permintaan, validasi, tanggapan langsung - ini adalah tentang hal yang sama, kami tidak akan berhenti secara terpisah. Mungkin masih ada cache tambahan, tetapi secara umum tidak ada masalah besar dengan cache juga.

Keamanan


Mari kita lihat pemeriksaan keamanan dengan lebih baik. Mungkin juga ada dekorator Keamanan yang sama, yang memeriksa apakah permintaan ini dapat dibuat atau tidak:



Tetapi ada kasus lain di mana kami menampilkan lebih dari satu catatan, dan daftar, dan untuk beberapa pengguna kami perlu menampilkan daftar lengkap (misalnya, untuk beberapa administrator super), dan untuk pengguna lain kami harus membuat daftar daftar terbatas, terbatas ketiga ke yang lain, baik, dan seperti yang sering terjadi dalam aplikasi perusahaan, hak akses bisa sangat canggih, jadi Anda harus yakin bahwa data yang tidak menargetkan pengguna ini tidak masuk ke daftar ini.

Masalahnya diselesaikan dengan cukup sederhana . Kami dapat mendefinisikan kembali antarmuka (IPermissionFilter) ke mana permintaan asli datang dan pengembalian yang dapat diminta. Perbedaannya adalah bahwa untuk queryable yang kembali, kami telah memberlakukan kondisi tambahan di mana, memeriksa pengguna saat ini dan berkata: "Di sini, kembalikan hanya data itu ke pengguna itu ..." - dan kemudian semua logika Anda terkait dengan izin . Sekali lagi, jika Anda memiliki dua programmer, satu programmer pergi untuk menulis izin, ia tahu bahwa ia perlu menulis banyak izinFilters dan memeriksa apakah mereka bekerja dengan benar untuk semua entitas. Dan programmer lain tidak tahu apa-apa tentang izin, dalam daftar mereka data yang benar selalu berlalu, itu saja. Karena mereka menerima input bukan lagi queryable asli dari dbContext, tetapi terbatas pada filter. IzinFilter tersebut juga memiliki properti tata letak, kita dapat menambahkan dan menerapkan semua permitFilters. Sebagai hasilnya, kami mendapatkan izinFilter yang dihasilkan, yang akan mempersempit pemilihan data secara maksimal, dengan mempertimbangkan semua kondisi yang sesuai untuk entitas ini.



Mengapa tidak melakukannya dengan alat bawaan ORM, misalnya, Filter Global dalam kerangka entitas? Sekali lagi, agar Anda tidak membuat ketergantungan siklik untuk diri sendiri dan tidak menyeret cerita tambahan tentang lapisan bisnis Anda ke dalam konteks.

Pipa Permintaan. Baca model


Masih melihat model membaca. Paradigma CQRS tidak menggunakan model domain dalam tumpukan membaca, sebagai gantinya, kami hanya langsung membuat Dto yang dibutuhkan browser saat ini.



Jika kami menulis dalam C #, maka kemungkinan besar kami menggunakan LINQ, jika tidak ada persyaratan kinerja yang mengerikan, dan jika ada, maka Anda mungkin tidak memiliki aplikasi perusahaan. Secara umum, masalah ini dapat diselesaikan sekali dan untuk semua dengan LinqQueryHandler. Inilah kendala yang cukup menyeramkan pada generik: ini adalah Query, yang mengembalikan daftar proyeksi, dan masih dapat memfilter proyeksi ini dan mengurutkan proyeksi ini. Dia juga hanya bekerja dengan beberapa jenis entitas dan tahu cara mengubah entitas ini menjadi proyeksi dan mengembalikan daftar proyeksi tersebut dalam bentuk Dto ke browser.



Implementasi metode Handle bisa sangat sederhana. Untuk jaga-jaga, periksa apakah filter TQuery ini mengimplementasikan entitas asli. Selanjutnya kami melakukan proyeksi, itu adalah ekstensi AutoMapper'a. Jika seseorang masih tidak tahu, AutoMapper dapat membangun proyeksi di LINQ, yaitu orang-orang yang akan membangun metode Pilih, dan tidak memetakannya dalam memori.

Lalu kami menerapkan pemfilteran, menyortir, dan menampilkan semuanya di browser. , DotNext, , , , , , expression' , .

SQL


. , DotNext', — SQL. Select , , , queryable- .



, . , Title, Title , . , . SubTitle, , , - , queryable- . , .

, . , , . , , . «JsonIgnore», . , , Dto. , , . JSON, , Created LastUpdated , SubTitle — , . , , , , , . , - .



. , -, , . , pipeline, . — , , . , SaveChanges, Query SaveChanges. , , , NuGet, .

. , - , , . , , , , , — . , , : « », — . .


, ?



- . .



, , , . MediatR , . , , — , MediatR pipeline behaviour. , Request/Response, RequestHandler' . Simple Injector, — .



, , , , TIn: ICommand.



Simple Injector' constraint' . , , , constraint', Handler', constraint. , constraint ICommand, SaveChanges constraint' ICommand, Simple Injector , constraint' , Handler'. , , , .

? Simple Injector MeriatR — , , Autofac', -, , , . , .

,


, «».



, «Clean architecture». .



- - , MVC, , .



, , , Angular, , , , . , : « — MVC-», : « Features, : , Blog - Import, - ».

, , , , MVC-, , - , . MVC . , , — . .





- , - -, .

-, , . , . , - , User Service, pull request', , User Service , . , - , - , . - , .

. , . , , , . , , , , , , , - . , ( , ), , «Delete»: , , . .

— «», , , , . , : , , , . , . , , . , , .

: . « », : , , . , , , , , , , . , . , - pull request , — , — - , . VCS : - , ? , - , , .



, , , . : . , . , , , , . , , , . , , . « », , . , , — , , .

: , - , . . - , , , , . - , - , , , , . .



. , IHandler . .

IHandler ICommandHandler IQueryHandler , . , , . , CommandHandler, CommandHandler', .

Kenapa begitu , Query , Query — . , , , Hander, CommandHandler QueryHandler, - use case, .

— , , , , : , .

, . , . , -.

C# 8, nullable reference type . , , , , .

ChangeTracker' ORM.

Exception' — , F#, C#. , - , - , . , , Exception', , LINQ, , , , , , Dapper - , , , .NET.

, LINQ, , permission' — . , , - , , . , — .

. :






— . . — «Domain Modeling Made Functional», F#, F#, , , , , . C# , , Exception'.

, , — «Entity Framework Core In Action». , Entity Framework, , DDD ORM, , ORM DDD .

Menit periklanan. 15-16 2019 .NET- DotNext Piter, . , .

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


All Articles