Selama Section ini kita memakai dua alat untuk menyusun class yang besar dari bagian yang lebih kecil: composition pada Chapter Composition — Mobil memiliki Mesin — dan inheritance pada Chapter Inheritance — Penguin mewarisi Burung. Keduanya menambah kemampuan, keduanya memakai ulang kode yang sudah ada, dan justru karena itu keduanya sering dipertukarkan secara keliru. Chapter terakhir OOP ini membandingkan keduanya secara berhadapan: kapan masing-masing adalah pilihan yang benar, bagaimana satu masalah yang sama dijawab dua desain, dan kerangka keputusan yang bisa dibawa ke project nyata. Di ujungnya, kita menutup Section dan menyiapkan bekal menuju Section Manajemen Memori.

Dua Hubungan: is-a dan has-a #

Composition menyatakan hubungan has-a: Mobil punya Mesin, RekeningBank punya daftar Transaksi, Layar punya Buffer. Inheritance menyatakan hubungan is-a: MobilBalap adalah Mobil, Penguin adalah Burung. Uji bahasanya sederhana dan berguna sebagai langkah pertama: bila kalimat “X adalah Y” terdengar natural dan benar dalam semua konteks, inheritance layak dipertimbangkan; bila yang terjadi adalah “X punya Y” atau “X menggunakan Y”, composition adalah jalurnya. Relasi is-a membawa janji tambahan yang sering dilupakan: setiap turunan berjanji bisa menggantikan class dasarnya di mana pun class dasar dipakai.

Uji bahasa hanyalah titik mulai, bukan vonis akhir. Contoh klasiknya: secara matematika, Persegi adalah PersegiPanjang — kalimat is-a terdengar sempurna. Tetapi mewarisi memaksa Persegi mematuhi kontrak PersegiPanjang, termasuk method setLebar() yang boleh dipanggil tanpa menyentuh tinggi — sesuatu yang mustahil bagi persegi yang kedua sisinya selalu sama. Relasi yang benar secara bahasa bisa keliru secara kontrak; ukuran akhirnya bukan kalimat, melainkan perilaku yang dijanjikan class dasar dan apakah setiap turunan mampu memenuhinya di semua konteks.

Tabel Perbandingan #

Tabel berikut merangkum perbedaan kedua alat pada aspek-aspek yang sudah kita buktikan sepanjang Section ini:

AspekInheritance (is-a)Composition (has-a)
Relasi yang dinyatakanTurunan menggantikan class dasarBagian menyatu di dalam class pemilik
Waktu pengikatanTerkunci sejak kompilasi; hierarki tidak bisa ditukar saat program berjalanBagian dipilih saat konstruksi, bahkan bisa ditukar saat berjalan bila disimpan lewat reference atau pointer
Jumlah hubunganSatu class dasar langsungBerapa pun bagian, dengan tipe yang bebas
Antarmuka yang dibawaSemua anggota public class dasar otomatis terbuka di turunanHanya method yang sengaja didelegasikan
Polymorphism runtimeInti inheritance: dynamic dispatch lewat virtual functionTidak otomatis; dicapai dengan menyimpan antarmuka sebagai bagian
Dampak perubahan class dasarMenjalar ke semua turunan (fragile base class)Terkurung di dalam class pemilik
Penggunaan ulang kodeMewarisi antarmuka dan isi sekaligus, kopling eratMeminjam lewat delegasi, kopling longgar
Konstruksi dan destruksiClass dasar dulu, kebalikannya saat destruksiMember mengikuti urutan deklarasi

Dua baris terakhir menghubungkan aturan yang sudah kita buktikan dengan jejak cetak: urutan konstruksi member pada Chapter Composition dan urutan konstruksi class dasar pada Chapter Inheritance. Tidak ada baris yang menyatakan salah satu alat lebih baik secara mutlak — setiap aspek adalah pertanyaan tentang kebutuhan desain yang sedang dihadapi.

Satu Masalah, Dua Desain #

Mari kita jawab satu masalah yang sama dengan dua desain: karakter burung beserta cara bergeraknya. Desain A meletakkan perilaku di hierarki inheritance; Desain B menyimpannya sebagai object Penggerak yang disusun ke dalam Burung. Kedua keluarga class sama-sama bernama Burung, sehingga keduanya kita pisahkan dengan namespace — materi Section Dasar Pemrograman yang kini berguna lagi:

cpp
#include <iostream>
#include <string>
#include <vector>

// ===== Desain A: inheritance (is-a) =====
namespace desain_inheritance {

class Burung {
public:
    virtual ~Burung() = default;

    virtual std::string nama() const {
        return "Burung";
    }
    virtual void gerak() const {          // default: semua burung terbang
        std::cout << "  terbang menembus awan\n";
    }
};

class Penguin : public Burung {
public:
    std::string nama() const override {
        return "Penguin";
    }
    void gerak() const override {         // wajib tahu asumsi dasar untuk mengoreksinya
        std::cout << "  berjalan di es (tidak bisa terbang)\n";
    }
};

} // namespace desain_inheritance

// ===== Desain B: composition (has-a) =====
namespace desain_komposisi {

class Penggerak {
public:
    virtual ~Penggerak() = default;
    virtual void bergerak() const = 0;    // kontrak perilaku yang berdiri sendiri
};

class Terbang : public Penggerak {
public:
    void bergerak() const override {
        std::cout << "  terbang menembus awan\n";
    }
};

class Meluncur : public Penggerak {
public:
    void bergerak() const override {
        std::cout << "  meluncur di atas es\n";
    }
};

class Burung {
private:
    std::string nama;
    const Penggerak& penggerak;           // has-a: perilaku disimpan sebagai bagian

public:
    Burung(const std::string& n, const Penggerak& p) : nama(n), penggerak(p) {}

