Membuat Kode yang Benar-benar Modular tanpa Ketergantungan

Diterbitkan: 2022-03-11

Mengembangkan perangkat lunak itu hebat, tapi… Saya pikir kita semua bisa setuju bahwa itu bisa menjadi sedikit rollercoaster emosional. Pada awalnya, semuanya hebat. Anda menambahkan fitur baru satu demi satu dalam hitungan hari jika bukan jam. Anda berada di roll!

Maju cepat beberapa bulan, dan kecepatan pengembangan Anda menurun. Apakah karena Anda tidak bekerja sekeras sebelumnya? Tidak juga. Mari kita maju cepat beberapa bulan lagi, dan kecepatan pengembangan Anda semakin menurun. Mengerjakan proyek ini tidak lagi menyenangkan dan menjadi hambatan.

Ini menjadi lebih buruk. Anda mulai menemukan banyak bug di aplikasi Anda. Seringkali, memecahkan satu bug menciptakan dua bug baru. Pada titik ini, Anda dapat mulai bernyanyi:

99 bug kecil dalam kode. 99 bug kecil. Ambil satu, tambal,

…127 bug kecil dalam kode.

Bagaimana perasaan Anda tentang mengerjakan proyek ini sekarang? Jika Anda seperti saya, Anda mungkin mulai kehilangan motivasi. Hanya saja sulit untuk mengembangkan aplikasi ini, karena setiap perubahan pada kode yang ada dapat memiliki konsekuensi yang tidak terduga.

Pengalaman ini umum di dunia perangkat lunak dan dapat menjelaskan mengapa begitu banyak programmer ingin membuang kode sumber mereka dan menulis ulang semuanya.

Alasan Mengapa Pengembangan Perangkat Lunak Lambat Seiring Waktu

Jadi apa alasan untuk masalah ini?

Penyebab utamanya adalah meningkatnya kompleksitas. Dari pengalaman saya, kontributor terbesar untuk kompleksitas keseluruhan adalah kenyataan bahwa, di sebagian besar proyek perangkat lunak, semuanya terhubung. Karena ketergantungan yang dimiliki setiap kelas, jika Anda mengubah beberapa kode di kelas yang mengirim email, pengguna Anda tiba-tiba tidak bisa mendaftar. Mengapa demikian? Karena kode registrasi Anda tergantung pada kode yang mengirim email. Sekarang Anda tidak dapat mengubah apa pun tanpa memperkenalkan bug. Tidak mungkin melacak semua dependensi.

Jadi begitulah; penyebab sebenarnya dari masalah kita adalah meningkatkan kompleksitas yang berasal dari semua dependensi yang dimiliki kode kita.

Bola Lumpur Besar dan Cara Menguranginya

Lucunya, isu ini sudah diketahui bertahun-tahun. Ini adalah anti-pola umum yang disebut "bola lumpur besar". Saya telah melihat jenis arsitektur itu di hampir semua proyek yang saya kerjakan selama bertahun-tahun di berbagai perusahaan.

Jadi apa sebenarnya anti-pola ini? Sederhananya, Anda mendapatkan bola lumpur besar ketika setiap elemen memiliki ketergantungan dengan elemen lain. Di bawah ini, Anda dapat melihat grafik dependensi dari proyek sumber terbuka Apache Hadoop yang terkenal. Untuk memvisualisasikan bola besar lumpur (atau lebih tepatnya, bola besar benang), Anda menggambar lingkaran dan menempatkan kelas dari proyek secara merata di atasnya. Hanya menarik garis antara setiap pasangan kelas yang bergantung satu sama lain. Sekarang Anda dapat melihat sumber masalah Anda.

Visualisasi "bola lumpur besar" Apache Hadoop dengan beberapa lusin node dan ratusan garis yang menghubungkannya satu sama lain.

"Bola lumpur besar" Apache Hadoop

Solusi dengan Kode Modular

Jadi saya bertanya pada diri sendiri: Apakah mungkin untuk mengurangi kerumitan dan tetap bersenang-senang seperti di awal proyek? Sejujurnya, Anda tidak dapat menghilangkan semua kerumitan. Jika Anda ingin menambahkan fitur baru, Anda harus selalu meningkatkan kompleksitas kode. Namun demikian, kompleksitas dapat dipindahkan dan dipisahkan.

