Inheritance memodelkan hubungan “is-a”: mobil adalah kendaraan, kucing adalah hewan. Namun tidak semua hubungan antar bagian program berbentuk keturunan — mobil punya mesin, toko punya katalog, pesanan punya metode pengiriman. Hubungan “has-a” semacam ini adalah wilayah object composition: merakit sebuah object dari object-object lain, lalu membiarkan tiap bagian mengerjakan spesialisasinya sendiri. Rantai inheritance yang panjang cepat menjadi kaku karena satu perubahan di parent menjalar ke semua keturunannya, sedangkan komposisi bisa dibongkar-pasang per bagian. Pada chapter ini kita akan belajar pola dasar komposisi, pola delegasi, dan prinsip klasik yang menyertainya: utamakan komposisi di atas inheritance.

Hubungan has-a dan is-a #

Kedua hubungan ini mudah tertukar ketika mendesain class, padahal konsekuensinya berbeda jauh. Hubungan is-a diwujudkan dengan extends dan mengikat class anak pada seluruh perilaku parent; hubungan has-a diwujudkan dengan menyimpan object lain sebagai property, biasanya lewat constructor. Diagram berikut memperlihatkan hubungan has-a antara Mobil dan Mesin:

Diagram tersebut dibaca: object Mobil memegang object Mesin di dalamnya — Mesin menjadi salah satu bagian penyusun Mobil. Wujud kodenya adalah property bertipe Mesin yang diisi lewat constructor:

php
<?php

class Mesin
{
    public function __construct(private int $tenagaKuda)
    {
    }

    public function start(): string
    {
        return "Mesin {$this->tenagaKuda} PK menyala";
    }
}

class Mobil
{
    public function __construct(private Mesin $mesin)
    {
    }

    public function jalankan(): string
    {
        return $this->mesin->start() . " - mobil siap jalan";
    }
}

$mobil = new Mobil(new Mesin(120));
echo $mobil->jalankan() . "\n";

Output yang Dihasilkan:

text
Mesin 120 PK menyala - mobil siap jalan

Mari kita bedah kode di atas:

  1. class Mesin: class kecil yang hanya tahu caranya menyala — lengkap dengan tenagaKuda miliknya sendiri.
  2. private Mesin $mesin: property Mobil bertipe object Mesin; constructor promotion mengisinya begitu mobil dibuat.
  3. jalankan(): method Mobil tidak tahu cara menyalakan mesin; method ini cukup memanggil $this->mesin->start() lalu menambahkan konteksnya sendiri.
  4. new Mobil(new Mesin(120)): object Mesin dibuat lebih dulu, lalu disuntikkan ke constructor Mobil — kedua object terikat tanpa satu pun kata extends.

Perhatikan bahwa Mobil tidak mewarisi apa pun dari Mesin. Esensi “mobil punya mesin” dipertahankan apa adanya, dan ketika kelak ada class MesinListrik, Mobil tinggal menerima object dari class tersebut tanpa mengubah dirinya sendiri — syaratnya saja type declaration-nya diganti ke interface, yang akan kita lakukan sebentar lagi.

Pola Delegasi #

Bentuk paling sederhana dari komposisi bekerja adalah delegasi: method class luar hanya meneruskan panggilan ke object yang dikandungnya. Toko dan katalog di bawah ini memperlihatkan pola tersebut:

php
<?php

class KatalogProduk
{
    private array $produk = [];

    public function tambah(string $nama): void
    {
        $this->produk[] = $nama;
    }

    public function semua(): array
    {
        return $this->produk;
    }
}

class Toko
{
    public function __construct(private KatalogProduk $katalog)
    {
    }

    public function tambahProduk(string $nama): void
    {
        $this->katalog->tambah($nama);
    }

    public function daftarNamaProduk(): array
    {
        return $this->katalog->semua();
    }
}

$toko = new Toko(new KatalogProduk());
$toko->tambahProduk("Kopi Torabika");
$toko->tambahProduk("Teh Melati");

echo implode(", ", $toko->daftarNamaProduk()) . "\n";

Output yang Dihasilkan:

text
Kopi Torabika, Teh Melati

Method tambahProduk() dan daftarNamaProduk() milik Toko tidak berisi logika penyimpanan sama sekali — keduanya sekadar meneruskan permintaan ke tambah() dan semua() milik KatalogProduk. Pemanggil tetap berurusan dengan Toko sebagai pintu masuk, sementara urusan penyimpanan produk tetap terkunci rapi di satu class. Delegasi seperti ini menjaga tiap class tetap kecil: Toko bisa menambah tanggung jawab lain (kasir, promosi) tanpa method penyimpanan produk ikut membengkak.

Komposisi dan Interface di Atas Inheritance #

Prinsip klasik perancangan object menyarankan: favor composition over inheritance — utamakan komposisi daripada inheritance. Alasannya paling terasa ketika variasi perilaku mulai berlipat. Bayangkan pengiriman yang punya dua dimensi: kecepatan (standar dan kilat) dan jangkauan (domestik dan internasional). Dengan inheritance murni, kita harus menyiapkan empat subclass untuk empat kombinasi, dan setiap dimensi baru menggandakan jumlahnya. Dengan komposisi, cukup satu class Pesanan yang menerima strategi pengiriman dari luar:

php
<?php

