Belajar OOP dengan Java - SOLID Principles
Series/Belajar OOP/Episode 14
Episode 14 of 23

Belajar OOP dengan Java - SOLID Principles

Menerapkan lima prinsip SOLID secara praktis dengan contoh Java: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, dan Dependency Inversion, ditutup dengan pola Dependency Injection via constructor dan composition root.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Sejauh ini kalian menguasai mekanisme OOP: class, inheritance, polymorphism, abstraction. Episode ini menaikkan level ke seni merancang: SOLID — lima prinsip yang dirumuskan Robert C. Martin dan disebarluaskan Michael Feathers. Jika empat pilar OOP adalah bahasa, SOLID adalah grammar-nya: aturan bagaimana menyusun class agar sistem tetap sehat saat bertumbuh.

Mengapa SOLID penting? Karena software yang ditulis tanpa prinsip ini akan terasa enak di minggu pertama, lalu menyakitkan di bulan ketiga. SOLID bukan teori akademis — setiap prinsipnya menjawab gejala nyata: class raksasa, modifikasi merembet, subclass yang menyalahi kontrak, dan dependency yang susah ditest.

S: Single Responsibility (SRP)

Satu Class, Satu Alasan Berubah

SRP: sebuah class seharusnya hanya punya satu alasan untuk berubah. Class yang menangani banyak tanggung jawab akan berubah karena banyak alasan — dan setiap perubahan berisiko memecah tanggung jawab lain.

JavaAnti-pattern: god class
public class Laporan {
    void bacaData() { /* membaca database */ }
    void formatHTML() { /* membentuk HTML */ }
    void kirimEmail() { /* mengirim email */ }
}

Tiga tanggung jawab (data, presentasi, pengiriman) → tiga alasan berubah → tiga arah bug. Pecah:

JavaSetiap class satu tanggung jawab
public class DataLaporan { /* membaca data */ }
public class FormatLaporan { /* membentuk HTML */ }
public class PengirimLaporan { /* mengirim email */ }

Tanda class melanggar SRP: metode deskripsinya butuh "dan" ("class ini menangani laporan dan email dan format"). SRP adalah akar dari class kecil yang mudah diuji.

O: Open/Closed (OCP)

Terbuka untuk Ekstensi, Tertutup untuk Modifikasi

OCP: kalian harus bisa menambah perilaku baru tanpa mengubah kode yang sudah ada dan bekerja. Cara melakukannya: polymorphism dan interface (episode 8-9).

JavaAnti-pattern: modifikasi per jenis
public double ongkos(String jenis) {
    if (jenis.equals("motor")) return 5000;
    else if (jenis.equals("mobil")) return 10000;
    return 0;
}

Setiap kendaraan baru = tambah else if = memodifikasi method yang sudah ada. Ubah menjadi berbasis interface:

JavaTerbuka untuk ekstensi
public interface Kendaraan {
    double ongkos();
}
 
public class Motor implements Kendaraan {
    public double ongkos() { return 5000; }
}
 
public class Mobil implements Kendaraan {
    public double ongkos() { return 10000; }
}

Menambah Truk implements Kendaraan tidak menyentuh kode lama sama sekali — terbuka untuk ekstensi, tertutup untuk modifikasi. OCP adalah kode yang "tumbuh dengan cara ditambahi, bukan diubah".

L: Liskov Substitution (LSP)

Subclass Harus Menggantikan Superclass-nya

LSP (Barbara Liskov): object subclass harus bisa menggantikan object superclass tanpa memecah program. Pemakai yang bergantung pada Kendaraan harus tetap bekerja walau diisi Motor, Mobil, atau tipe turunan apa pun.

Pelanggaran klasik:

JavaPelanggaran LSP
public class Burung {
    void terbang() { System.out.println("terbang"); }
}
 
public class Penguin extends Burung {
    @Override
    void terbang() {
        throw new UnsupportedOperationException("Penguin tidak bisa terbang");
    }
}

Pemakai menulis burung.terbang() dengan asumsi berhasil — lalu runtime meledak untuk penguin. LSP rusak karena relasi "is-a" dipaksakan (ingat episode 7). Perbaikan: jangan letakkan terbang() di superclass yang terlalu umum; pisahkan ke interface khusus:

JavaPerbaikan LSP
public interface Burung { void makan(); }
public interface BisaTerbang { void terbang(); }
 
public class Merpati implements Burung, BisaTerbang {
    public void makan() { }
    public void terbang() { }
}
 
public class Penguin implements Burung {
    public void makan() { }
}

Tes LSP tercepat: "Bisakah setiap pemakai superclass bekerja tanpa tahu jenisnya?" Jika jawabannya butuh instanceof, LSP dilanggar.

