Menggunakan ulang kode kelas lain dapat dilakukan lewat dua hubungan yang berbeda, yaitu pewarisan dengan extends dan komposisi (composition) yang menitipkan pekerjaan kepada objek lain lewat field. Keduanya menghilangkan duplikasi, tetapi konsekuensi jangka panjangnya berbeda jauh. Topik ini menyandingkan keduanya pada satu kasus yang sama, memperlihatkan cara pewarisan patah karena detail di balik induknya, lalu merapikan rancangan yang sama dengan komposisi.
Dua cara memakai ulang kode#
Pewarisan menyatakan hubungan adalah (is-a), sedangkan komposisi menyatakan hubungan memiliki (has-a). Kelas Lingkaran extends Bentuk menjanjikan bahwa setiap lingkaran dapat dipakai di mana pun bentuk diminta, sehingga polimorfisme bekerja. Sebaliknya, kelas yang memegang ArrayList di dalam fieldnya tidak menjanjikan apa pun tentang tipe objek itu di luar metode yang dipilihnya sendiri. Pekerjaannya diserahkan kepada objek yang dimilikinya, teknik yang disebut delegasi (delegation).
Kompiler menerima Tas extends ArrayList<String> tanpa protes, meskipun membayangkan tas sebagai array terasa keliru. Ujian yang sering dipakai adalah membalik kalimatnya, yaitu bila kalimat tas adalah ArrayList tidak terdengar yakin, hubungan pewarisannya kemungkinan besar keliru pula, dan kebutuhan yang sesungguhnya biasanya sekadar memakai ulang beberapa metode. Ujian ini penting karena konsekuensi pewarisan tidak berhenti pada kode, melainkan mencakup seluruh API induk yang ikut diwarisi beserta detail implementasinya yang ikut bergantung.
Bagian berikut menguji kedua rancangan dengan kasus yang sama, yaitu tas yang mencatat berapa banyak barang yang masuk ke dalamnya. Kasus sederhana ini cukup untuk memperlihatkan perbedaan nasib keduanya ketika rincian di balik induknya bergerak.
Pewarisan yang rapuh karena detail induk#
Rancangan pertama mewarisi ArrayList<String> dan mengoverride add() untuk menambah pencacah. Pada pemanggilan add() langsung, pencacah naik, dan pengujian sederhana pun lolos. Masalah muncul ketika barang dimasukkan lewat addAll(), karena keputusan tentang apakah addAll() memanggil add() atau tidak sepenuhnya ada di implementasi ArrayList, bukan pada kontrak yang tercantum di dokumentasinya.
import java.util.ArrayList;
import java.util.List;
public class DemoTasRapuh {
public static void main(String[] args) {
Tas tas = new Tas();
tas.add("buku");
tas.addAll(List.of("pulpen", "penghapus"));
System.out.println("Isi tas : " + tas.size());
System.out.println("Terhitung : " + tas.jumlahMasuk());
}
}
class Tas extends ArrayList<String> {
private int jumlahMasuk;
@Override
public boolean add(String item) {
jumlahMasuk++;
return super.add(item);
}
int jumlahMasuk() {
return jumlahMasuk;
}
}Isi tas : 3
Terhitung : 1Hasilnya mengejutkan pada pembacaan pertama, yaitu tiga barang ada di dalam tas tetapi pencacah hanya menghitung satu. Implementasi ArrayList.addAll() pada JDK ini menyalin isi koleksi secara massal ke array internalnya, sehingga override add() tidak pernah dipanggil untuk dua barang terakhir. Kode yang lolos kompilasi dan lolos pengujian satu per satu pun diam-diam salah begitu dipakai lewat jalur lain.
Kondisi semacam ini dikenal sebagai masalah kelas dasar yang rapuh (fragile base class), yaitu kebenaran kelas turunan bergantung pada detail implementasi induk yang tidak dijanjikan API dan boleh berubah antarversi. Bahayanya berlaku ke dua arah, karena implementasi yang justru memanggil add() dari dalam addAll() membuat pencacah menghitung dua kali. Subclass yang memperbaiki dirinya dengan membaca kode sumber induk hari ini baru saja mengunci dirinya pada perilaku yang tidak dijanjikan siapa pun.
Menulis ulang dengan komposisi#
Rancangan kedua memindahkan relasi dari adalah menjadi memiliki. Kelas Tas tidak lagi mewarisi ArrayList, melainkan memegangnya di dalam field private dan membuka hanya operasi yang benar-benar diperlukan, yaitu tambah(), tambahSemua(), dan pembaca ukurannya. Pencacah ditempatkan di satu titik, yaitu tambah(), dan tambahSemua() mendelegasikan pekerjaannya ke situ.
import java.util.ArrayList;
import java.util.List;
public class DemoTasKomposisi {
public static void main(String[] args) {
Tas tas = new Tas();
tas.tambah("buku");
tas.tambahSemua(List.of("pulpen", "penghapus"));
System.out.println("Isi tas : " + tas.ukuran());
System.out.println("Terhitung : " + tas.jumlahMasuk());
}
}
class Tas {
private final List<String> isi = new ArrayList<>();
private int jumlahMasuk;
void tambah(String item) {
jumlahMasuk++;
isi.add(item);
}
void tambahSemua(List<String> item) {
for (String satu : item) {
tambah(satu);
}
}
int ukuran() {
return isi.size();
}
int jumlahMasuk() {
return jumlahMasuk;
}
}Isi tas : 3
Terhitung : 3Angka kini konsisten karena tidak ada jalur masuk yang melompati pencacah, dan kebenarannya tidak lagi bergantung pada bagaimana ArrayList mengimplementasikan apa pun. Perubahan internal ArrayList antarversi tidak menyentuh Tas sama sekali, karena yang dipakai hanya API publiknya yang stabil dan dijanjikan. Titik lemah berpindah dari kode orang lain ke kode sendiri yang sepenuhnya dikendalikan.
Ukuran API yang terekspos ikut menyusut. Versi pewarisan mewarisi remove(), clear(), dan set() yang tidak diminta siapa pun, sedangkan versi komposisi hanya menawarkan tiga metode yang dirancang sendiri. Permukaan yang kecil membuat kelas lebih mudah diuji dan membatasi cara kelas lain bergantung padanya. Harganya nyata pula, yaitu beberapa baris delegasi yang ditulis manual, biaya kecil dibandingkan risiko yang dihindarinya.
Kapan pewarisan masih pantas#
Perbandingan di atas tidak berarti pewarisan harus dihindari, melainkan bahwa harganya perlu dibayar secara sadar. Tabel berikut menyandingkan konsekuensi keduanya.
| Aspek | Pewarisan | Komposisi |
|---|---|---|
| Hubungan yang dinyatakan | adalah, turunan menggantikan induk | memiliki, pekerjaan didelegasikan |
| Ketergantungan | API publik sekaligus detail implementasi induk | API publik objek yang dimiliki |
| API yang terekspos | seluruh warisan induk | hanya metode yang dirancang sendiri |
| Polimorfisme | tersedia langsung | tidak otomatis |
| Risiko utama | kelas dasar yang rapuh | kode delegasi yang lebih banyak |
Pewarisan pantas dipakai ketika hubungan adalah benar-benar berlaku di semua situasi dan polimorfismenya memang dibutuhkan. Hierarki Bentuk, Lingkaran, dan Persegi pada modul ini adalah contohnya, karena setiap turunan dapat menggantikan induknya di mana pun bentuk diminta dan perbedaannya murni perilaku yang dioverride. Bagian bersamanya disimpan di induk, dan metode pemakai seperti totalLuas bekerja tanpa mengenal jenisnya.
Diagram berikut merangkum dua relasi itu pada kasus tas yang sama. Di situ pewarisan mewariskan seluruh API induk ke turunan, sedangkan komposisi hanya meminjam API yang dipanggil.
Kesalahan umum#
- Mewarisi kelas utilitas atau collections hanya demi memakai ulang metodenya. Hubungan adalah tidak terpenuhi, sedangkan seluruh API induk ikut terekspos. Simpan objeknya di dalam field, lalu buka hanya operasi yang dibutuhkan.
- Mengoverride metode induk yang ternyata dipakai induk secara internal. Perilaku metode lain ikut berubah secara tak terduga, sebagaimana
add()danaddAll()pada demo. Baca dokumentasi induk tentang pemakaian internal metodenya (self-use) sebelum mengoverride, atau pilih komposisi. - Menumpuk hierarki lebih dari dua tingkat tanpa kebutuhan. Perubahan di induk menjalar ke seluruh rantai, dan memahami satu kelas menuntut pembacaan semua leluhurnya. Jagalah hierarki dangkal, dan pisahkan variasi lewat komposisi.
- Menganggap satu sisi selalu benar. Komposisi tidak memberikan polimorfisme, dan pewarisan tidak dirancang untuk sekadar berbagi kode. Kembalikan keputusannya ke pertanyaan hubungan, yaitu adalah atau memiliki, lalu biarkan jawaban itu menentukan alatnya.
Ringkasan#
- Pewarisan menyatakan hubungan adalah dan mewarisi seluruh API induk, sedangkan komposisi menyatakan memiliki dan membuka hanya API pilihannya sendiri.
- Kebenaran kelas turunan pewarisan dapat bergantung pada detail implementasi induk yang tidak dijanjikan API, masalah yang dikenal sebagai kelas dasar yang rapuh.
- Komposisi dengan delegasi menempatkan kebenaran di satu titik yang dikendalikan sendiri, sehingga perubahan internal induk tidak menular.
- Pewarisan tetap pantas bila hubungan adalah benar di semua situasi dan polimorfismenya dibutuhkan.
Referensi#
- JLS SE 21 §8.1.4: Superclasses and Subclasses — Oracle
— Semantik
extendsyang mengikat turunan pada rancangan induknya. - ArrayList (Java SE 21 API) — Oracle
— Dokumentasi kelas induk pada demo, yang memasok
addAll()beserta detail internalnya. - The Java Tutorials: Interfaces and Inheritance — Oracle — Pelajaran resmi tentang pewarisan, polimorfisme, dan interface.