Bagaimana Industri Lain Memecahkan Masalah Ini

Pikirkan tentang industri mekanik. Ketika beberapa toko mekanik kecil membuat mesin, mereka membeli satu set elemen standar, membuat beberapa yang khusus, dan menyatukannya. Mereka dapat membuat komponen-komponen tersebut sepenuhnya secara terpisah dan merakit semuanya di akhir, membuat hanya beberapa penyesuaian. Bagaimana ini mungkin? Mereka tahu bagaimana setiap elemen akan cocok satu sama lain dengan menetapkan standar industri seperti ukuran baut, dan keputusan di muka seperti ukuran lubang pemasangan dan jarak di antara mereka.

Diagram teknis dari mekanisme fisik dan bagaimana bagian-bagiannya cocok bersama. Potongan-potongan diberi nomor untuk dilampirkan berikutnya, tetapi urutan kiri-ke-kanan itu menjadi 5, 3, 4, 1, 2.

Setiap elemen dalam perakitan di atas dapat disediakan oleh perusahaan terpisah yang tidak memiliki pengetahuan apa pun tentang produk akhir atau bagian lainnya. Selama setiap elemen modular diproduksi sesuai dengan spesifikasi, Anda akan dapat membuat perangkat akhir sesuai rencana.

Bisakah kita meniru itu di industri perangkat lunak?

Tentu kita bisa! Dengan menggunakan antarmuka dan inversi prinsip kontrol; bagian terbaiknya adalah kenyataan bahwa pendekatan ini dapat digunakan dalam bahasa berorientasi objek apa pun: Java, C#, Swift, TypeScript, JavaScript, PHP—daftarnya terus bertambah. Anda tidak memerlukan kerangka kerja mewah untuk menerapkan metode ini. Anda hanya perlu mengikuti beberapa aturan sederhana dan tetap disiplin.

Pembalikan Kontrol Adalah Teman Anda

Ketika saya pertama kali mendengar tentang inversi kontrol, saya segera menyadari bahwa saya telah menemukan solusi. Ini adalah konsep mengambil dependensi yang ada dan membalikkannya dengan menggunakan antarmuka. Antarmuka adalah deklarasi sederhana dari metode. Mereka tidak memberikan implementasi konkret. Akibatnya, mereka dapat digunakan sebagai kesepakatan antara dua elemen tentang cara menghubungkannya. Mereka dapat digunakan sebagai konektor modular, jika Anda mau. Selama satu elemen menyediakan antarmuka dan elemen lain menyediakan implementasi untuk itu, mereka dapat bekerja sama tanpa mengetahui apa pun tentang satu sama lain. Itu brilian.

Mari kita lihat pada contoh sederhana bagaimana kita dapat memisahkan sistem kita untuk membuat kode modular. Diagram di bawah ini telah diimplementasikan sebagai aplikasi Java sederhana. Anda dapat menemukannya di repositori GitHub ini.

Masalah

Mari kita asumsikan bahwa kita memiliki aplikasi yang sangat sederhana yang hanya terdiri dari kelas Main , tiga layanan, dan satu kelas Util . Elemen-elemen itu saling bergantung satu sama lain dalam berbagai cara. Di bawah ini, Anda dapat melihat implementasi menggunakan pendekatan “bola besar lumpur”. Kelas hanya memanggil satu sama lain. Mereka digabungkan dengan erat, dan Anda tidak bisa begitu saja mengeluarkan satu elemen tanpa menyentuh yang lain. Aplikasi yang dibuat menggunakan gaya ini memungkinkan Anda untuk berkembang pesat pada awalnya. Saya percaya gaya ini sesuai untuk proyek proof-of-concept karena Anda dapat bermain-main dengan berbagai hal dengan mudah. Namun demikian, ini tidak sesuai untuk solusi siap produksi karena bahkan pemeliharaan dapat berbahaya dan setiap perubahan dapat membuat bug yang tidak dapat diprediksi. Diagram di bawah menunjukkan bola besar arsitektur lumpur ini.

Main menggunakan layanan A, B, dan C yang masing-masing menggunakan Util. Layanan C juga menggunakan Layanan A.

