Chapter penutup Section Pemrograman Berorientasi Objek sudah menetapkan kerangkanya: composition menyatakan has-a, inheritance menyatakan is-a, dan bila ragu — pilih composition. Prinsip ini bahkan punya nama baku, composition over inheritance, dan alasan teknisnya sudah Anda buktikan sendiri: hierarki terkunci sejak kompilasi, class dasar mewariskan asumsi ke semua turunannya (fragile base class), sedangkan bagian yang disusun tinggal ditukar. Chapter ini tidak mengulang perbandingan itu, melainkan memperdalam sisi praktisnya: bagaimana dua pola susunan paling sering dipakai di C++ modern — decorator dan strategy — mengeksekusi prinsip tersebut, termasuk jalur keduanya lewat template yang menghilangkan virtual dispatch sepenuhnya.

Kapan Inheritance Tetap Tepat #

Sebelum kita melatih kehati-hatian, adil pula mencatat: inheritance tidak dilarang, melainkan diberi syarat. Ia tepat ketika tiga hal bertemu sekaligus: relasi is-a benar di semua konteks (setiap turunan lulus uji substitusi LSP dari chapter lalu), program memang butuh koleksi polymorphic lewat pointer class dasar, dan antarmuka abstract class dipahami sebagai kontrak bersama seluruh keluarga. Contoh sahnya ada di kode chapter ini sendiri: Sensor dan StrategiKompresi adalah abstract class yang benar-benar dikonsumsi lewat pointer/reference dasar. Yang kita hindari adalah inheritance sebagai jalur cepat memakai ulang kode — Kalibrasi tidak mewarisi SensorKonstan sekadar ingin meminjam nilai_; ia membungkus sensor apa pun.

Decorator: Menambah Kemampuan dengan Membungkus #

Decorator menjawab pertanyaan yang sering muncul: bagaimana menambah kemampuan pada object — menggeser nilai, mencatat log, mengompres — tanpa membuat jumlah class membengkak sebagai hasil kali semua kemampuan di hierarki? Jawabannya: setiap kemampuan menjadi class kecil yang mengimplementasikan antarmuka yang sama dengan yang dibungkusnya dan memegang reference ke yang dibungkus. Lapisan bisa disusun bebas saat perakitan, seperti bawahan kabel:

cpp
// dekorasi-strategi.cpp — decorator dan strategy: dua rasa komposisi
// Kompilasi: g++ -std=c++23 -Wall -Wextra dekorasi-strategi.cpp -o dekorasi-strategi

#include <print>
#include <string>

// ===== Decorator: menambah kemampuan dengan membungkus, bukan mewarisi =====
class Sensor {
public:
    virtual ~Sensor() = default;
    virtual double baca() const = 0;
};

class SensorKonstan : public Sensor {        // sensor paling sederhana: nilai tetap
public:
    explicit SensorKonstan(double nilai) : nilai_(nilai) {}
    double baca() const override { return nilai_; }
private:
    double nilai_;
};

class DekoriSensor : public Sensor {         // decorator dasar: delegasi apa adanya
public:
    explicit DekoriSensor(const Sensor& bungkus) : bungkus_(bungkus) {}
    double baca() const override { return bungkus_.baca(); }
private:
    const Sensor& bungkus_;
};

class Kalibrasi : public DekoriSensor {      // lapisan 1: menggeser hasil bacaan
public:
    Kalibrasi(const Sensor& bungkus, double offset)
        : DekoriSensor(bungkus), offset_(offset) {}
    double baca() const override { return DekoriSensor::baca() + offset_; }
private:
    double offset_;
};

class Pencatat : public DekoriSensor {       // lapisan 2: mencatat setiap pembacaan
public:
    using DekoriSensor::DekoriSensor;
    double baca() const override {
        double nilai = DekoriSensor::baca();
        std::println("[pencatat] bacaan {:.1f}", nilai);
        return nilai;
    }
};

// ===== Strategy lewat antarmuka: kebijakan ditukar saat program berjalan =====
class StrategiKompresi {
public:
    virtual ~StrategiKompresi() = default;
    virtual std::string padatkan(const std::string& masukan) const = 0;
};

class RLE : public StrategiKompresi {        // run-length encoding: "aaabbb" -> "a3b3"
public:
    std::string padatkan(const std::string& masukan) const override {
        std::string hasil;
        for (std::size_t i = 0; i < masukan.size();) {
            std::size_t j = i;
            while (j < masukan.size() && masukan[j] == masukan[i]) {
                ++j;
            }
            hasil += masukan[i];
            hasil += std::to_string(j - i);
            i = j;
        }
        return hasil;
    }
};

class Identitas : public StrategiKompresi {  // kebijakan paling polos: tidak memampatkan
public:
    std::string padatkan(const std::string& masukan) const override { return masukan; }
};

class Kompresor {
public:
    explicit Kompresor(const StrategiKompresi& strategi) : strategi_(strategi) {}
    std::string olah(const std::string& masukan) const { return strategi_.padatkan(masukan); }
private:
    const StrategiKompresi& strategi_;
};

// ===== Strategy lewat template: kebijakan dipilih saat kompilasi =====
struct KebijakanRLE {
    std::string padatkan(const std::string& masukan) const {
        return RLE{}.padatkan(masukan);
    }
};

struct KebijakanIdentitas {
    std::string padatkan(const std::string& masukan) const { return masukan; }
};

template <typename Kebijakan>
class KompresorCepat {
public:
    std::string olah(const std::string& masukan) const {
        return kebijakan_.padatkan(masukan);
    }
private:
    Kebijakan kebijakan_;
};

