Banyak pengembang Node.js menggunakan modul (khusus) dependensi keras menggunakan memerlukan () untuk mengikat modul, tetapi ada pendekatan lain dengan pro dan kontra mereka. Saya akan membicarakannya di artikel ini. Empat pendekatan akan dipertimbangkan:
- Ketergantungan keras (membutuhkan ())
- Injeksi Ketergantungan
- Pencari Lokasi Layanan
- Kontainer Dependensi Tertanam (DI Container)
Sedikit tentang modul
Modul dan arsitektur modular adalah dasar dari Node.js. Modul menyediakan enkapsulasi (menyembunyikan detail implementasi dan hanya membuka antarmuka menggunakan module.exports), penggunaan kembali kode, pemecahan kode logis menjadi file. Hampir semua aplikasi Node.js terdiri dari banyak modul yang entah bagaimana harus berinteraksi. Jika Anda salah mengikat modul atau bahkan membiarkan interaksi modul melayang, maka Anda dapat dengan cepat menemukan bahwa aplikasi mulai "berantakan": perubahan kode di satu tempat menyebabkan kerusakan di tempat lain, dan unit testing menjadi sangat mustahil. Idealnya, modul harus memiliki
konektivitas tinggi, tetapi
kopling rendah.
Kecanduan keras
Ketergantungan yang kuat dari satu modul pada modul lainnya terjadi ketika membutuhkan () digunakan. Ini adalah pendekatan yang efektif, sederhana dan umum. Misalnya, kami hanya ingin menghubungkan modul yang bertanggung jawab untuk berinteraksi dengan database:
Pro:
- Kesederhanaan
- Organisasi visual modul
- Debugging mudah
Cons:
- Kesulitan untuk menggunakan kembali modul (misalnya, jika kita ingin menggunakan modul kita berulang kali, tetapi dengan contoh database yang berbeda)
- Kesulitan untuk pengujian unit (Anda harus membuat contoh database dummy dan entah bagaimana meneruskannya ke modul)
Ringkasan:
Pendekatan ini baik untuk aplikasi kecil atau prototipe, serta untuk menghubungkan modul stateless: pabrik, perancang dan set fitur.
Injeksi Ketergantungan
Gagasan utama injeksi ketergantungan adalah untuk mentransfer dependensi dari komponen eksternal ke modul. Dengan demikian, ketergantungan keras pada modul dihilangkan dan menjadi mungkin untuk menggunakannya kembali dalam konteks yang berbeda (misalnya, dengan contoh database yang berbeda).
Injeksi ketergantungan dapat diimplementasikan dengan melewatkan dependensi dalam argumen konstruktor atau dengan mengatur properti modul, tetapi dalam praktiknya lebih baik menggunakan metode pertama. Mari kita terapkan implementasi dependensi dalam praktik dengan membuat instance database menggunakan pabrik dan meneruskannya ke modul kami:
Modul eksternal:
const dbFactory = require('db'); const OurModule = require('./ourModule.js'); const dbInstance = dbFactory.createInstance('instance1'); const ourModule = OurModule(dbInstance);
Sekarang kita tidak hanya dapat menggunakan kembali modul kita, tetapi juga dengan mudah menulis unit test untuknya: cukup buat objek tiruan untuk instance basis data dan berikan ke modul.
Pro:
- Kemudahan tes unit menulis
- Meningkatkan penggunaan kembali modul
- Keterlibatan menurun, konektivitas meningkat
- Mengalihkan tanggung jawab untuk menciptakan dependensi ke level yang lebih tinggi - seringkali ini meningkatkan keterbacaan program, karena dependensi penting dikumpulkan di satu tempat, dan tidak disebarkan oleh modul
Cons:
- Kebutuhan untuk desain dependensi yang lebih menyeluruh: misalnya, urutan inisialisasi modul tertentu harus diikuti
- Kompleksitas manajemen ketergantungan, terutama ketika ada banyak
- Kemunduran dalam kelengkapan kode modul: menulis kode modul ketika ketergantungan datang dari luar lebih sulit karena kita tidak bisa langsung melihat ketergantungan ini.
Ringkasan:
Ketergantungan injeksi meningkatkan kompleksitas dan ukuran aplikasi, tetapi sebagai imbalannya memungkinkan penggunaan kembali dan membuat pengujian lebih mudah. Pengembang harus memutuskan apa yang lebih penting baginya dalam kasus tertentu - kesederhanaan dari ketergantungan yang sulit atau kemungkinan yang lebih luas untuk memperkenalkan ketergantungan.
Pencari Lokasi Layanan
Idenya adalah untuk memiliki registry dependensi yang bertindak sebagai perantara ketika memuat dependensi dengan modul apa pun. Alih-alih mengikat dengan keras, dependensi diminta oleh modul dari pencari layanan. Jelas, modul memiliki ketergantungan baru - pelacak layanan itu sendiri. Contoh dari pelacak layanan adalah sistem modul Node.js: modul meminta dependensi menggunakan require (). Dalam contoh berikut, kami akan membuat pencari lokasi layanan, mendaftarkan instance basis data, dan modul kami di dalamnya.
Modul eksternal:
const serviceLocator = require('./serviceLocator.js')(); serviceLocator.register('someParameter', 'someValue'); serviceLocator.factory('db', require('db')); serviceLocator.factory('ourModule', require('ourModule')); const ourModule = serviceLocator.get('ourModule');
Modul kami:
Perlu dicatat bahwa pelacak layanan menyimpan pabrik layanan alih-alih instance, dan itu masuk akal. Kami mendapat manfaat dari inisialisasi malas, dan sekarang kami tidak perlu khawatir tentang urutan inisialisasi modul - semua modul akan diinisialisasi saat dibutuhkan. Plus, kami mendapat kesempatan untuk menyimpan parameter di pencari layanan (lihat "someParameter").
Pro:
- Kemudahan tes unit menulis
- Menggunakan kembali modul lebih mudah daripada dengan kecanduan yang keras
- Mengurangi keterlibatan, meningkatkan konektivitas dibandingkan dengan kecanduan keras
- Mengalihkan tanggung jawab untuk menciptakan ketergantungan ke tingkat yang lebih tinggi
- Tidak perlu mengikuti urutan inisialisasi modul
Cons:
- Menggunakan kembali modul lebih sulit daripada menerapkan dependensi (karena ketergantungan tambahan dari pencari lokasi)
- Keterbacaan: Lebih sulit untuk memahami apa ketergantungan yang diperlukan oleh pencari layanan
- Keterlibatan meningkat dibandingkan dengan injeksi ketergantungan
Ringkasan
Secara umum, pelacak layanan mirip dengan injeksi ketergantungan, dalam beberapa hal lebih mudah (tidak ada urutan inisialisasi), dalam beberapa kasus lebih sulit (kurang dari kemungkinan menggunakan kembali kode).
Kontainer Dependensi Tertanam (DI Container)
Locator layanan memiliki kelemahan karena jarang diterapkan dalam praktik - ketergantungan modul pada locator itu sendiri. Wadah dependensi tertanam (wadah DI) tidak memiliki kelemahan ini. Bahkan, ini adalah pencari layanan yang sama dengan fungsi tambahan yang menentukan dependensi modul sebelum membuat turunannya. Anda bisa menentukan dependensi modul dengan mem-parsing dan mengekstraksi argumen dari konstruktor modul (dalam JavaScript, Anda bisa melemparkan tautan ke fungsi ke string menggunakan toString ()). Metode ini cocok jika pengembangan berjalan murni untuk server. Jika kode klien ditulis, sering kali diperkecil dan tidak ada gunanya mengekstrak nama argumen. Dalam hal ini, daftar dependensi dapat diteruskan sebagai array string (dalam Angular.js, berdasarkan penggunaan wadah DI, pendekatan ini digunakan). Kami menerapkan wadah DI menggunakan parsing argumen konstruktor:
const fnArgs = require('parse-fn-args'); module.exports = function() { const dependencies = {}; const factories = {}; const diContainer = {}; diContainer.factory = (name, factory) => { factories[name] = factory; }; diContainer.register = (name, dep) => { dependencies[name] = dep; }; diContainer.get = (name) => { if(!dependencies[name]) { const factory = factories[name]; dependencies[name] = factory && diContainer.inject(factory); if(!dependencies[name]) { throw new Error('Cannot find module: ' + name); } } diContainer.inject = (factory) => { const args = fnArgs(factory) .map(dependency => diContainer.get(dependency)); return factory.apply(null, args); } return dependencies[name]; };
Dibandingkan dengan pelacak layanan, metode injeksi telah ditambahkan, yang menentukan dependensi modul sebelum membuat turunannya. Kode modul eksternal tidak banyak berubah:
const diContainer = require('./diContainer.js')(); diContainer.register('someParameter', 'someValue'); diContainer.factory('db', require('db')); diContainer.factory('ourModule', require('ourModule')); const ourModule = diContainer.get('ourModule');
Modul kami terlihat persis sama dengan injeksi ketergantungan sederhana:
Sekarang modul kami dapat disebut keduanya dengan bantuan wadah DI dan melewatkannya contoh ketergantungan yang diperlukan secara langsung, menggunakan injeksi ketergantungan sederhana.
Pro:
- Kemudahan tes unit menulis
- Penggunaan kembali modul dengan mudah
- Berkurangnya keterlibatan, peningkatan konektivitas modul (terutama dibandingkan dengan pencari layanan)
- Mengalihkan tanggung jawab untuk menciptakan ketergantungan ke tingkat yang lebih tinggi
- Tidak perlu melacak inisialisasi modul
Minus terbesar:
- Komplikasi signifikan dari logika pengikatan modul
Ringkasan
Pendekatan ini lebih sulit untuk dipahami dan mengandung sedikit lebih banyak kode, tetapi sepadan dengan waktu yang dihabiskan untuk itu karena kekuatan dan keanggunannya. Dalam proyek-proyek kecil, pendekatan ini mungkin berlebihan, tetapi harus dipertimbangkan jika aplikasi besar sedang dirancang.
Kesimpulan
Pendekatan dasar untuk pengikatan modul di Node.js. dipertimbangkan. Seperti yang biasanya terjadi, "peluru perak" tidak ada, tetapi pengembang harus menyadari alternatif yang mungkin dan memilih solusi yang paling cocok untuk setiap kasus tertentu.
Artikel ini didasarkan pada bab dari buku
Node.js Design Patterns yang dirilis pada 2017. Sayangnya, banyak hal dalam buku ini sudah usang, jadi saya tidak bisa merekomendasikan 100% untuk membacanya, tetapi beberapa hal masih relevan sampai sekarang.