
Dalam artikel ini, saya akan berbicara tentang dasar-dasar injeksi ketergantungan (Eng. Dependency Injection, DI ) dalam bahasa yang sederhana, dan juga tentang alasan untuk menggunakan pendekatan ini. Artikel ini ditujukan bagi mereka yang tidak tahu injeksi ketergantungan apa, atau yang meragukan perlunya menggunakan teknik ini. Jadi mari kita mulai.
Apa itu kecanduan?
Mari kita lihat contoh pertama. Kami memiliki ClassA
, ClassB
dan ClassC
seperti yang ditunjukkan di bawah ini:
class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { }
Anda dapat melihat bahwa kelas ClassA
berisi turunan dari kelas ClassB
, jadi kita dapat mengatakan bahwa kelas ClassA
bergantung pada kelas ClassB
. Mengapa Karena ClassA
membutuhkan ClassB
untuk bekerja dengan benar. Kita juga dapat mengatakan bahwa kelas ClassB
adalah ketergantungan dari kelas ClassA
.
Sebelum melanjutkan, saya ingin menjelaskan bahwa hubungan seperti itu baik, karena kita tidak perlu satu kelas untuk melakukan semua pekerjaan dalam aplikasi. Kita perlu membagi logika ke dalam kelas yang berbeda, yang masing-masing akan bertanggung jawab untuk fungsi tertentu. Dan dalam hal ini, kelas-kelas akan dapat berinteraksi secara efektif.
Bagaimana cara bekerja dengan dependensi?
Mari kita lihat tiga metode yang digunakan untuk melakukan tugas injeksi ketergantungan:
Cara pertama: buat dependensi di kelas dependen
Sederhananya, kita dapat membuat objek kapan pun kita membutuhkannya. Lihatlah contoh berikut:
class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } }
Sangat mudah! Kami membuat kelas saat kami membutuhkannya.
Manfaatnya
- Mudah dan sederhana.
- Kelas dependen (
ClassA
dalam kasus kami) sepenuhnya mengontrol bagaimana dan kapan membuat dependensi.
Kekurangan
ClassA
dan ClassB
terkait erat satu sama lain. Karena itu, setiap kali kita perlu menggunakan ClassA
, kita akan dipaksa untuk menggunakan ClassB
, dan tidak mungkin untuk mengganti ClassB
dengan yang lain .- Dengan perubahan apa pun dalam inisialisasi kelas
ClassB
, Anda harus menyesuaikan kode di dalam kelas ClassA
(dan semua kelas lainnya bergantung pada ClassB
). Ini menyulitkan proses mengubah ketergantungan. ClassA
tidak dapat diuji. Jika Anda perlu menguji suatu kelas, namun ini adalah salah satu aspek terpenting dari pengembangan perangkat lunak, maka Anda harus melakukan pengujian unit masing-masing kelas secara terpisah. Ini berarti bahwa jika Anda ingin memverifikasi operasi yang benar dari kelas ClassA
dan membuat beberapa tes unit untuk memverifikasinya, maka, seperti yang ditunjukkan dalam contoh, Anda akan membuat instance kelas ClassB
, bahkan ketika Anda tidak tertarik dengannya. Jika kesalahan terjadi selama pengujian, maka Anda tidak akan dapat memahami di mana letaknya - di ClassA
atau ClassB
. Bagaimanapun, ada kemungkinan bahwa bagian dari kode di ClassB
menyebabkan kesalahan, sementara ClassA
bekerja dengan benar. Dengan kata lain, pengujian unit tidak dimungkinkan karena modul (kelas) tidak dapat dipisahkan satu sama lain.ClassA
harus dikonfigurasi sehingga dapat menyuntikkan dependensi. Dalam contoh kita, dia perlu tahu cara membuat ClassC
dan menggunakannya untuk membuat ClassB
. Akan lebih baik jika dia tidak tahu apa-apa tentang itu. Mengapa Karena prinsip tanggung jawab tunggal .
Setiap kelas seharusnya hanya melakukan tugasnya.
Oleh karena itu, kami tidak ingin kelas bertanggung jawab atas apa pun selain tugas mereka sendiri. Implementasi dependensi adalah tugas tambahan yang kami tetapkan untuk mereka.
Cara kedua: menyuntikkan dependensi melalui kelas kustom
Jadi, memahami bahwa menyuntikkan dependensi dalam kelas dependen bukanlah ide yang baik, mari kita jelajahi cara alternatif. Di sini, kelas dependen mendefinisikan semua dependensi yang dibutuhkan di dalam konstruktor dan memungkinkan kelas pengguna untuk menyediakannya. Apakah ini solusi untuk masalah kita? Kami akan mencari tahu nanti.
Lihatlah kode contoh di bawah ini:
class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium -
Sekarang ClassA
mendapatkan semua dependensi di dalam konstruktor dan cukup memanggil metode kelas ClassB
tanpa menginisialisasi apa pun.
Manfaatnya
ClassA
dan ClassB
sekarang longgar digabungkan, dan kita dapat mengganti ClassB
tanpa melanggar kode di dalam ClassA
. Sebagai contoh, alih-alih melewati ClassB
kita dapat melewati AssumeClassB
, yang merupakan subkelas dari ClassB
, dan program kami akan bekerja dengan baik.ClassA
sekarang dapat diuji. Saat menulis unit test, kita dapat membuat versi kita sendiri dari ClassB
(objek uji) dan meneruskannya ke ClassA
. Jika kesalahan terjadi saat lulus tes, sekarang kita tahu pasti bahwa ini pasti kesalahan di ClassA
.ClassB
dibebaskan dari bekerja dengan dependensi dan dapat fokus pada tugasnya.
Kekurangan
- Metode ini menyerupai mekanisme rantai, dan pada titik tertentu rantai harus terputus. Dengan kata lain, pengguna kelas
ClassA
harus tahu segalanya tentang inisialisasi ClassB
, yang pada gilirannya membutuhkan pengetahuan tentang inisialisasi ClassC
, dll. Jadi, Anda melihat bahwa setiap perubahan dalam konstruktor dari kelas-kelas ini dapat menyebabkan perubahan dalam kelas panggilan, belum lagi bahwa ClassA
dapat memiliki lebih dari satu pengguna, sehingga logika membuat objek akan diulang. - Terlepas dari kenyataan bahwa dependensi kami jelas dan mudah dipahami, kode pengguna tidak trivial dan sulit dikelola. Karena itu, semuanya tidak begitu sederhana. Selain itu, kode melanggar prinsip tanggung jawab tunggal, karena bertanggung jawab tidak hanya untuk pekerjaannya, tetapi juga untuk implementasi dependensi di kelas-kelas dependen.
Metode kedua jelas bekerja lebih baik daripada yang pertama, tetapi masih memiliki kekurangannya. Apakah mungkin menemukan solusi yang lebih cocok? Sebelum mempertimbangkan cara ketiga, mari kita bicara tentang konsep injeksi ketergantungan.
Apa itu injeksi ketergantungan?
Injeksi ketergantungan adalah cara untuk menangani dependensi di luar kelas dependen ketika kelas dependen tidak perlu melakukan apa pun.
Berdasarkan definisi ini, solusi pertama kami jelas tidak menggunakan ide injeksi dependensi, dan cara kedua adalah bahwa kelas dependen tidak melakukan apa pun untuk menyediakan dependensi. Tapi kami masih berpikir solusi kedua itu buruk. MENGAPA?!
Karena definisi injeksi dependensi tidak mengatakan apa-apa tentang di mana pekerjaan dengan dependensi harus dilakukan (kecuali di luar kelas dependen), pengembang harus memilih tempat yang cocok untuk injeksi dependensi. Seperti yang Anda lihat dari contoh kedua, kelas pengguna bukan tempat yang tepat.
Bagaimana melakukan yang lebih baik? Mari kita lihat cara ketiga untuk menangani dependensi.
Cara ketiga: biarkan orang lain menangani dependensi daripada kita
Menurut pendekatan pertama, kelas dependen bertanggung jawab untuk mendapatkan dependensi mereka sendiri, dan pada pendekatan kedua, kami memindahkan pemrosesan dependensi dari kelas dependen ke kelas pengguna. Mari kita bayangkan bahwa ada orang lain yang bisa menangani dependensi, sebagai akibatnya baik kelas dependen maupun pengguna tidak akan melakukan pekerjaan. Metode ini memungkinkan Anda untuk bekerja dengan dependensi dalam aplikasi secara langsung.
Implementasi injeksi ketergantungan yang “bersih” (menurut pendapat pribadi saya)
Tanggung jawab untuk menangani dependensi ada pada pihak ketiga, jadi tidak ada bagian dari aplikasi yang akan berinteraksi dengannya.
Ketergantungan injeksi bukanlah teknologi, kerangka kerja, perpustakaan, atau sesuatu seperti itu. Ini hanya sebuah ide. Idenya adalah untuk bekerja dengan dependensi di luar kelas dependen (lebih disukai di bagian yang dialokasikan secara khusus). Anda dapat menerapkan ide ini tanpa menggunakan pustaka atau kerangka kerja apa pun. Namun, kami biasanya beralih ke kerangka kerja untuk menerapkan dependensi, karena menyederhanakan pekerjaan dan menghindari penulisan kode templat.
Setiap kerangka kerja injeksi ketergantungan memiliki dua karakteristik yang melekat. Fungsi tambahan lainnya mungkin tersedia untuk Anda, tetapi dua fungsi ini akan selalu ada:
Pertama, kerangka kerja ini menawarkan cara untuk menentukan bidang (objek) yang harus diimplementasikan. Beberapa kerangka kerja melakukan ini dengan @Inject
keterangan bidang atau konstruktor menggunakan anotasi @Inject
, tetapi ada metode lain. Misalnya, Koin menggunakan fitur bahasa bawaan Kotlin untuk menentukan implementasi. Inject
berarti bahwa ketergantungan harus ditangani oleh kerangka kerja DI. Kode akan terlihat seperti ini:
class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } }
Kedua, kerangka kerja memungkinkan Anda untuk menentukan cara menyediakan setiap ketergantungan, dan ini terjadi dalam file terpisah. Kira-kira terlihat seperti ini (perlu diingat bahwa ini hanyalah contoh, dan mungkin berbeda dari kerangka ke kerangka kerja):
class OurThirdPartyGuy { fun provideClassC(){ return ClassC()
Jadi, seperti yang Anda lihat, setiap fungsi bertanggung jawab untuk memproses satu ketergantungan. Oleh karena itu, jika kita perlu menggunakan ClassA
di suatu tempat dalam aplikasi, hal berikut akan terjadi: Kerangka kerja DI kita menciptakan satu instance dari kelas ClassC
dengan memanggil provideClassC
, meneruskannya ke provideClassB
dan menerima instance dari ClassB
, yang dilewatkan untuk provideClassA
, dan sebagai hasilnya, ClassA
dibuat. Ini hampir ajaib. Sekarang mari kita periksa kelebihan dan kelebihan dari metode ketiga.
Manfaatnya
- Segalanya sesederhana mungkin. Baik kelas dependen dan kelas yang menyediakan dependensi jelas dan sederhana.
- Kelas digabungkan secara longgar dan mudah diganti oleh kelas lain. Misalkan kita ingin mengganti
ClassC
dengan AssumeClassC
, yang merupakan subkelas dari ClassC
. Untuk melakukan ini, Anda hanya perlu mengubah kode penyedia sebagai berikut, dan di mana pun ClassC
digunakan, versi baru sekarang akan digunakan secara otomatis:
fun provideClassC(){ return AssumeClassC() }
Harap dicatat bahwa tidak ada kode di dalam aplikasi yang berubah, hanya metode penyedia. Tampaknya tidak ada yang lebih sederhana dan lebih fleksibel.
- Testabilitas yang luar biasa. Anda dapat dengan mudah mengganti dependensi dengan versi uji selama pengujian. Faktanya, injeksi ketergantungan adalah penolong utama Anda dalam hal pengujian.
- Memperbaiki struktur kode, seperti aplikasi memiliki tempat terpisah untuk pemrosesan ketergantungan. Akibatnya, sisa aplikasi dapat fokus secara eksklusif pada fungsinya dan tidak tumpang tindih dengan dependensi.
Kekurangan
- Kerangka kerja DI memiliki ambang masuk tertentu, sehingga tim proyek perlu menghabiskan waktu dan mempelajarinya sebelum menggunakannya secara efektif.
Kesimpulan
- Penanganan ketergantungan tanpa DI dimungkinkan, tetapi hal itu dapat menyebabkan crash aplikasi.
- DI hanyalah ide yang efektif, yang dengannya memungkinkan untuk menangani dependensi di luar kelas dependen.
- Paling efektif menggunakan DI di bagian-bagian tertentu dari aplikasi. Banyak kerangka kerja berkontribusi untuk ini.
- Kerangka kerja dan perpustakaan tidak diperlukan untuk DI, tetapi mereka dapat banyak membantu.
Dalam artikel ini, saya mencoba menjelaskan dasar-dasar bekerja dengan konsep injeksi ketergantungan, dan juga mencantumkan alasan untuk menggunakan ide ini. Ada banyak sumber daya yang dapat Anda jelajahi untuk mempelajari lebih lanjut tentang penggunaan DI di aplikasi Anda sendiri. Sebagai contoh, bagian terpisah di bagian lanjutan dari kursus profesi Android kami didedikasikan untuk topik ini.