Mengapa Injeksi Ketergantungan Membuat Semuanya Salah

Dalam mencari pendekatan yang lebih baik, kita dapat menggunakan teknik yang disebut injeksi ketergantungan. Metode ini mengasumsikan bahwa semua komponen harus digunakan melalui antarmuka. Saya telah membaca klaim bahwa itu memisahkan elemen, tetapi apakah itu benar-benar? Tidak. Lihat diagram di bawah ini.

Arsitektur sebelumnya tetapi dengan injeksi ketergantungan. Sekarang Main menggunakan Layanan Antarmuka A, B, dan C, yang diimplementasikan oleh layanan terkaitnya. Layanan A dan C keduanya menggunakan Layanan Antarmuka B dan Antarmuka Util, yang diimplementasikan oleh Util. Layanan C juga menggunakan Antarmuka Layanan A. Setiap layanan bersama dengan antarmukanya dianggap sebagai elemen.

Satu-satunya perbedaan antara situasi saat ini dan bola besar lumpur adalah kenyataan bahwa sekarang, alih-alih memanggil kelas secara langsung, kami memanggil mereka melalui antarmuka mereka. Ini sedikit meningkatkan memisahkan elemen dari satu sama lain. Jika, misalnya, Anda ingin menggunakan kembali Service A dalam proyek yang berbeda, Anda dapat melakukannya dengan mengeluarkan Service A itu sendiri, bersama dengan Interface A , serta Interface B dan Interface Util . Seperti yang Anda lihat, Service A masih bergantung pada elemen lain. Akibatnya, kami masih mendapatkan masalah dengan mengubah kode di satu tempat dan mengacaukan perilaku di tempat lain. Itu masih menimbulkan masalah bahwa jika Anda memodifikasi Service B dan Interface B , Anda perlu mengubah semua elemen yang bergantung padanya. Pendekatan ini tidak menyelesaikan apa pun; menurut saya, itu hanya menambahkan lapisan antarmuka di atas elemen. Anda tidak boleh menyuntikkan dependensi apa pun, tetapi Anda harus menyingkirkannya sekali dan untuk semua. Semangat untuk kemerdekaan!

Solusi untuk Kode Modular

Pendekatan yang saya percaya memecahkan semua sakit kepala utama dependensi melakukannya dengan tidak menggunakan dependensi sama sekali. Anda membuat komponen dan pendengarnya. Pendengar adalah antarmuka yang sederhana. Kapan pun Anda perlu memanggil metode dari luar elemen saat ini, Anda cukup menambahkan metode ke pendengar dan menyebutnya sebagai gantinya. Elemen hanya diperbolehkan untuk menggunakan file, memanggil metode dalam paketnya, dan menggunakan kelas yang disediakan oleh kerangka kerja utama atau pustaka lain yang digunakan. Di bawah ini, Anda dapat melihat diagram aplikasi yang dimodifikasi untuk menggunakan arsitektur elemen.

Diagram aplikasi yang dimodifikasi untuk menggunakan arsitektur elemen. Utama menggunakan Util dan ketiga layanan. Main juga mengimplementasikan pendengar untuk setiap layanan, yang digunakan oleh layanan itu. Seorang pendengar dan layanan bersama-sama dianggap sebagai elemen.

Harap dicatat bahwa, dalam arsitektur ini, hanya kelas Main yang memiliki banyak dependensi. Ini menghubungkan semua elemen dan merangkum logika bisnis aplikasi.

Layanan, di sisi lain, adalah elemen yang sepenuhnya independen. Sekarang, Anda dapat mengeluarkan setiap layanan dari aplikasi ini dan menggunakannya kembali di tempat lain. Mereka tidak bergantung pada hal lain. Tapi tunggu, itu akan lebih baik: Anda tidak perlu mengubah layanan itu lagi, selama Anda tidak mengubah perilakunya. Selama layanan tersebut melakukan apa yang seharusnya mereka lakukan, mereka dapat dibiarkan tak tersentuh sampai akhir zaman. Mereka dapat dibuat oleh seorang insinyur perangkat lunak profesional, atau pembuat kode pertama kali dikompromikan dari kode spageti terburuk yang pernah dimasak dengan pernyataan goto yang dicampur. Tidak masalah, karena logika mereka dienkapsulasi. Meskipun mengerikan, itu tidak akan pernah menyebar ke kelas lain. Itu juga memberi Anda kekuatan untuk membagi pekerjaan dalam sebuah proyek antara beberapa pengembang, di mana setiap pengembang dapat mengerjakan komponen mereka sendiri secara mandiri tanpa perlu mengganggu yang lain atau bahkan mengetahui tentang keberadaan pengembang lain.

