Halo para pembaca! Pada artikel ini saya ingin berbicara tentang arsitektur proyek saya, yang saya refactored 4 kali pada peluncurannya, karena saya tidak puas dengan hasilnya. Saya akan berbicara tentang kelemahan pendekatan populer dan menunjukkan pendekatan saya sendiri.
Saya ingin segera mengatakan bahwa ini adalah artikel pertama saya, saya tidak mengatakan apa yang harus dilakukan seperti saya - benar. Saya hanya ingin menunjukkan apa yang saya lakukan, memberi tahu bagaimana hasil akhir saya, dan yang paling penting - dapatkan pendapat orang lain.
Saya bekerja dalam beberapa kampanye dan melihat banyak hal yang akan saya lakukan secara berbeda.
Sebagai contoh, saya sering melihat arsitektur N-Layer, ada lapisan untuk bekerja dengan data (DA), ada lapisan dengan logika bisnis (BL) yang berfungsi menggunakan DA dan mungkin beberapa layanan lain, dan ada juga lapisan tampilan \ API di mana permintaan diterima, diproses menggunakan BL. Tampaknya nyaman, tetapi melihat kode saya melihat situasi ini:
- [DA] menarik \ menulis \ mengubah data, bahkan jika kueri yang rumit - OK
- [BL] 80% memanggil 1 metode dan menggulung hasilnya di atas - Mengapa ini lapisan kosong?
- [View] 80% Panggilan 1 metode BL melemparkan hasil di atas - Mengapa ini lapisan kosong?
Selain itu, sangat modis untuk membungkus antarmuka sehingga nanti Anda dapat mengunci dan menguji - wow, wow!
- Kenapa basah?
- Nah, untuk memotong efek samping selama tes berlangsung.
- Artinya, kita akan protes tanpa efek samping, tetapi dalam dorongan dengan mereka?
...
Ini adalah hal dasar yang saya tidak suka dalam arsitektur ini, karena untuk menyelesaikan masalah seperti: "Daftar suka pengguna" adalah proses besar, tetapi pada kenyataannya ada 1 permintaan dalam database dan kemungkinan pemetaan.
Solusi sampel1) [DA] Tambahkan permintaan ke DA
2) [BL] Teruskan tanggapan DA
3) [Lihat] Meneruskan hasil BA, dapat mempromosikan
Jangan lupa bahwa semua metode ini masih perlu ditambahkan ke antarmuka, kami sedang menulis sebuah proyek untuk menjadi basah, dan bukan untuk solusi.
Di tempat lain, saya melihat implementasi API dengan pendekatan CQRS.
Solusinya tidak terlihat buruk, 1 folder - 1 fitur. Seorang pengembang membuat fitur duduk di foldernya dan hampir selalu bisa melupakan pengaruh kode-nya pada fitur lain, tetapi ada begitu banyak file yang hanya mimpi buruk. Model permintaan / respons, validator, helper, logika itu sendiri. Cari di studio praktis menolak untuk bekerja, ekstensi dimasukkan untuk menemukan hal-hal yang diperlukan dalam kode.
Ada banyak lagi yang bisa dikatakan, tetapi saya menyoroti alasan utama yang membuat saya menolaknya
Dan akhirnya ke proyek saya
Seperti yang saya katakan, saya refactored proyek saya beberapa kali, pada saat itu saya mempunyai "programmer depresi", saya tidak senang dengan kode saya, dan refactored itu, lagi dan lagi, pada akhirnya saya mulai menonton video tentang arsitektur aplikasi untuk melihat bagaimana yang lain melakukannya. Saya menemukan laporan Anton Moldovan tentang DDD dan pemrograman fungsional, dan berpikir: "Ini dia, saya butuh F #!".
Setelah menghabiskan beberapa hari di F #, saya menyadari bahwa, pada prinsipnya, saya akan melakukan hal yang sama di C # dan tidak lebih buruk. Video menunjukkan:
- Ini kode C #, itu omong kosong
- Inilah F # cool, less wrote - super.
Tetapi masalahnya adalah bahwa solusi pada F # diimplementasikan secara berbeda, dan terhadap ini mereka menunjukkan implementasi yang buruk pada C #. Prinsip utama adalah bahwa BL bukanlah sesuatu yang memanggil layanan DA dan melakukan semua pekerjaan, tetapi itu adalah fungsi murni .
Tentu saja F # bagus, saya menyukai beberapa fitur tetapi, seperti C #, ini hanya alat yang dapat digunakan dengan cara yang berbeda.
Dan saya kembali ke C # dan mulai membuat.
Saya menciptakan proyek-proyek semacam itu dalam solusinya:
- API
- Inti
- Layanan
- Tes
Saya juga menggunakan fitur C # 8, terutama jenis refensi nullable, saya akan menunjukkan aplikasinya.
Secara singkat tentang tugas lapisan yang saya berikan kepada mereka.
API
1) Menerima permintaan, model permintaan + validasi, batasan
2) Memanggil fungsi dari Core dan Layanan
Lebih detail
Di sini kita melihat kode yang sederhana dan mudah dibaca, saya pikir semua orang akan mengerti apa yang tertulis di sini.
Pola yang jelas diamati
1) Dapatkan data
2) Memproses, memodifikasi, dll. - Bagian ini perlu diuji.
3) Simpan.
3) Pemetaan, jika perlu
4) Penanganan kesalahan (logging + respons manusia)
Lebih detailKelas ini berisi semua kemungkinan kesalahan aplikasi yang ditanggapi oleh penangan pengecualian.


