
Artikel ini adalah bagian dari
Kronik Arsitektur Perangkat Lunak , serangkaian artikel tentang arsitektur perangkat lunak. Di dalamnya saya menulis tentang apa yang saya pelajari tentang arsitektur perangkat lunak, apa yang saya pikirkan dan bagaimana saya menggunakan pengetahuan. Konten artikel ini mungkin lebih masuk akal jika Anda membaca artikel sebelumnya dalam seri.
Setelah lulus dari universitas, saya mulai bekerja sebagai guru sekolah menengah, tetapi beberapa tahun yang lalu saya berhenti dan pergi ke pengembang perangkat lunak penuh waktu.
Sejak itu, saya selalu merasa bahwa saya harus memulihkan waktu yang "hilang" dan mencari tahu sebanyak mungkin, secepat mungkin. Oleh karena itu, saya mulai terlibat dalam eksperimen sedikit, banyak membaca dan menulis, memberikan perhatian khusus pada desain dan arsitektur perangkat lunak. Itulah sebabnya saya menulis artikel ini untuk membantu diri saya sendiri dalam studi saya.
Dalam artikel terakhir, saya berbicara tentang banyak konsep dan prinsip yang saya pelajari, dan sedikit tentang bagaimana saya memikirkannya. Tapi saya membayangkan mereka sebagai pecahan dari satu teka-teki besar.
Artikel ini adalah tentang bagaimana saya mengumpulkan semua fragmen ini. Saya pikir saya harus memberi mereka nama, jadi saya akan memanggil mereka
arsitektur eksplisit . Selain itu, semua konsep ini
"diuji dalam pertempuran" dan digunakan dalam produksi pada platform yang sangat andal. Salah satunya adalah platform e-commerce SaaS dengan ribuan toko online di seluruh dunia, yang lainnya adalah platform perdagangan yang beroperasi di dua negara dengan bus pesan yang memproses lebih dari 20 juta pesan per bulan.
Blok mendasar dari sistem
Mari kita mulai dengan
mengingat arsitektur
EBI dan
Ports & Adapters . Keduanya jelas memisahkan kode internal dan eksternal aplikasi, serta adaptor untuk menghubungkan kode internal dan eksternal.
Selain itu, arsitektur
Ports & Adapters secara eksplisit mendefinisikan tiga blok kode mendasar dalam sistem:
- Itu memungkinkan Anda untuk menjalankan antarmuka pengguna , terlepas dari jenisnya.
- Logika bisnis sistem atau inti aplikasi . Ini digunakan oleh UI untuk melakukan transaksi nyata.
- Kode infrastruktur yang menghubungkan inti aplikasi kita ke alat-alat seperti database, mesin pencari, atau API pihak ketiga.

Inti dari aplikasi adalah hal yang paling penting untuk dipikirkan. Kode ini memungkinkan Anda untuk melakukan tindakan nyata dalam sistem, yaitu, ini adalah aplikasi kami. Beberapa antarmuka pengguna (aplikasi web progresif, aplikasi seluler, CLI, API, dll.) Dapat bekerja dengannya, semuanya berjalan pada satu inti.
Seperti yang dapat Anda bayangkan, alur eksekusi yang umum terjadi dari kode di UI melalui inti aplikasi ke kode infrastruktur, kembali ke inti aplikasi dan, akhirnya, respons dikirim ke UI.

Alat-alatnya
Jauh dari kode kernel yang paling penting, masih ada alat yang digunakan aplikasi. Misalnya, mesin basis data, mesin pencari, server web, dan konsol CLI (meskipun dua yang terakhir juga merupakan mekanisme pengiriman).