I: Interface Segregation (ISP)

Jangan Memaksa Implementasi yang Tak Perlu

ISP: pemakai tidak boleh dipaksa bergantung pada method yang tidak digunakannya. Interface gemuk memaksa implementor menulis method kosong atau melempar exception — tanda bahaya.

JavaAnti-pattern: interface gemuk
public interface Mesin {
    void cetak();
    void scan();
    void fax();
}

Kelas PrinterSederhana terpaksa mengimplementasikan scan dan fax yang tidak dimilikinya. Pecah per peran:

JavaInterface kecil per peran
public interface Pencetak { void cetak(); }
public interface Pemindai { void scan(); }
public interface MesinFax { void fax(); }
 
public class PrinterSederhana implements Pencetak {
    public void cetak() { }
}
 
public class MesinSerbaBisa implements Pencetak, Pemindai, MesinFax {
    public void cetak() { }
    public void scan() { }
    public void fax() { }
}

ISP membuat interface fokus: class yang membutuhkan Pencetak hanya bergantung pada cetak(), tidak ikut membawa scan/fax.

D: Dependency Inversion (DIP)

Bergantung pada Abstraksi, Bukan Detail

DIP: class tingkat tinggi seharusnya tidak bergantung pada detail tingkat rendah — keduanya harus bergantung pada abstraksi. Ini bedanya dengan sekadar "program to interface": DIP membalik arah dependency.

JavaAnti-pattern: bergantung pada detail
public class Layanan {
    private EmailSender email = new EmailSender();
 
    void notif() {
        email.kirim("pesan");
    }
}

Layanan sekarang menempel pada EmailSender konkret — mengganti ke SMS berarti mengubah Layanan. Balik dependensinya:

JavaDependency Inversion
public interface Pengirim {
    void kirim(String pesan);
}
 
public class Layanan {
    private final Pengirim pengirim;
 
    public Layanan(Pengirim pengirim) {
        this.pengirim = pengirim;
    }
 
    void notif() {
        pengirim.kirim("pesan");
    }
}

Layanan kini bergantung pada abstraksi (Pengirim), bukan detail. Detail (EmailSender, SmsSender) juga bergantung pada abstraksi yang sama. Arah dependency terbalik — DIP terpenuhi.

Dependency Injection

Constructor Injection

Cara mengisi dependency yang sudah dibalik adalah Dependency Injection (DI) — memberikan dependency dari luar, bukan membuat di dalam. Bentuk paling disukai: constructor injection (contoh Layanan di atas). Keuntungannya:

  • Dependency terlihat eksplisit di signature constructor.
  • Object lahir lengkap — tidak mungkin eksis tanpa dependency.
  • Mudah ditest: masukkan Pengirim palsu (kita lakukan di episode 16 dengan Mockito).

Composition Root

Pertanyaan yang muncul: kalau semua class menerima dependency, siapa yang membuat semuanya? Jawabannya: composition root — satu tempat paling atas aplikasi yang merangkai seluruh graph object.

JavaComposition root sederhana
public class Aplikasi {
    public static void main(String[] args) {
        Pengirim pengirim = new EmailSender();
        Layanan layanan = new Layanan(pengirim);
        layanan.notif();
    }
}

Prinsipnya: penggabungan object terjadi sekali di composition root; seluruh aplikasi di bawahnya hanya menerima dependency yang sudah jadi. Framework seperti Spring otomatis melakukan ini (dibahas di episode 20), tetapi memahami composition root manual tetap penting karena semua framework memakai konsep yang sama.

Tip

Kalau kalian hanya ingat satu prinsip, ingat DIP: bangun class yang menerima dependency lewat constructor, bergantung pada interface, dan rangkai semuanya di satu composition root. Ini sudah membawa kalian 80% menuju kode yang mudah dirawat dan ditest.

Penutup

Inti yang harus dibawa pulang:

  • S: satu class, satu alasan berubah.
  • O: terbuka untuk ekstensi via interface, tertutup untuk modifikasi.
  • L: subclass harus aman menggantikan superclass — jangan paksa "is-a".
  • I: interface kecil per peran, jangan paksa implementasi yang tak dipakai.
  • D: bergantung pada abstraksi; balik arah dependency.
  • DI: berikan dependency lewat constructor; rangkai di composition root.

Di episode 15 selanjutnya kita akan membahas design patterns — katalog Gang of Four: creational (Singleton, Factory, Builder), structural (Adapter, Decorator, Proxy, Facade), dan behavioral (Observer, Strategy, Template Method, Command). Sampai jumpa di episode 15!

Belajar OOP dengan Java - SOLID Principles | Belajar OOP