Ternyata aplikasi tersebut berfungsi, atau memberikan kesalahan tertentu, dan bukan kesalahan yang diproses adalah efek samping atau bug, log kesalahan tersebut langsung terbang ke saya di telegram dalam obrolan dengan bot.
Saya memiliki AppError.Kasus kesalahan ini untuk kasus yang tidak jelas.
Saya memiliki CallBack dari layanan lain, itu akan memiliki userId di sistem saya, dan jika saya tidak menemukan pengguna dengan ID ini, entah sesuatu terjadi pada pengguna atau tidak jelas sama sekali, kesalahan seperti itu terbang kepada saya seperti KRITIS, secara teori seharusnya tidak untuk muncul, tetapi jika itu terjadi, itu memerlukan intervensi saya.

Inti, yang paling menarik
Saya selalu berpikir bahwa BLs hanyalah fungsi yang memberikan hasil yang sama dengan input yang sama. Kompleksitas kode pada lapisan ini adalah pada tingkat pekerjaan laboratorium, bukan fungsi-fungsi hebat yang dengan jelas dan tanpa kesalahan melakukan tugasnya. Dan penting bahwa tidak ada efek samping di dalam fungsi, semua fungsi yang diperlukan adalah parameternya.
Jika fungsi membutuhkan keseimbangan pengguna, maka KAMI mendapatkan keseimbangan, dan mentransfernya ke fungsi, dan JANGAN mendorong layanan pengguna ke BL.
1) Tindakan dasar entitas
Lebih detail

Saya datang dengan metode sebagai metode ekstensi sehingga kelas tidak mengasapi, dan fungsi dapat dikelompokkan berdasarkan fitur.


Saya menganggap konstruksi model entitas yang baik menjadi topik yang sama pentingnya.
Sebagai contoh, saya memiliki pengguna, pengguna memiliki saldo dalam beberapa mata uang. Salah satu keputusan khas yang saya ambil tanpa ragu-ragu adalah esensi "Saldo" dan hanya menempatkan serangkaian saldo di pengguna. Tapi kenyamanan macam apa yang membawa keputusan seperti itu?
1) Menambah / menghapus mata uang. Tugas ini segera berarti bagi kami tidak hanya menulis kode baru, tetapi juga migrasi, dengan mengisi / menghapus semua pengguna yang ada, dan ini adalah opsi termudah. Tuhan melarang, untuk menambahkan mata uang baru, Anda harus membuat tombol untuk pengguna, yang dia klik dan memulai penciptaan dompet baru untuk beberapa jenis proses bisnis. Akibatnya, hanya perlu memperluas enum untuk mata uang baru, dan mereka menulis fitur lain untuk membuat dompet dengan sebuah tombol, mereka melemparkan tugas lain ke depan.
2) Dalam kode, konstanta FirstOrDefault (s => s.Currency == mata uang) dan memeriksa nol
Keputusan saya
Dengan model itu sendiri, saya menjamin diri saya bahwa tidak akan ada saldo nol, dan dengan membuat operator pengindeks saya menyederhanakan kode saya di semua tempat interaksi dengan saldo.
Layanan
Lapisan ini memberi saya alat yang nyaman untuk bekerja dengan berbagai layanan.
Dalam proyek saya, saya menggunakan MongoDB dan untuk memudahkan pekerjaan, saya membungkus koleksi dalam repositori.
Lebih detailRepositori itu sendiri