Rasanya aneh menempatkan konsol CLI di bagian tematik yang sama dengan DBMS, karena mereka memiliki tujuan yang berbeda. Namun pada kenyataannya, keduanya adalah alat yang digunakan oleh aplikasi. Perbedaan utama adalah bahwa konsol CLI dan server web
memberi tahu aplikasi untuk melakukan sesuatu , sebaliknya DBMS kernel,
menerima perintah dari aplikasi . Ini adalah perbedaan yang sangat penting, karena sangat memengaruhi cara kita menulis kode untuk menghubungkan alat-alat ini ke inti aplikasi.
Menghubungkan alat dan mekanisme pengiriman ke inti aplikasi
Blok alat penghubung kode ke inti aplikasi disebut adapter (
arsitektur Ports & Adapters ). Mereka memungkinkan logika bisnis untuk berinteraksi dengan alat tertentu, dan sebaliknya.
Adaptor yang memberi tahu aplikasi untuk melakukan sesuatu disebut
adaptor primer atau kontrol , sedangkan adaptor yang memberi tahu aplikasi untuk melakukan sesuatu disebut
adaptor sekunder atau terkelola .
Pelabuhan
Namun,
adaptor ini tidak dibuat secara kebetulan, tetapi untuk menyesuaikan dengan titik masuk tertentu di inti aplikasi,
port . Port
tidak lebih dari spesifikasi bagaimana alat dapat menggunakan inti aplikasi atau sebaliknya. Dalam sebagian besar bahasa dan dalam bentuknya yang paling sederhana, port ini akan menjadi antarmuka, tetapi sebenarnya ia dapat terdiri dari beberapa antarmuka dan DTO.
Penting untuk dicatat bahwa
port (antarmuka) ada di dalam logika bisnis , dan adaptor ada di luar. Agar templat ini berfungsi dengan baik, sangat penting untuk membuat port sesuai dengan kebutuhan inti aplikasi, dan tidak hanya meniru API alat.
Adaptor primer atau kontrol
Adaptor primer atau kontrol
membungkus port dan menggunakannya untuk memberi tahu kernel aplikasi apa yang harus dilakukan.
Mereka mengubah semua data dari mekanisme pengiriman menjadi panggilan metode dalam inti aplikasi.
Dengan kata lain, adaptor kontrol kami adalah pengontrol atau perintah konsol, mereka tertanam dalam konstruktor mereka dengan beberapa objek yang kelasnya mengimplementasikan antarmuka (port) yang diperlukan oleh perintah pengontrol atau konsol.
Dalam contoh yang lebih spesifik, port mungkin adalah antarmuka layanan atau antarmuka repositori yang dibutuhkan pengontrol. Implementasi spesifik dari layanan, repositori, atau permintaan kemudian diimplementasikan dan digunakan dalam controller.
Selain itu, port bisa berupa bus perintah atau antarmuka bus permintaan. Dalam hal ini, implementasi spesifik dari perintah atau bus permintaan dimasukkan ke dalam controller, yang kemudian membuat perintah atau permintaan dan meneruskannya ke bus yang sesuai.
Adaptor Sekunder atau Terkelola
Tidak seperti control Adapters yang membungkus port,
Adaptor yang dikelola mengimplementasikan port, sebuah interface, dan kemudian memasukkan inti aplikasi di mana port diperlukan (dengan tipe).

Misalnya, kami memiliki aplikasi asli yang perlu menyimpan data. Kami membuat antarmuka persistensi dengan metode
menyimpan array data dan metode
menghapus baris dalam tabel dengan ID-nya. Mulai sekarang, di mana pun aplikasi perlu menyimpan atau menghapus data, kami akan meminta dalam konstruktor sebuah objek yang mengimplementasikan antarmuka kegigihan yang kami tetapkan.
Sekarang buat adaptor khusus MySQL yang akan mengimplementasikan antarmuka ini. Ini akan memiliki metode untuk menyimpan array dan menghapus baris dalam tabel, dan kami akan memperkenalkannya di mana pun antarmuka persisten diperlukan.
Jika pada suatu saat kami memutuskan untuk mengubah penyedia basis data, misalnya, ke PostgreSQL atau MongoDB, kami hanya perlu membuat adaptor baru yang mengimplementasikan antarmuka persistensi khusus untuk PostgreSQL dan memperkenalkan adaptor baru alih-alih yang lama.
Kontrol inversi
Fitur karakteristik templat ini adalah bahwa adaptor bergantung pada alat tertentu dan port tertentu (dengan mengimplementasikan antarmuka). Tetapi logika bisnis kami hanya bergantung pada port (antarmuka), yang dirancang untuk memenuhi kebutuhan logika bisnis dan tidak bergantung pada adaptor atau alat tertentu.

Ini berarti bahwa ketergantungan diarahkan ke pusat, yaitu, ada
inversi dari prinsip kontrol di tingkat arsitektur .
Meskipun, sekali lagi,
sangat penting bahwa port dibuat sesuai dengan kebutuhan inti aplikasi, dan tidak hanya meniru API alat .
Organisasi inti aplikasi
Arsitektur Onion mengambil lapisan DDD dan menggabungkannya ke dalam
arsitektur port dan adaptor . Level-level ini dirancang untuk menghadirkan logika bisnis, bagian dalam "segi enam" port dan adaptor. Seperti sebelumnya, arah ketergantungan menuju pusat.
Lapisan Aplikasi (Lapisan Aplikasi)
Use cases adalah proses yang dapat diluncurkan dalam kernel dengan satu atau lebih antarmuka pengguna. Misalnya, CMS mungkin memiliki satu UI untuk pengguna biasa, UI independen lainnya untuk administrator CMS, CLI lain, dan API web. UI (aplikasi) ini dapat memicu kasus penggunaan yang unik atau umum.
Use case didefinisikan pada level aplikasi - level pertama DDD dan arsitektur Onion.