Akhirnya, Anda dapat mulai menulis kode independen sekali lagi, seperti di awal proyek terakhir Anda.

Pola Elemen

Mari kita definisikan pola elemen struktural sehingga kita dapat membuatnya secara berulang.

Versi paling sederhana dari elemen terdiri dari dua hal: Kelas elemen utama dan pendengar. Jika Anda ingin menggunakan sebuah elemen, maka Anda perlu mengimplementasikan listener dan melakukan panggilan ke kelas utama. Berikut adalah diagram konfigurasi paling sederhana:

Diagram elemen tunggal dan pendengarnya dalam aplikasi. Seperti sebelumnya, Aplikasi menggunakan elemen, yang menggunakan pendengarnya, yang diimplementasikan oleh Aplikasi.

Jelas, Anda perlu menambahkan lebih banyak kerumitan ke dalam elemen pada akhirnya tetapi Anda dapat melakukannya dengan mudah. Pastikan saja tidak ada kelas logika Anda yang bergantung pada file lain dalam proyek. Mereka hanya dapat menggunakan kerangka utama, pustaka yang diimpor, dan file lain dalam elemen ini. Ketika datang ke file aset seperti gambar, tampilan, suara, dll., mereka juga harus dienkapsulasi dalam elemen sehingga di masa depan mereka akan mudah digunakan kembali. Anda cukup menyalin seluruh folder ke proyek lain dan itu dia!

Di bawah ini, Anda dapat melihat contoh grafik yang menunjukkan elemen yang lebih maju. Perhatikan bahwa ini terdiri dari tampilan yang digunakan dan tidak bergantung pada file aplikasi lain. Jika Anda ingin mengetahui metode sederhana untuk memeriksa dependensi, lihat saja bagian impor. Apakah ada file dari luar elemen saat ini? Jika demikian, maka Anda perlu menghapus dependensi tersebut dengan memindahkannya ke dalam elemen atau dengan menambahkan panggilan yang sesuai ke listener.

Diagram sederhana dari elemen yang lebih kompleks. Di sini, arti yang lebih besar dari kata "elemen" terdiri dari enam bagian: View; Logika A, B, dan C; Elemen; dan Elemen Pendengar. Hubungan antara dua yang terakhir dan Aplikasi sama seperti sebelumnya, tetapi Elemen dalam juga menggunakan Logika A dan C. Logika C menggunakan Logika A dan B. Logika A menggunakan Logika B dan Tampilan.

Mari kita lihat juga contoh sederhana “Hello World” yang dibuat di Java.

 public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String[] args) { App app = new App(); app.start(); } }

Awalnya, kami mendefinisikan ElementListener untuk menentukan metode yang mencetak output. Elemen itu sendiri didefinisikan di bawah ini. Saat memanggil sayHello pada elemen, itu hanya mencetak pesan menggunakan ElementListener . Perhatikan bahwa elemen sepenuhnya independen dari implementasi metode printOutput . Itu dapat dicetak ke konsol, printer fisik, atau UI mewah. Elemen tidak bergantung pada implementasi itu. Karena abstraksi ini, elemen ini dapat digunakan kembali dalam aplikasi yang berbeda dengan mudah.

Sekarang lihat kelas App utama. Ini mengimplementasikan pendengar dan merakit elemen bersama dengan implementasi konkret. Sekarang kita bisa mulai menggunakannya.

Anda juga dapat menjalankan contoh ini dalam JavaScript di sini

Arsitektur Elemen

Mari kita lihat penggunaan pola elemen dalam aplikasi skala besar. Menunjukkannya dalam proyek kecil adalah satu hal—mengaplikasikannya ke dunia nyata adalah hal lain.

