Liskov Subtitution Principle (LSP)

AndriawanAndriawan
5 min read

Gagasan atau idea yang dikenalkan oleh Barbara Liskov pada tahun 1987 pada conference dengan judul Data abstraction and Hierarchy. Dalam gagasannya memiliki kesimpulan kurang lebih seperti ini, Jika S adalah subtitusi dari T, maka T bisa digantikan oleh S tanpa merubah properti-properti pada program. Maka syarat S agar dapat menggantikan T adalah S harus memiliki perilaku seperti T dengan syarat-syarat tertentu.

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.

Jika dalam buku “Design Principles and Design Pattern” menyebutkan seperti ini

“Subclass should be substitutable for their base classes”

Bahwa class turunan harus bisa disubtitusikan untuk Class Basenya. Sebagai contoh, jika Sebuah function semisal function office(ketua: PemimpinClass), jika WakilKetuaClass merupakan subclass dari PemimpinClass maka argument pada fungsi office bisa digantikan oleh WakilKetuaClass.

Principle ini terlihat sederhana dan banyak yang tidak terlalu mendetailkan prinsip ini. Menurut saya pribadi prinsip ini merupakan prinsip paling rumit dibanding 4 prinsip lainnya dalam SOLID, karena pada prinsip ini ada beberapa aturan yang harus dipatuhi. Terutama konsep-konsep dasar pada OOP seperti Covariance, Contravariance dan Invariant. Jika belum memahami tentang hal tersebut silahkan membaca posting sebelumnya di link berikut.

Covariance dan Contravariane

Selain itu silahkan membaca posting lainnya tentang class invariant pada link berikut.

Class Invariant

Perlunya memahami konsep-konsep dasar tersebut dapat mempermudah reader dalam memahami aturan-aturan yang perlu dipatuhi pada LSP. Sederhananya pada prinsip ini adalah tentang mensubtitusikan/menggantikan Obejct sejenis. Sebagai contoh.

// super type
class Machine {}

// sub type mini machine
class MiniMachine extends Machine {}

// sub type large machine
class LargeMachine extends Machine {}

Jika melihat cuplikan code tersebut akan mengerti apa yang dimaksud mensubtitusi. Dimana pada cuplikan code tersebut terdapat tiga buah class, dimana dua diantaranya adalah subtype. Jika kedua subtype tersebut diimplementasikan akan seperti ini

const mini = new MiniMachine
const large = new LargeMachine

function dispatch(machine: Machine) {}

Pada function dispatch terdapat param machine yang bertype Machine. Karena param machine bertipe Machine maka dua instance yang dibuat bisa saling menggantikan sebagai argument pada param machine di fungsi dispatch karena kedua instance tersebut merupakan turunan dari class Machine.

Namun LSP tidak sesederhana itu. Ada aturan-aturan yang perlu dipatuhi. Hal tersebut diperlukan guna mejaga ketahanan dari program yang dibuat.

Aturan-aturan LSP

Berikut ini adalah pejelasan tentang aturan-aturan yang perlu dipatuhi.

Signature Rules

  • Convariance dimana keluar dari method atau function harus memiliki degree yang lebih kecil dari suppertype-nya.

  • Contravariance dimana nilai argument harus memiliki degree lebih besar dari pada supertype-nya.

  • Jumlah throw exception pada logika method bisa dikurangi, tapi tidak boleh ditambah.

Untuk pembahasan tentang Covariance dan Contravariance bisa membaca artikle tentang ini terlebih dahulu pada link berikut. Aturan lainnya bahwa jumlah Throw Exception pada method boleh dikurangi tapi tidak boleh ditambah. Karena akan mengakibatkan unpredictable throw exception yang mengakibatkan system break.

Property Rules

  • Class invariant

Dimana class/object selalu terjaga dan tergaransi selama object class tersebut hidup. Untuk penjelasan detail tentang class invariant bisa membaca pada post sebelumnya di link berikut

Class Invariant

  • History constraint

Dimana property yang seharusnya immutable tetap terjaga atau tidak boleh berubah. Sebagai contoh.

