Layanan kirim kafe Budi berjalan baik: pesanan pelanggan bisa dibawa kurir, dan tiap pesanan kargo menampilkan resi lewat class PesananKargo yang mewarisi PesananKafe. Bulan berikutnya manajemen menambah kebijakan diskon — lalu ketegangan desain muncul. Pesanan kargo berdiskon: subclass baru? Pesanan biasa berdiskon: subclass lagi? Tiga kebijakan berikutnya? Kombinasi subclass bertambah seperti papan catur, dan setiap perubahan kecil harus disentuh di banyak tempat.
Situasi inilah yang membuat pemilih antara inheritance dan composition menjadi keputusan desain paling sering dibahas dalam OOP. Chapter penutup section ini menyelesaikan kebutuhan yang sama dengan kedua cara dalam satu berkas, membandingkan hasilnya, lalu merangkum pertimbangan memilih dalam satu tabel.
Kebutuhan yang Sama, Dua Jalur #
Kebutuhannya sengaja dibuat ringkas: sebuah pesanan menampilkan label() berupa nama item dan harga, dan beberapa pesanan membawa tambahan — pelacakan kargo dengan resi, label diskon, atau keduanya.
Jalur pertama memakai inheritance yang sudah kita kuasai: PesananKargo extends PesananKafe menambah resi dan menimpa label(). Jalur kedua memakai composition: Pesanan tidak mewarisi siapa-siapa, melainkan punya daftar bagian kecil — object PelacakKargo, object LabelDiskon — yang masing-masing tahu cara menampilkan labelnya sendiri. Class Pesanan hanya merakit hasil dari bagian-bagian itu.
// dua-pendekatan.js
// Kebutuhan sama — label pesanan dengan pelacakan dan diskon — dua cara.
// Cara 1: inheritance — PesananKargo adalah sebuah PesananKafe.
class PesananKafe {
constructor(item, total) {
this.item = item;
this.total = total;
}
label() {
return `${this.item} — Rp${this.total}`;
}
}
class PesananKargo extends PesananKafe {
constructor(item, total, resi) {
super(item, total);
this.resi = resi;
}
label() {
return `${super.label()} [Resi ${this.resi}]`;
}
}
// Cara 2: composition — Pesanan punya tambahan berupa object kecil.
class PelacakKargo {
constructor(resi) {
this.resi = resi;
}
label() {
return `[Resi ${this.resi}]`;
}
}
class LabelDiskon {
constructor(persen) {
this.persen = persen;
}
label() {
return `[Diskon ${this.persen}%]`;
}
}
class Pesanan {
constructor(item, total, tambahan = []) {
this.item = item;
this.total = total;
this.tambahan = tambahan;
}
label() {
const bagian = this.tambahan.map((t) => t.label());
return [`${this.item} — Rp${this.total}`, ...bagian].join(" ");
}
}
const kargo = new PesananKargo("Kopi Susu", 22000, "KFR-8821");
const racikan = new Pesanan("Kopi Susu", 22000, [
new PelacakKargo("KFR-8821"),
new LabelDiskon(10),
]);
console.log(kargo.label());
console.log(racikan.label());
racikan.tambahan = [new LabelDiskon(25)];
console.log(racikan.label());Output yang Dihasilkan:
Kopi Susu — Rp22000 [Resi KFR-8821]
Kopi Susu — Rp22000 [Resi KFR-8821] [Diskon 10%]
Kopi Susu — Rp22000 [Diskon 25%]Mari kita bedah kedua jalurnya:
- Baris 5–14 (
class PesananKafe): induk yang sederhana — data item dan total, pluslabel()dasar. - Baris 16–25 (
class PesananKargo): jalur inheritance. Constructor memanggilsuper, danlabel()di baris 23 menimpa versi induk dengan menyambungsuper.label()— pola penimpaan chapter inheritance. - Baris 28–36 (
class PelacakKargo): bagian kecil jalur composition. Ia tidak tahu apa-apa tentangPesanan; tugasnya satu, menghasilkan teks labelnya sendiri. - Baris 38–46 (
class LabelDiskon): bagian kedua dengan kontrak yang sama — punya methodlabel(). Kontrak sesederhana ini yang membuat bagian bisa dipasang-lepas. - Baris 48–59 (
class Pesanan): perakit composition. Constructor menerimatambahanberupa array bagian, danlabel()memetakan setiap bagian ke teksnya lalu menggabungkan semuanya. - Baris 61 (
new PesananKargo(...)): jalur inheritance menghasilkan label pertama — identik dengan versi composition untuk kasus satu tambahan. - Baris 62–65 (
new Pesanan(..., [ ... ])): jalur composition merakit dua bagian sekaligus tanpa subclass baru apa pun. - Baris 70–71 (penukaran bagian): array
tambahandiganti saat runtime — diskon 25% menggantikan isian lama — dan label ketiga langsung mengikuti tanpa membuat class baru.
Hasil yang Berbeda di Ujung #
Dua baris pertama keluaran menunjukkan keduanya setara untuk kebutuhan dasar. Baris ketiga adalah tempat jalur keduanya membelah: composition memungkinkan isi tambahan diganti kapan saja pada instance yang sama, karena bagian hanyalah data yang dirujuk — bukan identitas class yang terkunci sejak new.
Dengan inheritance, variasi setara memerlukan class tambahan (PesananKargoDiskon), dan instance yang sudah dibuat tidak bisa berganti sifat. Itulah kata kunci perbandingannya: inheritance mengunci hubungan saat class ditulis; composition menahan keputusan sampai saat object dipakai.
Uji Kebutuhan Berikutnya #
Ukuran yang lebih adil untuk menimbang kedua pendekatan: apa yang terjadi saat kebutuhan baru tiba. Katakanlah manajemen menambah label kemasan Fragile untuk pesanan tertentu.
Di jalur inheritance, kebutuhan itu tidak menemukan tempat yang wajar. PesananKargoDiskon sudah ada; menambahkan kemasan berarti menulis subclass lagi — PesananKargoDiskonFragile — dan mengulang semua constructor induknya. Jumlah class bertumbuh mengikuti hasil kali kombinasi kemampuan, bukan jumlah kemampuannya.
Di jalur composition, jawabannya satu class kecil baru dengan kontrak label() yang sama:
| Kemampuan | Jalur inheritance | Jalur composition |
|---|---|---|
| Menambah label kemasan | Subclass baru per kombinasi, constructor diulang | Satu class LabelKemasan kecil, lalu dimasukkan ke array |
| Mengubah kemampuan pesanan yang sudah ada | Tidak bisa; identitas class terkunci | Ganti isi tambahan kapan saja |
| Membaca kode | Menelusuri rantai keturunan | Baca daftar bagian di constructor |
| Menguji perilaku | Dikaitkan ke induknya | Tiap bagian diuji sendiri-sendiri |
Baris terakhir keluaran berkas tadi adalah bukti hidup kolom kedua: racikan kehilangan pelacaknya dan berganti diskon, semuanya tanpa satu pun class baru.
Kapan Mewarisi, Kapan Menyusun #
Tidak ada pemenang mutlak — keduanya punya wilayah yang jelas:
| Pertimbangan | Pilih inheritance | Pilih composition |
|---|---|---|
| Hubungan antar konsep | “adalah sebuah”: Siswa adalah Member | “punya sebuah”: Pesanan punya pelacak |
| Perubahan kebutuhan | Jarang; hierarki stabil | Sering; bagian bisa ditukar |
| Kombinasi kemampuan | Sedikit; satu jalur keturunan | Banyak; array bagian yang dirakit |
| Polimorfisme di kode pemakai | Gratis lewat instanceof dan rantai prototype | Dibangun lewat kontrak method seperti label() |
| Pengujian | Mengikut sertakan induknya | Tiap bagian kecil diuji terpisah |
Kaidah yang terbukti tahan lama: mulai dari composition, dan gunakan inheritance ketika hubungan “adalah sebuah” benar-benar berdiri — keturunan sungguh merupakan jenis khusus dari induknya di semua skenario, bukan hanya berbagi beberapa baris kode. Berbagi kode saja bukan alasan yang cukup; hubungan itulah yang menentukan.
Bila penilaiannya masih sulit, tiga pertanyaan pendek biasanya cukup untuk memutuskan. Pertama, coba bacakan hubungan keduanya sebagai kalimat: “X adalah sebuah Y” atau “X punya sebuah Y” — kalimat kedua hampir selalu menunjuk composition. Kedua, bayangkan kebutuhan baru datang bulan depan: pertumbuhan class mana yang lebih mampu diterima, satu subclass atau satu bagian kecil? Ketiga, periksa apakah perilaku perlu berubah saat program berjalan — pertukaran bagian hanya mungkin lewat composition. Pertanyaan ketiga inilah yang membuat Pesanan merakit tambahan sebagai array, bukan mewariskan PelacakKargo.
Sampai di sini, section Object-Oriented JavaScript tuntas menuntaskan satu benang merah: dari [[Prototype]] yang kita intip di chapter pertama, rantai pencarian, constructor function, class sebagai pembungkusnya, hingga keputusan desain penutup ini. Beberapa bagian — seperti super, #field, dan static — kembali muncul di section Functional Programming dan Asynchronous JavaScript dengan peran baru, jadi fondasi yang baru dibangun ini akan terus terpakai.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
dua-pendekatan.js
Unduh berkas// dua-pendekatan.js
// Kebutuhan sama — label pesanan dengan pelacakan dan diskon — dua cara.
// Cara 1: inheritance — PesananKargo adalah sebuah PesananKafe.
class PesananKafe {
constructor(item, total) {
this.item = item;
this.total = total;
}
label() {
return `${this.item} — Rp${this.total}`;
}
}
class PesananKargo extends PesananKafe {
constructor(item, total, resi) {
super(item, total);
this.resi = resi;
}
label() {
return `${super.label()} [Resi ${this.resi}]`;
}
}
// Cara 2: composition — Pesanan punya tambahan berupa object kecil.
class PelacakKargo {
constructor(resi) {
this.resi = resi;
}
label() {
return `[Resi ${this.resi}]`;
}
}
class LabelDiskon {
constructor(persen) {
this.persen = persen;
}
label() {
return `[Diskon ${this.persen}%]`;
}
}
class Pesanan {
constructor(item, total, tambahan = []) {
this.item = item;
this.total = total;
this.tambahan = tambahan;
}
label() {
const bagian = this.tambahan.map((t) => t.label());
return [`${this.item} — Rp${this.total}`, ...bagian].join(" ");
}
}
const kargo = new PesananKargo("Kopi Susu", 22000, "KFR-8821");
const racikan = new Pesanan("Kopi Susu", 22000, [
new PelacakKargo("KFR-8821"),
new LabelDiskon(10),
]);
console.log(kargo.label());
console.log(racikan.label());
racikan.tambahan = [new LabelDiskon(25)];
console.log(racikan.label());