Dependency Inversion Principle (DIP)

AndriawanAndriawan
5 min read

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:

0
Subscribe to my newsletter

Read articles from Andriawan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Andriawan
Andriawan