Pewarisan sering menjadi jawaban pertama kali kita ingin memakai ulang kode, padahal extends adalah relasi paling kaku dalam OOP: subclass terikat permanen pada parent dan mewarisi seluruh permukaan API-nya. Ujian klasiknya sederhana — apakah hubungan tersebut benar-benar is-a (adalah) atau sekadar has-a (memiliki)? Mobil memiliki mesin, bukan merupakan mesin; membuat Mobil extends Mesin semacam itu terasa keliru sejak baris pertama, tetapi variasinya muncul di banyak kode nyata dengan wujud yang lebih halus. Pada Chapter ini kita bedah gejala pewarisan yang terpasang pada relasi keliru, lalu membangun ulang contoh kendaraan itu dengan komposisi dan delegasi method.

Ujian is-a Sebelum extends #

Sebelum menulis extends, uji kalimatnya: “subclass adalah parent”. Lingkaran adalah Bentuk terdengar benar, sehingga hierarki bangun datar pada Chapter Abstract Classes sah. Sebaliknya “Mobil adalah MesinBensin” terdengar aneh karena mobil terdiri atas banyak komponen; mesin hanya salah satunya. Ketika hubungan yang sebenarnya has-a dipaksakan menjadi is-a, tiga gejala biasanya mengikuti:

  • Semantik melenceng: setiap kemampuan mesin (atur RPM, jenis bahan bakar) ikut menjadi kemampuan mobil, meski tidak relevan.
  • Slot pewarisan terpakai: Java hanya mengizinkan satu extends; slot yang dipakai Mesin tidak bisa dipakai class lain yang lebih layak.
  • Perilaku tertular: method publik parent bocor menjadi API subclass tanpa disaring — perubahan kecil pada parent bisa mengubah perilaku semua subclass.

Gejala pada Pewarisan Mobil dan Mesin #

Program berikut memakai pewarisan dan tetap berjalan — compiler tidak menolaknya. Kerusakannya ada di struktur, dan struktur itulah yang membuat kode sulit tumbuh:

class MesinBensin {
    public void nyalakan() {
        System.out.println("Mesin bensin menyala, idle 800 rpm");
    }

    public int rpmMaksimum() {
        return 6000;
    }
}

class MesinListrik {
    public void nyalakan() {
        System.out.println("Mesin listrik siap, senyap");
    }

    public int dayaKwh() {
        return 60;
    }
}

// Versi pewarisan yang keliru: MobilBensin "adalah" MesinBensin.
// Satu-satunya slot extends terpakai untuk mesin.
class MobilBensin extends MesinBensin {
    private final String pemilik;

    public MobilBensin(String pemilik) {
        this.pemilik = pemilik;
    }

    public void berangkat() {
        System.out.print(pemilik + ": ");
        nyalakan(); // method warisan dari MesinBensin
        System.out.println("  rpm maksimum: " + rpmMaksimum());
    }
}

// MobilListrik juga ingin jadi Mobil, tetapi slot extends sudah
// dipakai MesinListrik — perilaku berangkat() terpaksa ditulis ulang.
class MobilListrik extends MesinListrik {
    private final String pemilik;

    public MobilListrik(String pemilik) {
        this.pemilik = pemilik;
    }

    public void berangkat() {
        System.out.print(pemilik + ": ");
        nyalakan();
        System.out.println("  daya: " + dayaKwh() + " kWh");
    }
}

public class MobilPewarisan {
    public static void main(String[] args) {
        new MobilBensin("Budi").berangkat();
        new MobilListrik("Siti").berangkat();
    }
}

Output yang Dihasilkan:

text
Budi: Mesin bensin menyala, idle 800 rpm
  rpm maksimum: 6000
Siti: Mesin listrik siap, senyap
  daya: 60 kWh