Struktur aplikasi web full-stack yang ingin saya gunakan terlihat sebagai berikut:

 src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements

Dalam folder kode sumber, kami awalnya membagi file klien dan server. Ini adalah hal yang wajar untuk dilakukan, karena keduanya berjalan di dua lingkungan yang berbeda: browser dan server back-end.

Kemudian kami membagi kode di setiap lapisan ke dalam folder yang disebut app dan elemen. Elemen terdiri dari folder dengan komponen independen, sedangkan folder aplikasi menghubungkan semua elemen dan menyimpan semua logika bisnis.

Dengan begitu, elemen dapat digunakan kembali di antara proyek yang berbeda, sementara semua kompleksitas khusus aplikasi dienkapsulasi dalam satu folder dan cukup sering direduksi menjadi panggilan sederhana ke elemen.

Contoh Langsung

Percaya bahwa praktik selalu mengalahkan teori, mari kita lihat contoh nyata yang dibuat di Node.js dan TypeScript.

Contoh Kehidupan Nyata

Ini adalah aplikasi web yang sangat sederhana yang dapat digunakan sebagai titik awal untuk solusi yang lebih maju. Itu mengikuti arsitektur elemen serta menggunakan pola elemen struktural yang ekstensif.

Dari sorotan, Anda dapat melihat bahwa halaman utama telah dibedakan sebagai elemen. Halaman ini mencakup tampilannya sendiri. Jadi, ketika, misalnya, Anda ingin menggunakannya kembali, Anda cukup menyalin seluruh folder dan memasukkannya ke dalam proyek yang berbeda. Cukup sambungkan semuanya dan Anda siap.

Ini adalah contoh dasar yang menunjukkan bahwa Anda dapat mulai memperkenalkan elemen dalam aplikasi Anda sendiri hari ini. Anda dapat mulai membedakan komponen independen dan memisahkan logikanya. Tidak masalah seberapa berantakan kode yang sedang Anda kerjakan.

Kembangkan Lebih Cepat, Gunakan Kembali Lebih Sering!

Saya berharap, dengan seperangkat alat baru ini, Anda dapat lebih mudah mengembangkan kode yang lebih mudah dipelihara. Sebelum Anda mulai menggunakan pola elemen dalam praktik, mari kita rekap semua poin utama dengan cepat:

  • Banyak masalah dalam perangkat lunak terjadi karena ketergantungan antara banyak komponen.

  • Dengan membuat perubahan di satu tempat, Anda dapat memperkenalkan perilaku yang tidak terduga di tempat lain.

Tiga pendekatan arsitektur umum adalah:

  • Bola lumpur yang besar. Ini bagus untuk pengembangan yang cepat, tetapi tidak begitu bagus untuk tujuan produksi yang stabil.

  • Injeksi ketergantungan. Ini adalah solusi setengah matang yang harus Anda hindari.

  • arsitektur elemen. Solusi ini memungkinkan Anda membuat komponen independen dan menggunakannya kembali di proyek lain. Ini dapat dipertahankan dan brilian untuk rilis produksi yang stabil.

Pola elemen dasar terdiri dari kelas utama yang memiliki semua metode utama serta pendengar yang merupakan antarmuka sederhana yang memungkinkan komunikasi dengan dunia luar.

Untuk mencapai arsitektur elemen tumpukan penuh, pertama-tama Anda memisahkan front-end Anda dari kode back-end. Kemudian Anda membuat folder di masing-masing untuk aplikasi dan elemen. Folder elemen terdiri dari semua elemen independen, sedangkan folder aplikasi menyatukan semuanya.

Sekarang Anda dapat pergi dan mulai membuat dan membagikan elemen Anda sendiri. Dalam jangka panjang, ini akan membantu Anda membuat produk yang mudah dirawat. Semoga berhasil dan beri tahu saya apa yang Anda buat!

Juga, jika Anda menemukan diri Anda terlalu dini mengoptimalkan kode Anda, baca Cara Menghindari Kutukan Pengoptimalan Prematur oleh sesama Toptaler Kevin Bloch.

Terkait: Praktik Terbaik JS: Bangun Bot Perselisihan dengan TypeScript dan Injeksi Ketergantungan