Lapisan ini berisi layanan aplikasi (dan antarmuka mereka) sebagai objek kelas satu, dan juga berisi antarmuka port dan adaptor (port), yang mencakup antarmuka ORM, antarmuka mesin pencari, antarmuka pengiriman pesan, dll. Dalam kasus di mana kami menggunakan bus perintah dan / atau bus permintaan, pada level ini adalah perintah dan penangan permintaan yang sesuai.
Layanan aplikasi dan / atau penangan perintah berisi logika penggunaan kasus penggunaan, proses bisnis. Sebagai aturan, peran mereka adalah sebagai berikut:
- gunakan repositori untuk mencari satu atau lebih entitas;
- minta entitas ini untuk menjalankan beberapa logika domain;
- dan menggunakan penyimpanan untuk menyimpan kembali entitas, secara efektif menyimpan perubahan data.
Penangan perintah dapat digunakan dalam dua cara:
- Mereka mungkin berisi logika untuk mengeksekusi use case;
- Mereka dapat digunakan sebagai bagian sederhana dari koneksi dalam arsitektur kita yang menerima perintah dan hanya memanggil logika yang ada di layanan aplikasi.
Pendekatan mana yang digunakan tergantung pada konteksnya, misalnya:
- Kami sudah memiliki layanan aplikasi dan sekarang bus perintah sedang ditambahkan?
- Apakah bus perintah memungkinkan Anda untuk menentukan kelas / metode sebagai penangan, atau apakah Anda perlu memperluas atau mengimplementasikan kelas atau antarmuka yang ada?
Lapisan ini juga mengandung
peristiwa aplikasi pemicu yang mewakili beberapa hasil dari use case. Peristiwa ini memicu logika yang merupakan efek samping dari use case, seperti mengirim email, memberi tahu API pihak ketiga, mengirim pemberitahuan push, atau bahkan meluncurkan use case lain yang merupakan komponen aplikasi lainnya.
Tingkat domain
Lebih jauh di dalam ada tingkat domain. Objek pada level ini berisi data dan logika untuk mengelola data ini, yang khusus untuk domain itu sendiri dan tidak tergantung pada proses bisnis yang memicu logika ini. Mereka independen dan sama sekali tidak mengetahui tingkat aplikasi.

Layanan Domain
Seperti yang saya sebutkan di atas, peran layanan aplikasi:
- gunakan repositori untuk mencari satu atau lebih entitas;
- minta entitas ini untuk menjalankan beberapa logika domain;
- dan menggunakan penyimpanan untuk menyimpan kembali entitas, secara efektif menyimpan perubahan data.
Tetapi kadang-kadang kita menemukan beberapa logika domain, yang mencakup berbagai entitas dari jenis yang sama atau berbeda, dan logika domain ini bukan milik entitas itu sendiri, yaitu, logika bukan tanggung jawab langsung mereka.
Oleh karena itu, reaksi pertama kami mungkin menempatkan logika ini di luar entitas dalam layanan aplikasi. Namun, ini berarti bahwa dalam kasus lain, logika domain tidak akan digunakan kembali: logika domain harus tetap berada di luar level aplikasi!
Solusinya adalah membuat layanan domain, yang perannya adalah untuk memperoleh seperangkat entitas dan menjalankan beberapa logika bisnis pada mereka. Layanan domain milik tingkat domain dan karenanya tidak tahu apa-apa tentang kelas di tingkat aplikasi, seperti layanan aplikasi atau repositori. Di sisi lain, ia dapat menggunakan layanan domain lain dan, tentu saja, objek model domain.
Model domain
Di pusat adalah model domain. Itu tidak bergantung pada apa pun di luar lingkaran ini dan berisi objek bisnis yang mewakili sesuatu dalam domain. Contoh objek tersebut adalah, pertama-tama, entitas, serta nilai objek, enum, dan objek apa pun yang digunakan dalam model domain.
Peristiwa domain juga hidup dalam model domain. Ketika kumpulan data tertentu berubah, peristiwa ini dipicu, yang berisi nilai baru dari properti yang diubah. Acara-acara ini ideal, misalnya, untuk digunakan dalam modul sumber acara.
Komponen
Sejauh ini, kami telah mengisolasi kode dalam lapisan, tetapi ini adalah isolasi kode yang terlalu terperinci. Sama pentingnya untuk melihat gambar dengan tampilan yang lebih umum. Kita berbicara tentang membagi kode menjadi subdomain dan
konteks terkait sesuai dengan ide-ide Robert Martin yang diungkapkan dalam
arsitektur berteriak [yaitu, arsitektur harus "berteriak" tentang aplikasi itu sendiri, dan bukan tentang kerangka mana yang digunakannya - kira-kira. trans.]. Mereka berbicara tentang mengatur paket berdasarkan fungsi atau komponen, bukan demi lapisan, dan Simon Brown menjelaskannya dengan sangat baik dalam artikel
"Paket komponen dan pengujian arsitektur" di blognya:

