Dependency Inversion Principle (DIP)

Ide utama dari prinsip ini adalah mengurangi masalah saling ketergantungan dari module-module pada software atau aplikasi. Karena ketergantungan antar module, akan menyulitkan dalam memaintain module-module tersebut pada fase selanjutnya. Sebagai contoh yaitu ketika ada perubahan pada low level module, maka high level module yang terkait dengan module tersebut juga harus mengalami perubahan. Akibatnya jika module-module yang terkait pada module yang berubah tersebut banyak jumlahnya, dipastikan menambah beban timeline development untuk merivisi module-module tersebut lebih panjang. Hal tersebut akan sangat-sangat berpengaruh besar pada biaya development dan bisnis. Selain itu juga akan memperbesar kemungkinan kerusakan pada module-module yang tidak seharusnya ikut diubah.
Tulisan ini adalah bagian dari SOLID Principles Series, sehingga untuk bisa membaca secara menyeluruh dan berkesinambungan, alangkah lebih baik bisa membaca secara urut dari link series berikut.
Berdasarkan Wikipedia, dua point utama dari prinsip ini:
High level modules should not import anything from low-level modules. Both should depend on abstractions (module-module High-level)
Abstraction should not depend on details. Details (concrete implementations) should depend on abstractions.
Beberapa mungkin bingung, seperti apa itu high-level module, apa itu low-level module dan juga yang dimaksud abstraction. Yang pertama perlu diingat bahwa module itu bisa berupa class, bisa kumpulan class untuk menyelesaikan kasus penggunaan tertentu (usecase). Berikut sedikit penjelasan mengenai istilah-istilah tersebut.
High-Level module merupakan module teratas/terdepan yang memerlukan module/class yang lebih detail untuk menyelesaikan suatu masalah atau logic. Cirinya, module ini akan menginisialisasi module lain di dalamnya.
Low-Level module merupakan module yang lebih detail untuk menyelesaikan suatu masalah atau logic. Cirinya, module ini tidak menginisialisasi module tertentu di dalamnya.
Abstraction, proses men-generalkan sebuah logic yang kompleks atau menyembunyikan detail-detail yang tidak diperlukan untuk diketahui antar module. Biasanya menggunakan interface atau abstract class.
Untuk lebih memperjelas, mari kita lihat dari contoh code berikut.
Account Class File
class EmailAccount {
public emailBucket: string[] = []
public notifyType: string
constructor() {
this.sendType = 'email'
}
addEmail(email: string) {
this.emailBucket.push(email)
}
validate(email: string) {
return email
}
}
Notification Class File
class Notifier {
private account: EmailAccount
constructor(email: string) {
this.account = new EmailAccount
this.account.addEmail(email)
}
send() {
const data = this.send.emailBucket
const type = this.send.notifyType
this.sendTo(type, data)
}
sendTo(type: string, data: string) {
console.log({type, data})
}
}
Code di atas memperlihatkan bahwa class Notifier
adalah high-level module
karena class atau module ini membutuhkan class yang lebih detail (EmailAccount
) untuk menyelesaikan tugasnya, terlihat pada constructor class ini. Sedangkan class EmailAccount
disebut juga sebagai low-level module
karena class atau module ini lebih explicit yaitu management account email dan utamanya module ini tidak menginisialisasi atau tidak memerlukan module lain di dalamnya.
Seperti terlihat pada class Notifier
pada section constructor, class ini meng-initialisasi EmailAccount
yang diset ke property account
. Lalu pada bagian method send
terjadi pengambilan property emailBucket
dan notifyType
yang merupaka property milik EmailAccount
. Bagian ini lah yang menjadi masalah. Class atau module ini menyalahi aturan dari Dependency Inversion Principle (DIP). Karena seandainya terjadi perubahan pada property pada module EmailAccount, misalnya property emailBucket
berubah menjadi emails
maka programmer harus merevisi juga high-level module
yaitu class Notifier
. Seharusnya agar tidak melanggar aturan DIP, sebaiknya jika terjadi perubahan pada EmailAccount
module , maka pada Notifier
tidak perlu ada perubahan. Untuk programmer perlu melakukan abstraction yang biasanya menggunakan interface
atau abstract class
. Sebagai contoh code di sebelumnya diubah menjadi seperti berikut.
Abstraction
type Prop = {
type: string
data: string[]
}
// MG is short of Management
interface IAccountMG {
getData(): Prop
}
Pertama kita perlu melakukan abstraction
. Seperti pada contoh snippet code di atas melakukan abstraction dengan memanfaatkan type
dan interface
. Di typescript sendiri, kita bisa menggunakan banyak cara untuk abstraction
, bisa menggunakan type
, interface
atau juga menggunakan abstract class
. Tapi untuk contoh kasus di code ini cukup menggunakan type
dan interface
saja.
Seperti yang terlihat pada interface IAccountMG
terdapat member method getData
dengan return value berupa type Prop
. Tujuan dari type Prop
adalah kontrak agar module apapun yang meng-implement interface IAccountMG
maka return dari getData
harus selalu ber-type Prop
dengan begitu return type akan selalu konsisten.
Account class
class EmailAccount implements IAccountMG {
public emailBucket: string[] = []
public notifyType: string
constructor() {
this.sendType = 'email'
}
addEmail(email: string) {
email = this.validate(email)
this.emailBucket.push(email)
}
validate(email: string) {
return email
}
getData() {
return {
data: this.emailBucket,
type: this.notifyType
}
}
}
Selanjutnya class EmailAccount
meng-implement interface IAccountMG
. Dengan meng-implement interface IAccountMG
maka class EmailAccount
akan dipaksa untuk meng-implementasi method getData
juga. Karena method ini yang akan di call pada high-level module
maka dari itu method getData
wajib ada dan callable (bisa dieksekusi).
Notifier Class
class Notifier {
private account: IAccountMG
constructor(email: string) {
this.account = new EmailAccount
this.account.addEmail(email)
}
send() {
const payload: Prop = this.account.getData()
this.sendTo(payload)
}
sendTo(payload: Prop) {
console.log(payload)
}
}
Lalu pada contructor
class Notifier
sama seperti sebelumnya yaitu menginisialisasi class EmailAccount. Tapi yang berbeda kali ini adalah type property account
pada Notifier
bukan lagi EmailAccount
melainkan interface IAccountMG
. Dengan begini module Notifier
tidak perlu tahu yang dilakukan getData
pada module account
. Yang perlu diketahui oleh module Notifier
hanya bahwa getData
bersifat dapat dieksekusi dan memberikan return type Prop. Itulah tujuan dari abstraction
yaitu menyederhanakan module dengan menyembunyikan detail pada Low-Level Module
yang tidak perlu diketahui oleh High-Level module
.
Lalu kenapa menggunakan penamaan method getData
dari pada getEmail
misalnya. Kembali ke point ke dua pada gagasan dari DIP, yaitu abstraction tidak boleh berdasarkan detail dari implementator. Implementator-lah yang harus mengikuti abstraction. Maksudnya penamaan dan fungsi member method harus lebih general, seperti misal pada kasus code di sini, jika misalkan akan menambah module lain yang bukan berupa kumpulan email maka getData
sudah cukup untuk merepresentasikan module implementatornya.
references:
Subscribe to my newsletter
Read articles from Andriawan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by