class VendingMachine {
    protected maxItem: number = 0;

    constructor(maxItem: number) {
       this.maxItem = maxItem;
    }
}

Pada class VendingMachine terdapat property maxItem pada code di atas. Property ini hanya boleh di-set nilai sekali ketika pembuatan object. Karena pada dasarnya maxItem adalah asumsi bahwa ketika pembuatan machine, maksimal barang yang bisa ditampung sesuai dengan desain dari mechine itu sendiri dan bernilai constant selama mesin berjalan. Sehingga kecil kemungkinan jika ada perubahan nilai maxItem tersebut.

  • Preconditions boleh dipertahankan tapi tidak boleh dikuatkan dari method di class parent-nya.

Dimana terdapat conditional flow yang mungkin ada pada head dari sebuah method public pada supertype, atau bisa kita syarat yang harus dipenuhi sebelum masuk ke section logic dari method tersebut. Misal seperti code di bawah ini.

class VendingMachine {    
    protected maxItem: number = 10;

    doSomething(min: number) {

      // pre condition
      if (min < 2 || min >= this.maxItem) {
         throw new Error("Invalid min item")
      }

      // main logic method

      // post condition
    }
}

class MiniMachine extends VendingMachine {

    doSomething(min: number) {
        // dikuatkan, dilarang menguatkan
        // ---
      // if (min < 3 || min >= this.maxItem) {
      //   throw new Error("Invalid min item")
      // }

      // dilemahkan, diperbolehkan
        if (min < 1 || min >= this.maxItem) {
         throw new Error("Invalid min item")
      }
    }

}

Pada cuplikan code tersebut, pada method doSomething terdapat control flow dimana nilai min tidak boleh kurang dari 2. Maka pada method di subtype-nya, nilai 2 tersebut tidak boleh dikuatkan misalkan menjadi 3. Hal tersebut bisa mengakibatkan program break/berhenti jika terdapat program lain menggunakan/mengeksekusi method tersebut. Kecuali jika batas tersebut dilemahkan, misal jadi batasnya 1 item. Hal tersebut tidak akan bermasalah, karena secara asumsinya ketika pertama kali class tersebut didesain, batasan minimal 2 item. Jadi semua client yang menggunkan class tersebut masih mempertahankan bahwa syarat vending machine minimal 2 item. Ketika nilai diganti jadi 1, maka program tidak akan break karena masih memenuhi syarat.

Jadi yang dimaksud pada pre-condition adalah, jangan membuat syarat baru pada head method yang akan membuat program break.

  • Postconditions boleh dipertahankan tapi tidak boleh dilemahkan dari method di class parent-nya

Post condition kebalikan dari pre condition. Post condition ini berada pada foot method atau setelah logic utama dari method dijalankan. Dimana syarat dari LSP adalah tidak boleh dilemahkan.

class VendingMachine {    
    protected maxItem: number = 10;

    doSomething(min: number) {

      // pre condition

      // main logic method
      const sum = min - 3;

      // post condition
      if (sum < 5) {
         throw new Error("Value is too small")
      }
    }
}

class MiniMachine extends VendingMachine {
   doSomething(min: number) {

       // Dilemahkan, tidak diperbolehkan
       // ---
       // if (sum < 6) {
       //  throw new Error("Invalid result")
       // }

       // Dikuatkan, diperbolehkan
       // --

       if (sum < 3) {
           throw new Error("Value is too small")
       }
   }
}

Pada cuplikan code tersebut terlihat bahwa jika hasil sum kurang dari 5 maka tampilkan Error. Ketika sudah banyak client yang menjadi turunan dari class VendingMachine tersebut, berarti sudah mendefinisikan bahwa paling besar adalah nilai 5 sebagai batas. Jika postcondition pada subtype dilemahkan, misal menjadi sum < 6, maka ketika hasil perhitungan pada main logic bernilai 5, program yang seharusnya bisa berlanjut tapi berakhir break karena tidak memenuhi syarat.

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