Mari kita bedah gejalanya satu per satu:

  1. Baris 21 (class MobilBensin extends MesinBensin): hubungan is-a dideklarasikan untuk relasi yang sesungguhnya has-a. Method rpmMaksimum() kini menjadi bagian dari API mobil, padahal pemanggil tidak perlu mengetahui detail mesin.
  2. Baris 39 (class MobilListrik extends MesinListrik): kita ingin satu class Mobil yang dipakai bersama, tetapi slot extends sudah dipakai mesin. Java tidak mengizinkan MobilListrik extends MesinListrik, Mobil.
  3. Dua method berangkat(): logika yang sama (sapa pemilik, nyalakan mesin) ditulis ulang di kedua class. Setiap variasi mesin baru — hibrida, hidrogen — memaksa class mobil baru plus duplikasi baru.
  4. new MobilBensin(...) di main: objek mobil tidak bisa berganti mesin setelah dibuat; satu mobil terpatri pada satu jenis mesin selama hidupnya.

Komposisi dan Delegasi Method #

Komposisi membalik arahnya: Mobil menyimpan objek Mesin sebagai field, lalu method Mobil mendelegasikan pekerjaan ke field tersebut. Mesin kita modelkan sebagai interface agar variasi mesin bebas bertambah tanpa menyentuh class Mobil:

interface Mesin {
    void nyalakan();

    String jenis();
}

class MesinBensin implements Mesin {
    @Override
    public void nyalakan() {
        System.out.println("Mesin bensin menyala, idle 800 rpm");
    }

    @Override
    public String jenis() {
        return "bensin";
    }
}

class MesinListrik implements Mesin {
    @Override
    public void nyalakan() {
        System.out.println("Mesin listrik siap, senyap");
    }

    @Override
    public String jenis() {
        return "listrik";
    }
}

// Versi komposisi: Mobil MEMILIKI (has-a) Mesin, bukan MERUPAKAN (is-a) Mesin.
class Mobil {
    private final String pemilik;
    private Mesin mesin;

    public Mobil(String pemilik, Mesin mesin) {
        this.pemilik = pemilik;
        this.mesin = mesin;
    }

    // Delegasi: method Mobil menyerahkan pekerjaan ke objek mesin.
    public void berangkat() {
        System.out.print(pemilik + ": ");
        mesin.nyalakan();
    }

    // Keuntungan komposisi: mesin bisa dipasang ulang saat runtime.
    public void pasangMesin(Mesin mesinBaru) {
        this.mesin = mesinBaru;
        System.out.println(pemilik + " memasang mesin " + mesinBaru.jenis());
    }
}

public class MobilKomposisi {
    public static void main(String[] args) {
        Mobil mobilBudi = new Mobil("Budi", new MesinBensin());
        mobilBudi.berangkat();

        mobilBudi.pasangMesin(new MesinListrik());
        mobilBudi.berangkat();
    }
}

Output yang Dihasilkan:

text
Budi: Mesin bensin menyala, idle 800 rpm
Budi memasang mesin listrik
Budi: Mesin listrik siap, senyap

Mari kita bedah perubahan kuncinya:

  1. Baris 1 (interface Mesin): kontrak mesin dipisah dari implementasinya. Mobil bergantung pada kontrak, bukan pada satu jenis mesin — pola program to interface yang akan dirangkum di Chapter Prinsip Desain OOP.
  2. private Mesin mesin;: relasi has-a kini eksplisit sebagai field. Karena bertipe Mesin (interface), field ini bisa menampung mesin jenis apa pun, bahkan yang belum ditulis saat class Mobil dikompilasi.
  3. public void berangkat(): body-nya tetap pendek lewat delegasi — mesin.nyalakan(). Method mobil tidak perlu tahu rpm, kWh, atau detail lain; tanggung jawab tetap di tempatnya.
  4. pasangMesin(Mesin mesinBaru): objek yang sama berganti mesin pada runtime tanpa membuat class baru. Pada output, mobil Budi sempat memakai mesin bensin lalu lanjut dengan mesin listrik — hal yang mustahil pada versi pewarisan.

Kapan Pewarisan Tetap Tepat #