interface MetodePengiriman
{
    public function kirim(string $paket): string;
}

class PengirimanStandar implements MetodePengiriman
{
    public function kirim(string $paket): string
    {
        return "Paket $paket dikirim standar (3-5 hari)";
    }
}

class PengirimanKilat implements MetodePengiriman
{
    public function kirim(string $paket): string
    {
        return "Paket $paket dikirim kilat (1 hari)";
    }
}

class Pesanan
{
    public function __construct(private MetodePengiriman $pengiriman)
    {
    }

    public function proses(string $paket): string
    {
        return $this->pengiriman->kirim($paket);
    }
}

$standar = new Pesanan(new PengirimanStandar());
$kilat = new Pesanan(new PengirimanKilat());

echo $standar->proses("Buku PHP") . "\n";
echo $kilat->proses("Laptop") . "\n";

Output yang Dihasilkan:

text
Paket Buku PHP dikirim standar (3-5 hari)
Paket Laptop dikirim kilat (1 hari)

Mari kita amati bagaimana ketiga class tersebut bekerja sama:

  1. MetodePengiriman: interface mendefinisikan kontrak “cara mengirim paket” tanpa memedulikan implementasinya — persis seperti yang kita pelajari pada chapter Interfaces.
  2. PengirimanStandar dan PengirimanKilat: dua implementasi yang berdiri sendiri; menambah PengirimanInternasional tidak menyentuh class yang sudah ada.
  3. Pesanan: class pemakai yang menyimpan strategi lewat constructor lalu mendelegasikan pemanggilan kirim().

Inheritance bukan musuh yang harus dihindari total — hubungan is-a yang sejati (misalnya PesananKorporat extends Pesanan) tetap sah. Yang dihindari adalah pemakaian inheritance semata untuk menumpuk kemampuan, karena kombinasi kemampuan bertumbuh eksponensial sementara komposisi merakitnya secara linier.

Komposisi dengan Closure #

Bagian yang dikomposisikan tidak harus selalu object dari class bernama — closure juga bisa memegang peran tersebut. Closure (yang kita bahas tuntas pada chapter Closures ) adalah function tanpa nama yang bisa disimpan seperti nilai, sehingga behavior bisa disuntikkan ke dalam object tanpa mendefinisikan class baru:

php
<?php

class AturanDiskon
{
    public function __construct(private Closure $kalkulasi)
    {
    }

    public function hitung(int $totalBelanja): int
    {
        return ($this->kalkulasi)($totalBelanja);
    }
}

$diskonBertingkat = new AturanDiskon(function (int $total): int {
    if ($total >= 500000) {
        return 100000;
    }
    if ($total >= 200000) {
        return 30000;
    }
    return 0;
});

foreach ([150000, 250000, 600000] as $total) {
    echo "Belanja Rp$total: diskon Rp" . $diskonBertingkat->hitung($total) . "\n";
}

Output yang Dihasilkan:

text
Belanja Rp150000: diskon Rp0
Belanja Rp250000: diskon Rp30000
Belanja Rp600000: diskon Rp100000

Class AturanDiskon tidak tahu apa pun tentang ambang belanja; class ini hanya tahu cara memanggil closure yang diterimanya lewat constructor. Ekspresi ($this->kalkulasi)($totalBelanja) adalah cara menginvokasi closure yang tersimpan di property — property dibungkus kurung agar PHP tahu yang dipanggil adalah nilai callable, bukan method. Berkat pola ini, aturan diskon bertingkat didefinisikan tepat di titik pembuatan object, dan aturan lain (diskon member, diskon hari raya) tinggal ditulis sebagai closure berbeda tanpa class tambahan.

Pratinjau Dependency Injection #

Jika Anda memperhatikan semua contoh di atas, ada satu pola yang selalu berulang: object tidak pernah membuat bagian-bagiannya sendiri di dalam constructor — bagian-bagian itu selalu dikirim dari luar. Pola pengiriman dependency dari luar ini disebut dependency injection, dan pola ini adalah pondasi arsitektur aplikasi modern: object yang menerima dependency-nya dari luar menjadi mudah diuji (implementasi asli bisa diganti test double) dan mudah dikonfigurasi ulang. Pada Section Arsitektur Aplikasi , kita akan mengembangkan pola ini menjadi container yang merakit seluruh object graph aplikasi secara otomatis.

Komposisi juga berjodoh dengan final dari chapter sebelumnya: class yang dikunci final tetap bisa diperluas maknanya ketika variasinya masuk lewat interface di constructor. Kombinasi ketiganya — interface sebagai kontrak, komposisi sebagai perakitan, final sebagai gembok — membentuk gaya perancangan yang stabil sekaligus lentur.

Ringkasan #

Object composition memodelkan hubungan has-a: object besar dirakit dari object-object kecil yang masing-masing berspesialisasi, dihubungkan lewat property bertipe object atau interface. Pola delegasi menjaga class luar tetap ramping dengan meneruskan panggilan ke bagian yang tepat, interface membuat bagian-bagian itu saling dipertukarkan, dan closure memungkinkan perilaku dikirim sebagai nilai. Dengan mengutamakan komposisi di atas inheritance, kode kita siap bertemu topik-topik lanjutan berikutnya — dimulai dari sesuatu yang mendasar: bagaimana PHP membandingkan dua object yang berbeda.

Referensi #