Saya seorang pendukung pengorganisasian paket komponen dan ingin mengubah diagram Simon Brown tanpa malu-malu sebagai berikut:

Bagian-bagian kode ini saling memotong untuk semua lapisan yang dijelaskan sebelumnya, dan ini adalah
komponen dari aplikasi kita. Contoh komponen adalah penagihan, pengguna, verifikasi, atau akun, tetapi mereka selalu dikaitkan dengan domain. Konteks terbatas, seperti otorisasi dan / atau otentikasi, harus dianggap sebagai alat eksternal yang kami buat adaptor dan bersembunyi di balik port.

Putuskan Komponen
Seperti halnya dalam unit kode yang halus (kelas, antarmuka, sifat, mixin, dll.), Unit besar (komponen) mendapat manfaat dari kopling yang lemah dan konektivitas yang ketat.
Untuk memisahkan kelas, kami menggunakan injeksi dependensi, memperkenalkan dependensi ke dalam kelas, daripada membuatnya di dalam kelas, dan juga membalik dependensi, membuat kelas bergantung pada abstraksi (antarmuka dan / atau kelas abstrak) alih-alih kelas tertentu. Ini berarti bahwa kelas dependen tidak tahu apa-apa tentang kelas spesifik yang akan digunakan, itu tidak memiliki referensi ke nama lengkap dari kelas-kelas di mana ia bergantung.
Demikian pula, dalam komponen yang benar-benar terputus, setiap komponen tidak tahu apa-apa tentang komponen lainnya. Dengan kata lain, ia tidak memiliki tautan ke blok kode berbutir halus dari komponen lain, bahkan ke antarmuka! Ini berarti injeksi ketergantungan dan inversi ketergantungan tidak cukup untuk memisahkan komponen, kita akan memerlukan semacam konstruksi arsitektur. Peristiwa, inti umum, konsistensi akhirnya, dan bahkan layanan pencarian mungkin diperlukan!