Komposisi bukan pengganti mutlak pewarisan. Hierarki bangun datar (Lingkaran, Persegi extends Bentuk) tetap tepat karena setiap subclass memang bentuk dengan rumus sendiri, dan kita memanfaatkan polimorfisme lewat tipe parent. Pewarisan juga lazim pada framework yang memang merancang parent sebagai titik perluasan. Patokannya: gunakan extends saat relasi is-a jelas dan Anda ingin meng-override perilaku parent; gunakan komposisi saat hanya butuh memakai kemampuan objek lain.

Relasi has-a yang baru kita bangun sebenarnya punya variasi lagi: mesin melekat erat pada mobil, tetapi pemain tidak melekat erat pada tim, dan peminjaman buku hanyalah hubungan sementara. Chapter Association, Aggregation, dan Composition memetakan ketiga tingkat kelekatan tersebut lengkap dengan implementasi Java-nya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

MobilKomposisi.java

Unduh berkas
java
interface Mesin {
    void nyalakan();

    String jenis();
}

class MesinBensin implements Mesin {
    @Override
    public void nyalakan() {
        System.out.println("Mesin bensin menyala, idle 800 rpm");
    }

    @Override
    public String jenis() {
        return "bensin";
    }
}

class MesinListrik implements Mesin {
    @Override
    public void nyalakan() {
        System.out.println("Mesin listrik siap, senyap");
    }

    @Override
    public String jenis() {
        return "listrik";
    }
}

// Versi komposisi: Mobil MEMILIKI (has-a) Mesin, bukan MERUPAKAN (is-a) Mesin.
class Mobil {
    private final String pemilik;
    private Mesin mesin;

    public Mobil(String pemilik, Mesin mesin) {
        this.pemilik = pemilik;
        this.mesin = mesin;
    }

    // Delegasi: method Mobil menyerahkan pekerjaan ke objek mesin.
    public void berangkat() {
        System.out.print(pemilik + ": ");
        mesin.nyalakan();
    }

    // Keuntungan komposisi: mesin bisa dipasang ulang saat runtime.
    public void pasangMesin(Mesin mesinBaru) {
        this.mesin = mesinBaru;
        System.out.println(pemilik + " memasang mesin " + mesinBaru.jenis());
    }
}

public class MobilKomposisi {
    public static void main(String[] args) {
        Mobil mobilBudi = new Mobil("Budi", new MesinBensin());
        mobilBudi.berangkat();

        mobilBudi.pasangMesin(new MesinListrik());
        mobilBudi.berangkat();
    }
}

MobilPewarisan.java

Unduh berkas
java
class MesinBensin {
    public void nyalakan() {
        System.out.println("Mesin bensin menyala, idle 800 rpm");
    }

    public int rpmMaksimum() {
        return 6000;
    }
}

class MesinListrik {
    public void nyalakan() {
        System.out.println("Mesin listrik siap, senyap");
    }

    public int dayaKwh() {
        return 60;
    }
}

// Versi pewarisan yang keliru: MobilBensin "adalah" MesinBensin.
// Satu-satunya slot extends terpakai untuk mesin.
class MobilBensin extends MesinBensin {
    private final String pemilik;

    public MobilBensin(String pemilik) {
        this.pemilik = pemilik;
    }

    public void berangkat() {
        System.out.print(pemilik + ": ");
        nyalakan(); // method warisan dari MesinBensin
        System.out.println("  rpm maksimum: " + rpmMaksimum());
    }
}

// MobilListrik juga ingin jadi Mobil, tetapi slot extends sudah
// dipakai MesinListrik — perilaku berangkat() terpaksa ditulis ulang.
class MobilListrik extends MesinListrik {
    private final String pemilik;

    public MobilListrik(String pemilik) {
        this.pemilik = pemilik;
    }

    public void berangkat() {
        System.out.print(pemilik + ": ");
        nyalakan();
        System.out.println("  daya: " + dayaKwh() + " kWh");
    }
}

public class MobilPewarisan {
    public static void main(String[] args) {
        new MobilBensin("Budi").berangkat();
        new MobilListrik("Siti").berangkat();
    }
}