    void tampil() const {
        std::cout << nama << ":\n";
        penggerak.bergerak();             // delegasi ke bagian
    }
};

} // namespace desain_komposisi

int main() {
    std::cout << "== Desain A: inheritance ==\n";
    desain_inheritance::Burung elang;
    desain_inheritance::Penguin penguin;
    std::vector<desain_inheritance::Burung*> kawanan = { &elang, &penguin };
    for (const desain_inheritance::Burung* b : kawanan) {
        std::cout << b->nama() << ":\n";
        b->gerak();
    }

    std::cout << "== Desain B: composition ==\n";
    desain_komposisi::Terbang terbang;
    desain_komposisi::Meluncur meluncur;
    desain_komposisi::Burung elangB("Elang", terbang);
    desain_komposisi::Burung pinguinB("Penguin", meluncur);
    elangB.tampil();
    pinguinB.tampil();
    return 0;
}

Output yang Dihasilkan:

text
== Desain A: inheritance ==
Burung:
  terbang menembus awan
Penguin:
  berjalan di es (tidak bisa terbang)
== Desain B: composition ==
Elang:
  terbang menembus awan
Penguin:
  meluncur di atas es

Mari kita bedah:

  1. Baris 6 dan 33 (namespace): dua keluarga class hidup berdampingan meski sama-sama bernama Burung; main() membedakannya lewat nama lengkap seperti desain_inheritance::Burung.
  2. Baris 15-17 (gerak() di class dasar): default “terbang” adalah asumsi yang ditanamkan di class dasar. Asumsi inilah perangkapnya: kesalahan tidak terlihat di class dasar, melainkan menunggu di class turunan yang tidak cocok.
  3. Baris 25-27 (override Penguin): Penguin hanya aman bila penulisnya tahu asumsi dasar dan meng-override-nya. Override yang isinya membatalkan makna default — “tidak bisa terbang” — adalah sinyal klasik bahwa relasi is-a sedang direnggangkan.
  4. Baris 38 (bergerak() const = 0): kontrak Penggerak ditulis sebagai pure virtual function dari Chapter Pure Virtual Functions — perilaku menjadi antarmuka yang berdiri sendiri.
  5. Baris 41-53 (Terbang dan Meluncur): setiap perilaku menjadi class kecil yang bisa dipakai siapa pun. Bila Pesawat atau Naga butuh terbang, keduanya menyusun object Terbang yang sama tanpa menyentuh hierarki burung.
  6. Baris 58 (const Penggerak& penggerak): inilah has-a — Burung menyimpan reference ke perilaku, tanpa menyalin dan tanpa memiliki. tampil() di baris 63-66 mendelegasikan pekerjaan lewat penggerak.bergerak().
  7. Baris 110-117 (Desain A di main()): koleksi pointer dasar dan dynamic dispatch dari Chapter Polymorphism bekerja seperti biasa — gerak() versi masing-masing dijalankan.
  8. Baris 119-125 (Desain B di main()): perilaku disiapkan dulu sebagai object, lalu setiap Burung dirakit dari nama dan Penggerak pilihannya. Penguin versi B tidak pernah punya method terbang yang harus dibatalkan.

Output kedua desain identik — perbedaannya ada di struktur yang tidak terlihat. Desain A mewariskan asumsi; kebenaran Penguin bergantung pada kedisiplinan meng-override, dan burung baru yang lupa mewarisi kemampuan terbang diam-diam. Desain B tidak punya janji yang bisa berbohong: Burung tidak pernah menawarkan method terbang, dan perilaku bisa beragam tanpa menyentuh class Burung. Bila member penggerak kelak disimpan lewat pointer yang bisa ditukar, perilaku bahkan berubah saat program berjalan — hal yang mustahil pada hierarki yang terkunci sejak kompilasi.

Kapan Memilih yang Mana #

Pilih inheritance ketika relasi is-a benar-benar terjaga: setiap turunan mampu menggantikan class dasar di semua konteks, koleksi polymorphic lewat pointer dasar memang dibutuhkan, atau antarmuka abstract class menjadi kontrak bagi seluruh keluarga. Pilih composition ketika yang dibutuhkan adalah layanan dan komponen: bagian beragam atau berubah, kopling longgar diinginkan, atau tidak ada substitusi yang dijanjikan. Kerangka praktisnya satu kalimat: bila ragu, pilih composition; naikkan ke inheritance hanya bila is-a terjaga di semua konteks dan polymorphism runtime memang dibutuhkan.

Sepanjang 27 chapter Section ini, kita bergerak dari pertanyaan paling dasar — apa itu class dan object — melalui constructor, destructor, access specifiers, dan encapsulation, lalu membuka babak kedua dengan composition, inheritance, virtual function, dan polymorphism, hingga tiba pada keputusan desain yang dibahas chapter ini. Semua aturannya punya bukti yang bisa dijalankan ulang: urutan konstruksi, urutan destruksi, dynamic dispatch, kontrak pure virtual, dan dua jebakan besar yang baru saja ditangani.

Dari sini, peta berikutnya sudah mulai tergambar. Alokasi memori dua kali menyelinap masuk: new dan delete pada Chapter Destructor dan Virtual Destructors, serta container pointer pada Chapter Polymorphism — dan keduanya meninggalkan pertanyaan yang sama: object hidup di stack atau heap, siapa pemiliknya, dan bagaimana menghapusnya dengan pasti. Section Manajemen Memori mengangkat pertanyaan-pertanyaan itu satu per satu, dimulai dari bagaimana C++ benar-benar menyimpan object, lalu membangun jawaban modernnya di atas pola RAII yang sudah kita kenalkan pada Chapter Destructor.

Rujukan resmi: Derived classes (cppreference) , RAII (cppreference) .