Dependency Injection Dengan Pendekatan Functional
Istilah Dependency Injection (DI) biasa digunakan untuk merujuk pada teknik dimana suatu object menyuplai dependencies dari object lain. Tujuan utamanya adalah agar object tersebut tidak terikat pada satu implementasi tertentu saja, membuatnya menjadi lebih fleksibel terhadap perubahan. Istilah ini cukup dikenal dalam dunia Object-Oriented Programming.
Berikut contoh kecil DI:
- Kita definisikan dependency di constructor class [1] dan berikan constraint dengan sebuah interface agar implementasi object
fsService
dapat diganti-ganti sesuka hati selama comply dengan interface tersebut. - Dependency dapat diakses dengan keyword
this
[2]. - Dependency diberikan ketika object
AppGenerator
diinstansiasi [3].
Dengan pemodelan dependency lewat class constructor ini, class AppGenerator
menjadi lebih fleksibel dan tidak terikat pada satu implementasi saja. We program to an interface, not implementation. Detil implementasi dapat diganti semisal ketika testing:
Testing jadi jauh lebih mudah dan gak perlu berinteraksi dengan file system beneran. AppGenerator
cukup tau apa yang perlu dilakukan namun tak perlu tau bagaimana melakukannya. Sering juga disebut dengan Inversion of Control.
Pertanyaannya: bagaimana jika dilakukan dengan pendekatan functional? Apakah bisa dilakukan tanpa adanya class? Cukup dengan function?
Explicit Dependencies
Kita dapat melihat pada contoh di atas bahwa method initProject
bergantung pada object fsService
yang kemudian diakses melalui magic keyword “this”. Bicara dalam konteks function, kita tak lagi dapat mengandalkan keyword “this” dan harus mencari cara lain agar tetap bisa mengakses fsService
.
Satu-satunya cara adalah dengan menyuplai fsService
langsung melalui function argument. Kenyataannya pure function memang mengharuskan kita untuk memberi semua data yang diperlukan melalui argument. Sehingga DI tetap bisa dilakukan dengan cara ini.
Well, technically dependencies == function arguments.
No more this
! Dependency didefinisikan langsung di function argument tanpa mengabaikan substitutability object fsService
.
Factory Function
Anggap saja requirement berubah di kemudian hari. User butuh fitur logging [1] untuk mengetahui apakah program berhasil dijalankan atau tidak. Lalu ada tambahan options [2] juga agar user dapat mengkostumasi project dengan lebih leluasa.
Fungsi initProject
kini membutuhkan dua buah external service (fsService
dan logService
) dan dua buah argument “biasa” (projectName
dan options
). Pada dasarnya mereka semua sama-sama function argument, hanya concern-nya saja yang berbeda. Kita bisa melakukan sedikit refactor untuk membuat external service terlihat lebih eksplisit dan terpisah dari yang lainnya dengan factory function.
makeInitProject
menerima dua buah service dan mengembalikan fungsi initProject
yang menerima argument “biasa”. makeInitProject
seolah berperan sebagai constructor
dalam menerima dan menyimpan dependencies dengan bantuan closure. Namun esensinya pendekatan ini tidaklah berbeda dengan solusi sebelumnya.
Di artikel Functional Approach to Dependency Injection si penulis juga menggunakan pendekatan ini yang bisa dicapai dengan partial application jika bahasanya mendukung currying, seperti F#.
Di artikel lain seperti Dependency Injection using Modules yang ditulis menggunakan ReasonML, juga memanfaatkan ide yang sama.
Who needs class when you have functions 😛
Saya ingin menambahkan satu pendekatan lain yang mirip dengan factory function: yaitu type class.
Type Class
Cara lain agar DI dapat diimplementasikan dengan cara functional adalah melalui [type class]({{< ref “./kenalan-dulu-sama-type-class.md” >}}). Beberapa yang mendukung fitur type class diantaranya Haskell, Idris, dan Purescript. Type class sendiri adalah kumpulan-kumpulan method tanpa implementasi — seperti interface — yang memungkinkan tercapainya ad-hoc polymorphism.
So, how does it look like?
- Pertama, kita harus definisikan type class
FsService
[1] yang memiliki dua buah method:exists
dancreate
. Berikanm
constraint Monad agar nantinya bisa kita berikan instanceEffect
(atauAff
untuk komputasi asynchronous) dan menjalankan real side-effect (IO operation). - Berikan constraint
FsService
[2] pada functioninitProject
agar kita bisa memanggil kedua buah methodFsService
[3a, 3b] di dalamnya.
Dibandingkan dengan pendekatan-pendekatan sebelumnya di atas, kita tidak melihat dependency fsService
terdefinisikan di function argument, hanya constraint class FsService
di type signature saja. Lalu bagaimana kita bisa meng-inject implementasi konkrit dari class FsService
kalau fungsi initProject
sendiri tidak menerimanya di argument?
HINT 💡:
Compiler yang menyuplainya saat compile time
Yang harus kita lakukan saat ini hanyalah membuat implementasi (instance) dari type class FsService
semisal:
Mari bahas step-by-step:
- Kita memberikan
Aff
— sebuah Monad yang merepresentasikan komputasi asynchronous — instanceFsService
[1] dan beri nama instance tersebutfsServiceAff
. Ini berarti methodexists
dancreate
bisa kita panggil di dalam konteks Aff (we’ll do it below 👇🏼). - Implementation details [2]. Jika dipanggil, program akan berinteraksi dengan file system.
Tinggal satu langkah tersisa: tempatkan function initProject
ke dalam konteks Aff
agar compiler dapat meng-inject instance fsServiceAff
. Kita bisa menggunakan fungsi launchAff_
.
Langkah barusan sangat penting karena jika tidak, compiler tidak akan bisa melakukan DI. Seandainya kita tempatkan fungsi initProject
misal ke dalam konteks Effect
seperti main = pure $ initProject "my-awesome-project"
, compiler akan gagal mencari instance FsService Effect
karena kita memang belum membuatnya: “No type class instance was found for FsService Effect
.”
Kembali ke konteks Aff
. Jika kita intip output hasil compile-nya, kita akan melihat bagaimana compiler melakukan DI untuk kita secara otomatis:
Bisa dicoba di link ini.
Dependency Baru
Sejauh ini kita dapat menyimpulkan bahwa compiler melakukan dependency injection saat compile time hanya dengan mendeklarasikan constraint type class pada type signature. Artinya ketika kita punya service tambahan e.g logService
dan promptService
, kita hanya perlu mengubah type signature-nya dan voila, dependencies ter-inject.
Hasil compile-nya:
Nice! 🎉 🎉 🎉
Ganti Implementasi
Di awal artikel saya menyebutkan salah satu benefit DI adalah kemampuan mengganti detil implementasi service tanpa perlu mengubah code yang menggunakannya. Dalam hal ini type class juga bisa diinstansiasi oleh tipe data lain. Contohnya ketika testing, kita ingin membuat “spy” terhadap method FsService
dengan memanfaatkan Writer monad.
No problems whatsoever.
Putting it All Together
Dengan pattern ini, kita tidak lagi melakukan Dependency Injection dengan menyuplainya secara eksplisit lewat function argument seperti solusi di awal artikel. Kita mengalihkannya ke type signature. Biar compiler yang menuntaskan pekerjaan Dependency Injection-nya. Dari segi estetika, dependencies dan function arguments juga terpisah jelas, sesuai goal awal kita.
Menurut saya, ini win-win solution untuk masalah DI 😉
To sum up, this may be our final code:
Wrap up
Kita tetap bisa melakukan DI dengan hanya menggunakan functions. Factory function dapat membantu penulisan kode menjadi lebih jelas mana dependency (service) dan mana function argument biasa. Dan ternyata pendekatan ini cukup lumrah di bahasa-bahasa functional lainnya.
Saya harap artikel ini bermanfaat dalam menambah wawasan teman-teman sekalian. Terima kasih banyak. Stay well 🙂