int main() {
    // Decorator: bacaan mentah -> dikalibrasi -> dipantau
    SensorKonstan mentah(25.0);
    Kalibrasi dikalibrasi(mentah, -2.5);      // kompensasi sensor yang selalu kelebihan 2,5
    Pencatat dipantau(dikalibrasi);
    std::println("bacaan akhir: {:.1f}", dipantau.baca());

    // Strategy runtime: satu class Kompresor, kebijakan ditukar dari luar.
    // Kebijakan dibuat sebagai object bernama — member `strategi_` hanya
    // reference, sehingga object sumbernya harus hidup lebih lama darinya.
    Identitas identitas;
    RLE rle;
    Kompresor k1(identitas);
    std::println("identitas : {}", k1.olah("aaabbb"));
    Kompresor k2(rle);
    std::println("rle       : {}", k2.olah("aaabbb"));

    // Strategy compile-time: tipe Kebijakan menentukan perilaku
    KompresorCepat<KebijakanRLE> kc1;
    KompresorCepat<KebijakanIdentitas> kc2;
    std::println("templat   : {} dan {}", kc1.olah("aaabbb"), kc2.olah("aaabbb"));
    return 0;
}

Output yang Dihasilkan:

text
[pencatat] bacaan 22.5
bacaan akhir: 22.5
identitas : aaabbb
rle       : a3b3
templat   : a3b3 dan aaabbb

Bedah bagian decorator-nya:

  1. Baris 22-28 (DekoriSensor): decorator dasar. Ia adalah Sensor (mengimplementasikan antarmuka yang sama) sekaligus punya Sensor (menyimpan reference bungkus_). Dua hubungan ini berdampingan itulah ciri decorator — dan inilah satu-satunya tempat di chapter ini inheritance dipakai untuk kepentingan implementasi.
  2. Baris 30-37 (Kalibrasi): lapisan menambah perilaku dengan mendelegasikan dulu lalu menggeser: DekoriSensor::baca() + offset_. Perhatikan pemanggilannya eksplisit ke class dasar — bukan rekursi yang memanggil dirinya sendiri.
  3. Baris 39-47 (Pencatat): lapisan kedua menambah efek samping (mencatat) sebelum mengembalikan nilai. Baris 41 (using DekoriSensor::DekoriSensor;) mewariskan constructor class dasar agar Pencatat tidak perlu menulisnya ulang.
  4. Baris 107-112 (main()): perakitannya terbaca seperti kalimat — sensor konstan 25,0, dikalibrasi minus 2,5, dipantau. Output mengikuti urutan bungkus: [pencatat] bacaan 22.5 tercetak ketika Pencatat::baca() memanggil Kalibrasi::baca(), dan nilai 22,5 mengalir keluar.

Kekuatan pola ini terlihat saat kebutuhan berkembang: sensor kedua, lapisan pengali, atau urutan lapisan yang berbeda — semuanya kombinasi baru dari class yang sudah ada, tanpa class baru di hierarki. Bandingkan dengan jalur inheritance: SensorTerkalibrasiPencatat, SensorPencatatTerkalibrasi, SensorTerkalibrasi… jumlah class tumbuh sebagai hasil kali semua kemampuan.

Strategy: Antarmuka Runtime vs Template Kompilasi #

Strategy memilih algoritma dari luar class yang memakainya. Kode di atas menyajikan dua varian untuk satu masalah yang sama — mengompres teks — dan perbedaannya menyangkut waktu keputusan. Varian pertama (baris 50-84) memilih saat program berjalan: Kompresor menyimpan const StrategiKompresi&, dua object Kompresor k1 dan k2 berperilaku berbeda karena diberi kebijakan berbeda di constructor, dan pilihan itu boleh berubah kapan pun — cukup beri dia object kebijakan lain. Varian kedua (baris 87-105) memilih saat kompilasi: KompresorCepat<KebijakanRLE> dan KompresorCepat<KebijakanIdentitas> adalah dua tipe yang berbeda; kebijakannya struct biasa tanpa virtual function sama sekali, sehingga panggilan kebijakan_.padatkan() bisa di-inline compiler — jalur yang sama dengan template dan konsep dari Section Generic Programming.

Varian mana yang dipilih adalah keputusan desain dengan konsekuensi yang jelas:

AspekStrategy via antarmuka (virtual)Strategy via template policy
Waktu pemilihan kebijakanSaat program berjalanSaat kompilasi
Tipe container pemegangKompresor satu tipe untuk semua kebijakanKompresorCepat<K> — satu tipe per kebijakan
Biaya panggilanVirtual dispatch (indireksi)Nol — bisa di-inline
Ukuran binerSatu kopian class pemakaiSatu instantiasi per kebijakan yang dipakai
Ganti kebijakanRuntime — input, konfigurasi, stateHarus rekompilasi
Kebutuhan antarmuka bersamaAbstract classKonvensi method saja (atau concept)

Aturan pembilasannya: butuh satu tipe yang memegang kebijakan berbeda-beda, atau kebijakan ditentukan saat program berjalan? Antarmuka virtual. Semua kebijakan sudah pasti saat kompilasi dan jalur kodenya panas (dipanggil jutaan kali)? Template policy. Yang mahal adalah memilih salah satu karena kebiasaan — memakai virtual untuk sesuatu yang selalu diketahui saat kompilasi, atau template untuk sesuatu yang berubah tiap eksekusi.

Dua pola ini hanya penghuninya; pola-pola semacam ini di dunia nyata punya nama kolektif — design patterns — dan beberapa di antaranya begitu sering dipakai di C++ modern sehingga layak dikenali satu per satu, lengkap dengan kritiknya.

Referensi: Virtual functions (cppreference) , Templates (cppreference) .