LSP: esensi, prinsip substitusi Barbara Liskov dalam pengembangan

Penulis: IT Sectr Diterbitkan: 2026-05-12 Waktu membaca: 9 mnt

LSP (Liskov Substitution Principle) — prinsip SOLID ketiga yang menentukan kondisi pewarisan yang benar dalam pemrograman berorientasi objek. Prinsip ini dirumuskan oleh Barbara Liskov pada tahun 1987 dan diformalkan sebagai: jika S adalah subtipe dari T, maka objek T dapat diganti dengan objek S tanpa mengubah properti program. Sebagaimana disebutkan dalam buku Robert Martin Clean Architecture (2017), prinsip substitusi mensyaratkan bahwa subclass tidak boleh melemahkan kontrak kelas dasar.

Poin utama

  • LSP — prinsip substitusi Liskov, prinsip SOLID ketiga tentang pewarisan yang benar
  • Subclass harus mempertahankan kontrak kelas dasar — prasyarat dan pascasyarat
  • Pelanggaran LSP muncul dalam masalah persegi dan persegi panjang serta exception yang dilemparkan
  • Komposisi sering lebih diutamakan daripada pewarisan untuk mematuhi LSP
  • Desain berdasarkan kontrak (Design by Contract) — cara formal untuk memeriksa LSP

Apa itu LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — prinsip substitusi yang dirumuskan oleh Barbara Liskov pada konferensi OOPSLA tahun 1987. Definisi formal: misalkan q(x) adalah properti yang dapat dibuktikan dari objek x bertipe T. Maka q(y) harus dapat dibuktikan untuk objek y bertipe S, di mana S adalah subtipe dari T. Sederhananya: objek subclass harus berperilaku sedemikian rupa sehingga kode yang bekerja dengan kelas dasar tetap berfungsi dengan benar dengan subclass.

Dalam praktiknya, LSP berarti bahwa subclass tidak boleh melanggar kontrak kelas dasar. Kontrak mencakup prasyarat (apa yang diperlukan untuk memanggil metode), pascasyarat (apa yang dijamin setelah pemanggilan), dan invarian (kondisi yang dipertahankan selama masa hidup objek). Subclass dapat memperkuat prasyarat atau memperlemah pascasyarat — inilah pelanggaran LSP.

Contoh klasik pelanggaran LSP — persegi yang mewarisi dari persegi panjang. Metode setWidth pada persegi panjang mengatur lebar, pada persegi — baik lebar maupun tinggi. Klien yang mengharapkan perilaku persegi panjang (perubahan satu sisi tidak mempengaruhi sisi lain) mendapatkan hasil yang tidak terduga. Persegi bukanlah subtipe yang benar dari persegi panjang.

Kondisi formal LSP

LSP menetapkan tiga kondisi untuk pewarisan yang benar: prasyarat subclass tidak boleh lebih kuat dari prasyarat kelas dasar (subclass tidak meminta lebih), pascasyarat subclass tidak boleh lebih lemah dari pascasyarat kelas dasar (subclass tidak menjamin kurang), invarian kelas dasar harus dipertahankan di subclass. Kondisi ini dikenal sebagai aturan desain berdasarkan kontrak oleh Bertrand Meyer.

Jika setidaknya satu kondisi dilanggar — kode yang menggunakan polimorfisme dapat mengalami kegagalan. Compiler tidak memeriksa kontrak semantik, hanya kontrak sintaksis. Oleh karena itu LSP adalah masalah disiplin arsitektur, bukan pengetikan statis.

Bagaimana prinsip substitusi Liskov bekerja

Mekanisme LSP didasarkan pada kompatibilitas perilaku tipe. Jika kelas S mewarisi dari kelas T, kode klien harus dapat menggunakan S di mana pun T diharapkan, tanpa mengubah perilakunya. Ini mencakup tidak hanya signature metode, tetapi juga semantiknya.

LSP tidak melarang subclass untuk menambahkan perilaku baru. Melanggar ekspektasi kode yang ditulis untuk kelas dasar adalah dilarang. Jika kelas dasar menjamin bahwa metode save tidak melempar exception, subclass tidak boleh melemparnya. Jika kelas dasar mengembalikan nilai non-negatif, subclass tidak boleh mengembalikan nilai negatif.