Memicu logika pada komponen lain
Ketika salah satu komponen kami (komponen B) perlu melakukan sesuatu setiap kali sesuatu yang lain terjadi di komponen lain (komponen A), kami tidak bisa langsung melakukan panggilan langsung dari komponen A ke kelas / metode komponen B, karena maka A akan terhubung ke B.
Namun, kita dapat menggunakan event manager untuk mengirimkan acara aplikasi, yang akan dikirim ke komponen yang mendengarkannya, termasuk B, dan pendengar acara di B akan memicu tindakan yang diinginkan. Ini berarti bahwa komponen A akan tergantung pada manajer acara, tetapi akan terpisah dari komponen B.
Namun, jika peristiwa itu sendiri "hidup" di A, ini berarti bahwa B tahu tentang keberadaan A dan terkait dengannya. Untuk menghapus ketergantungan ini, kita dapat membuat perpustakaan dengan serangkaian fungsionalitas inti aplikasi yang akan dibagikan oleh semua komponen -
inti bersama . Ini berarti bahwa kedua komponen akan tergantung pada inti bersama, tetapi akan dipisahkan satu sama lain. Inti umum berisi fungsionalitas seperti peristiwa aplikasi dan domain, tetapi juga dapat berisi objek spesifikasi dan apa pun yang masuk akal untuk dibagikan. Pada saat yang sama, itu harus berukuran minimum, karena setiap perubahan dalam kernel yang sama akan mempengaruhi semua komponen aplikasi. Selain itu, jika kita memiliki sistem polyglot, katakanlah, ekosistem layanan microser dalam berbagai bahasa, maka inti bersama tidak boleh bergantung pada bahasa sehingga semua komponen memahaminya. Sebagai contoh, alih-alih kernel umum dengan kelas peristiwa, itu akan berisi deskripsi peristiwa (yaitu, nama, properti, bahkan metode, meskipun mereka akan lebih berguna dalam objek spesifikasi) dalam bahasa universal seperti JSON sehingga semua komponen / layanan mikro dapat menafsirkannya dan bahkan mungkin secara otomatis menghasilkan implementasi spesifik mereka sendiri.
Pendekatan ini bekerja baik dalam aplikasi monolitik maupun terdistribusi, seperti ekosistem layanan mikro. Tetapi jika peristiwa dapat disampaikan hanya secara tidak sinkron, maka pendekatan ini tidak cukup untuk konteks di mana logika pemicu dalam komponen lain harus segera bekerja! Di sini, komponen A perlu membuat panggilan HTTP langsung ke komponen B. Dalam hal ini, untuk memutuskan komponen, kita memerlukan layanan pencarian. Komponen A akan menanyakan ke mana dia mengirim permintaan untuk memulai tindakan yang diinginkan. Atau, buat permintaan ke layanan penemuan, yang akan meneruskannya ke layanan yang sesuai dan pada akhirnya mengembalikan respons kepada pemohon.
Pendekatan ini mengaitkan komponen dengan layanan penemuan, tetapi tidak menghubungkannya satu sama lain.Mengambil data dari komponen lain
Seperti yang saya lihat, komponen tidak diperbolehkan untuk mengubah data yang tidak "dimiliki", tetapi dapat meminta dan menggunakan data apa pun.Penyimpanan data bersama untuk komponen
Jika komponen harus menggunakan data milik komponen lain (misalnya, komponen penagihan harus menggunakan nama klien yang termasuk dalam komponen akun), maka komponen tersebut berisi objek permintaan ke penyimpanan data. Artinya, komponen penagihan dapat mengetahui tentang kumpulan data apa pun, tetapi harus menggunakan data hanya baca dari negara lain.Pisahkan penyimpanan data untuk komponen
Dalam hal ini, templat yang sama diterapkan, tetapi tingkat penyimpanan data menjadi lebih rumit. Kehadiran komponen dengan gudang data mereka sendiri berarti bahwa setiap gudang data berisi:- Seperangkat data yang dimiliki dan dapat diubah oleh suatu komponen, menjadikannya satu-satunya sumber kebenaran;
- Dataset yang merupakan salinan data komponen lain yang tidak dapat diubah dengan sendirinya, tetapi diperlukan untuk fungsionalitas komponen. Data ini harus diperbarui setiap kali ada perubahan dalam komponen pemilik.
, . , , , . , , .
, , , β . ? ? ?
«» (Clean Architecture), UMLishβ¦
/
, , Query.
[ 18.11.2017] DTO, , . MorphineAdministered , .
, , , . , .
Objek kueri berisi kueri yang dioptimalkan yang hanya mengembalikan beberapa data mentah yang akan ditampilkan kepada pengguna. Data ini dikembalikan ke DTO, yang tertanam dalam ViewModel. ViewModel ini mungkin memiliki semacam logika tampilan dan akan digunakan untuk mengisi tampilan.Di sisi lain, layanan aplikasi berisi logika use-case yang menyala ketika kita ingin melakukan sesuatu pada sistem, dan tidak hanya melihat beberapa data. Layanan aplikasi tergantung pada repositori yang mengembalikan entitas yang berisi logika yang perlu dimulai. Mungkin juga tergantung pada layanan domain untuk mengoordinasikan proses domain di beberapa entitas, tetapi ini adalah kasus yang jarang terjadi.Setelah menguraikan kasus penggunaan, layanan aplikasi dapat memberi tahu seluruh sistem bahwa kasus penggunaan telah terjadi, maka itu akan tergantung pada pengirim acara untuk memicu acara tersebut.Sangat menarik untuk dicatat bahwa kami memiliki antarmuka pada mesin persistensi dan repositori. Ini mungkin tampak berlebihan, tetapi mereka melayani tujuan yang berbeda:- Antarmuka Persistence adalah lapisan abstraksi di atas ORM, sehingga kita dapat menukar ORM tanpa mengubah inti aplikasi.
- persistence-. , MySQL MongoDB. persistence- , ORM, . , , , , , , MongoDB SQL.
C /
, /, , , , . , .
. , . , .
[ 18.11.2017] DTO, , . MorphineAdministered , .
, , , . , , . .
β , β . , Ports & Adapters, Onion Clean.

Kesimpulan
, , , , .
Rencana tidak berguna, tetapi perencanaan adalah segalanya. - Eisenhower
Infografis ini adalah peta konsep. Mengetahui dan memahami semua konsep ini membantu Anda merencanakan arsitektur yang sehat dan aplikasi yang bisa diterapkan.Namun:Peta bukan wilayah. - Alfred Korzybsky
,
! β , , , , , !, , , . , , , , .
.
:
Β«, Β» .