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:

text
Kopi Susu — Rp22000 [Resi KFR-8821]
Kopi Susu — Rp22000 [Resi KFR-8821] [Diskon 10%]
Kopi Susu — Rp22000 [Diskon 25%]

Mari kita bedah kedua jalurnya:

  1. Baris 5–14 (class PesananKafe): induk yang sederhana — data item dan total, plus label() dasar.
  2. Baris 16–25 (class PesananKargo): jalur inheritance. Constructor memanggil super, dan label() di baris 23 menimpa versi induk dengan menyambung super.label() — pola penimpaan chapter inheritance.
  3. Baris 28–36 (class PelacakKargo): bagian kecil jalur composition. Ia tidak tahu apa-apa tentang Pesanan; tugasnya satu, menghasilkan teks labelnya sendiri.
  4. Baris 38–46 (class LabelDiskon): bagian kedua dengan kontrak yang sama — punya method label(). Kontrak sesederhana ini yang membuat bagian bisa dipasang-lepas.
  5. Baris 48–59 (class Pesanan): perakit composition. Constructor menerima tambahan berupa array bagian, dan label() memetakan setiap bagian ke teksnya lalu menggabungkan semuanya.
  6. Baris 61 (new PesananKargo(...)): jalur inheritance menghasilkan label pertama — identik dengan versi composition untuk kasus satu tambahan.
  7. Baris 62–65 (new Pesanan(..., [ ... ])): jalur composition merakit dua bagian sekaligus tanpa subclass baru apa pun.
  8. Baris 70–71 (penukaran bagian): array tambahan diganti 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:

KemampuanJalur inheritanceJalur composition
Menambah label kemasanSubclass baru per kombinasi, constructor diulangSatu class LabelKemasan kecil, lalu dimasukkan ke array
Mengubah kemampuan pesanan yang sudah adaTidak bisa; identitas class terkunciGanti isi tambahan kapan saja
Membaca kodeMenelusuri rantai keturunanBaca daftar bagian di constructor
Menguji perilakuDikaitkan ke induknyaTiap 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:

PertimbanganPilih inheritancePilih composition
Hubungan antar konsep“adalah sebuah”: Siswa adalah Member“punya sebuah”: Pesanan punya pelacak
Perubahan kebutuhanJarang; hierarki stabilSering; bagian bisa ditukar
Kombinasi kemampuanSedikit; satu jalur keturunanBanyak; array bagian yang dirakit
Polimorfisme di kode pemakaiGratis lewat instanceof dan rantai prototypeDibangun lewat kontrak method seperti label()
PengujianMengikut sertakan induknyaTiap 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
javascript
// 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());