Dalam proyek nyata, LSP paling sering dilanggar dengan menambahkan logika kondisional dalam metode subclass: “jika kondisi — lempar exception”, “jika kondisi — kembalikan null”. Setiap “kejuatan” semacam itu merusak polimorfisme dan memaksa kode klien untuk memeriksa tipe objek sebelum pemanggilan — yang bertentangan dengan ide desain berorientasi objek.

Dalam proyek mobile, pelanggaran LSP yang tipikal terjadi saat membuat ViewModel dasar. Jika BaseViewModel menjamin bahwa metode onCleared membebaskan semua sumber daya, dan subclass menimpa metode ini sebagai kosong — kode apa pun yang mengandalkan pembebasan sumber daya melalui panggilan polimorfik onCleared akan bekerja tidak benar. LSP mensyaratkan bahwa subclass memanggil super.onCleared() atau melakukan pekerjaan yang sama sendiri. Komposisi melalui LifecycleObserver — alternatif yang mengecualikan pelanggaran LSP dalam manajemen siklus hidup.

Tanda-tanda pelanggaran LSP dalam kode

Indikator utama pelanggaran LSP meliputi: pemeriksaan tipe objek melalui instanceof atau is sebelum pemanggilan metode, implementasi metode kosong (stub), melempar exception NotImplementedError atau UnsupportedOperationException, mengembalikan null sebagai ganti nilai. Masing-masing pola ini menandakan bahwa subclass bukan subtipe yang benar.

Tanda umum lainnya — pewarisan dengan tujuan menggunakan kembali kode, bukan untuk memodelkan relasi “adalah” (is-a). Kelas Bird memiliki metode fly(). Kelas Penguin mewarisi dari Bird dan menimpa fly() sebagai kosong atau melempar exception. Ini adalah pelanggaran LSP: penguin bukan subtipe yang benar dari burung.

Dalam pengembangan mobile, LSP dilanggar saat membuat ViewHolder, Fragment, atau ViewController dasar dengan metode stub. Jika subclass tidak menggunakan setengah dari metode kelas dasar — pewarisan telah dipilih secara tidak benar. Komposisi atau pemisahan antarmuka menyelesaikan masalah dengan lebih tepat.

Tes LSP

Tes sederhana untuk memeriksa LSP: tulis unit test untuk kelas dasar yang memeriksa kontraknya (nilai kembali, exception, efek samping). Jalankan tes ini untuk setiap subclass. Jika tes gagal — LSP dilanggar. Pendekatan ini disebut “pengujian melalui kontrak kelas dasar”.

Dalam proyek Android, tes semacam itu berguna untuk ViewModel dan Repository. Jika BaseViewModel menjamin status Loading sebelum error, dan subclass melempar error tanpa Loading — tes akan mendeteksi pelanggaran LSP pada tahap CI.

Contoh LSP dalam pengembangan mobile

Mari kita lihat contoh Android dengan penanganan ClickListener. Pelanggaran LSP terjadi ketika implementasi dasar menjamin sesuatu dan subclass melanggarnya.

kotlin
// Kelas dasar dengan jaminan: onClick akan dipanggil
open class BaseClickListener {
    open fun onClick(view: View) {
        // pemrosesan dasar
    }
}

// Pelanggaran LSP: subclass menambahkan kondisi yang melempar exception
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Solusi yang benar: kontrak tidak dilanggar
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Contoh iOS dengan protokol DataSource menunjukkan pelanggaran LSP melalui pengembalian nil sebagai ganti data:

swift
// Protokol dengan kontrak: mengembalikan data atau error
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Pelanggaran LSP: mengembalikan nil tanpa error
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // array kosong sebagai ganti error
    }
}

// Kepatuhan LSP yang benar
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Aturan praktis: jika subclass tidak dapat memenuhi kontrak kelas dasar — ia seharusnya tidak menjadi subclass. Alternatif — ekstrak antarmuka dengan kontrak minimal dan implementasikannya di setiap tipe dengan caranya sendiri.

LSP dan pewarisan: kapan memilih komposisi

Komposisi lebih diutamakan daripada pewarisan dalam situasi di mana relasi “adalah” (is-a) tidak jelas atau bersyarat. Contoh klasik: Manager adalah Employee? Ya. Tapi apakah Square adalah Rectangle yang benar? LSP mengatakan “tidak”. Jika Anda ragu tentang kebenaran pewarisan — pilih komposisi.