Monga memblokir dokumen pada saat bekerja dengannya, masing-masing, ini akan membantu kami menyelesaikan masalah dalam persaingan permintaan. Dan di mong ada metode untuk mencari entitas + yang bekerja padanya, misalnya: "Temukan pengguna dengan id dan tambahkan 10 ke saldo saat ini"
Dan sekarang tentang fitur C # 8.


Tanda tangan metode memberitahu saya bahwa Pengguna dapat kembali, dan mungkin Null, masing-masing, ketika saya melihat Pengguna? Saya segera mendapatkan peringatan kompiler, dan melakukan pemeriksaan nol.

Ketika metode mengembalikan Pengguna, saya bekerja dengan percaya diri.

Saya juga ingin menarik perhatian pada fakta bahwa tidak ada try catch karena hanya ada pengecualian dari "situasi aneh" dan data yang salah yang tidak boleh sampai di sini karena ada validasi. Juga tidak ada try catch di lapisan API, hanya ada satu handler pengecualian global.
Hanya ada satu metode yang melempar Pengecualian adalah metode Pembaruan.
Itu menerapkan perlindungan terhadap kehilangan data dalam mode multi-threaded.

Mengapa Anda tidak menggunakan metode monga yang disebutkan di atas?
Ada tempat-tempat di mana saya masih tidak tahu pasti apakah saya bisa bekerja dengan pengguna, mungkin dia tidak punya uang untuk tindakan ini, jadi pada awalnya saya mengeluarkan pengguna dan memeriksanya, lalu bermutasi dan simpan.

Aplikasi saya dalam teori akan mengubah keseimbangan pengguna lebih dari 1 kali per detik, karena ini akan menjadi game cepat.
Tetapi model pengguna itu sendiri, jelas terlihat bahwa rujukan pengguna adalah opsional, dan Anda dapat bekerja dengan semua hal lain tanpa memikirkan nol.

Akhirnya Tes
Seperti yang saya katakan, Anda hanya perlu menguji logika, dan logika fungsi kami tanpa efek samping.
Oleh karena itu, kami dapat menjalankan pengujian kami dengan sangat cepat dan dengan berbagai parameter.
Lebih detailSaya mengunduh nuget FSCheck yang menghasilkan data yang masuk secara acak dan memungkinkan berbagai kasus.
Saya hanya perlu membuat berbagai pengguna, memberi makan pengujian mereka dan memeriksa perubahannya.
Ada pembangun kecil untuk membuat pengguna seperti itu, tetapi mudah untuk diperluas.

Dan inilah tes-tesnya sendiri



Setelah beberapa perubahan, saya menjalankan tes, setelah 1-2 detik saya melihat semuanya beres.
Hal ini juga dalam rencana untuk menulis tes E2E untuk memeriksa seluruh API dari luar dan memastikan bahwa itu berfungsi sebagaimana mestinya, dari permintaan hingga tanggapan.
Keripik
Hal-hal keren yang mungkin Anda butuhkanSetiap permintaan saya didoping, ketika bug terjadi, saya menemukan requestId dan dapat dengan mudah mereproduksi bug dengan mengulangi permintaan, karena API saya tidak memiliki status, dan setiap permintaan hanya bergantung pada parameter permintaan.

Untuk meringkas.
Kami benar-benar menulis solusi, dan bukan kerangka kerja di mana banyak abstraksi tambahan, serta mok. Kami melakukan penanganan kesalahan di satu tempat dan jarang terjadi. Kami memisahkan BL dan efek samping, sekarang BL hanyalah logika lokal yang dapat digunakan kembali. Kami tidak menulis fungsi tambahan yang hanya meneruskan panggilan ke fungsi lain. Saya akan aktif membaca komentar dan menambah artikel, terima kasih!