Dalam pengembangan mobile, komposisi sering digunakan melalui dependency injection: alih-alih mewarisi perilaku dari kelas dasar, kelas menerimanya melalui konstruktor. ViewModel tidak mewarisi dari Repository, tetapi menerimanya sebagai dependensi. Ini mengecualikan pelanggaran LSP secara definisi — tidak ada pewarisan, tidak ada pelanggaran kontrak.

Tanda bahwa pewarisan harus diganti dengan komposisi: subclass tidak menggunakan sebagian metode kelas dasar, subclass menimpa metode dengan stub kosong, kode klien memeriksa tipe objek melalui instanceof. Dalam kasus ini, pewarisan telah dipilih secara tidak benar dan LSP dilanggar.

Solusi melalui antarmuka

Antarmuka memecahkan masalah LSP tanpa pewarisan: setiap tipe mengimplementasikan tepat metode yang dibutuhkannya. Alih-alih kelas dasar Bird dengan metode fly (di mana Penguin tidak terbang) — antarmuka Flyable, yang hanya diimplementasikan oleh burung yang terbang. Penguin mengimplementasikan Bird tanpa metode fly — LSP tidak dilanggar.

Dalam arsitektur Android, pendekatan ini diterapkan melalui antarmuka tersegregasi UseCase: alih-alih satu UseCase besar dengan metode getAll, getById, save, delete — antarmuka terpisah GetItemsUseCase, SaveItemUseCase. Klien hanya bergantung pada antarmuka yang diperlukan dan setiap kelas yang mengimplementasikan antarmuka tersebut benar dari sudut pandang LSP.

Pertanyaan yang sering diajukan

Apa perbedaan LSP dengan pewarisan biasa?

Pewarisan — mekanisme bahasa, LSP — aturan penggunaan yang benar dari mekanisme ini. Pewarisan menjamin kompatibilitas signature (sintaksis), LSP mensyaratkan kompatibilitas perilaku (semantik). Pewarisan tanpa LSP menghasilkan polimorfisme yang rusak saat runtime.

Apakah null di subclass selalu melanggar LSP?

Jika kelas dasar menjamin pengembalian non-null — ya. Jika kontrak mengizinkan null (nilai opsional) — tidak. LSP tidak melarang null, ia melarang pelemahan kontrak. Pelajari dokumentasi kelas dasar dan periksa apakah kontrak subclass kompatibel.

Bagaimana LSP diterapkan pada protokol di Swift?

Pada protokol LSP diterapkan sama seperti pada kelas. Implementasi protokol harus mematuhi kontrak semantik: jika protokol mendefinisikan metode sebagai non-throwing, implementasi tidak boleh melempar error. Swift tidak memeriksa ini pada tingkat kompilasi — tanggung jawab ada pada pengembang.

Apakah LSP dapat dilanggar saat menggunakan sealed class?

Sealed class di Kotlin — kasus khusus, karena hierarki tertutup dan diketahui oleh compiler. LSP berlaku lebih sedikit untuk sealed class, karena semua subtipe disebutkan secara eksplisit dalam ekspresi when. Kesalahan subclass sealed akan bersifat lokal, bukan kesalahan polimorfik tersembunyi.

Bagaimana cara menguji kepatuhan LSP dalam proyek?

Tulis tes parameterized untuk kelas dasar yang dijalankan untuk semua subclass-nya. Tes memeriksa kontrak perilaku utama: nilai kembali, exception, status. Jika tes gagal pada salah satu subclass — LSP dilanggar. Dalam CI, tes semacam itu mencegah regresi kode polimorfik.

Ringkasan

  • LSP (Liskov Substitution Principle) — prinsip substitusi, ketiga dalam SOLID, tentang kompatibilitas semantik pewarisan
  • Subclass harus mempertahankan kontrak kelas dasar: prasyarat, pascasyarat, dan invarian
  • Pemeriksaan instanceof dan penimpaan metode kosong — tanda utama pelanggaran LSP
  • Komposisi dan antarmuka memecahkan masalah LSP di mana pewarisan tidak tepat
  • Masalah persegi dan persegi panjang — contoh klasik ketidakcocokan subtipe
  • Tes kontrak untuk kelas dasar, dijalankan untuk semua subclass, mendeteksi pelanggaran LSP di CI
  • Sealed class di Kotlin mengurangi risiko LSP berkat hierarki tertutup yang diketahui compiler

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga