Kelima chapter sebelumnya melatih insting: pecah tanggung jawab, pisahkan urusan, atur arah dependensi. Di pertengahan 2000-an, Robert C. Martin merangkum insting serupa yang berulang-ulang muncul dalam desain berorientasi objek menjadi lima prinsip yang singkatannya jadi terkenal: SOLID — Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, dan Dependency Inversion. Kekuatan SOLID bukan pada hurufnya, melainkan pada cara ia memberi nama pada rasa tidak nyaman yang sudah kita temui: “class ini terlalu banyak urusan” adalah SRP, “menambah fitur berarti menyunting class lama” adalah OCP. Chapter ini membahas kelimanya dengan fokus praktik — masing-masing ringkas, dengan potongan kode C++ yang bisa langsung dikenali — lalu menutupnya dengan satu program demonstratif yang menghidupkan kelima prinsip sekaligus.

SRP: Satu Alasan untuk Berubah #

Single Responsibility Principle menyatakan: satu class punya satu alasan untuk berubah. Bukan “satu fungsi”, melainkan satu aktor yang menuntut perubahan:

cpp Satu class, tiga alasan berubah — pelanggaran SRP (sketsa)
class LaporanKomisi {
public:
    void hitung(const std::vector<Transaksi>& data);   // berubah saat aturan bisnis berubah
    void cetakKeKonsol();                              // berubah saat format tampilan berubah
    void kirimKeEmail(const std::string& tujuan);      // berubah saat kebijakan pengiriman berubah
};

Ketiga method melayani tiga aktor berbeda — analis bisnis, desainer tampilan, admin infrastruktur — dan setiap permintaan dari satu aktor memaksa kita menyunting class yang juga dipakai dua aktor lain. Obatnya persis pola yang sudah kita latih: pecah menjadi RekapPenjualan (data dan hitungan), PemformatLaporan (teks), dan PengirimLaporan (pengiriman). Perhatikan bahwa SRP bukan soal jumlah baris — class 300 baris yang satu aktor bisa sehat, class 40 baris yang melayani tiga aktor bisa sakit.

OCP: Terbuka untuk Diperluas, Tertutup untuk Diubah #

Open-Closed Principle: modul seharusnya boleh diperluas tanpa harus disunting. Pelanggaran klasiknya adalah rantai if-else yang memanjang setiap kali jenis baru datang:

cpp Setiap kanal baru menambah satu cabang — pelanggaran OCP (sketsa)
std::string serialisasi(const Barang& b, const std::string& format) {
    if (format == "csv")  { /* baris CSV */ }
    if (format == "json") { /* object JSON */ }
    if (format == "xml")  { /* elemen XML */ }     // dan terus bertambah...
    return {};
}

Setiap format baru berarti menyunting function yang sudah teruji — risiko regresi di tempat yang seharusnya tidak disentuh. Bentuk OCP yang kita pakai sepanjang section ini adalah polymorphism dan injection: definisikan antarmuka Serialisasi dengan method tulis(), buat SerialisasiCsv, SerialisasiJson sebagai class baru, dan function pemangil tidak pernah disunting lagi. Template punya jalur OCP keduanya pada level kompilasi — materi yang akan dibandingkan di chapter Composition over Inheritance.

LSP: Turunan yang Bisa Menggantikan Tanpa Kejutan #

Liskov Substitution Principle adalah kaidah bagi is-a yang sudah kita uji di Section OOP: setiap class turunan harus bisa menggantikan class dasarnya tanpa mengejutkan pemanggil. Bentuk pelanggarannya bukan error compiler, melainkan kontrak yang direnggangkan:

cpp Turunan yang memperketat syarat — pelanggaran LSP (sketsa)
class Burung {
public:
    virtual void terbang() { /* naik ke udara */ }
};

class Penguin : public Burung {
public:
    void terbang() override {
        throw std::logic_error("Penguin tidak bisa terbang");   // kejutan!
    }
};

Pemanggil yang sah menganggap setiap Burung* bisa terbang() — loop sederhana atas koleksi burung kini bisa berakhir exception tanpa ada yang salah di kode pemanggilnya. LSP menuntut turunan tidak memperketat syarat masukan, tidak memperlemah janji keluaran, dan tidak menambah efek samping yang tak dijanjikan class dasar. Di C++ kontrak itu tidak tertulis di sintaks — ia tertulis di dokumentasi dan di test; itulah sebabnya test untuk class dasar sebaiknya dijalankan ulang untuk semua turunannya.

ISP: Kontrak Sekecil yang Dipakai Pemanggil #

Interface Segregation Principle: jangan paksakan pemakai bergantung pada method yang tidak ia pakai. Antarmuka yang gemuk memaksa setiap class turunan mengimplementasikan — atau mengosongkan — method yang tidak relevan:

cpp Antarmuka gemuk — pelanggaran ISP (sketsa)
class MesinKantor {
public:
    virtual void cetak(const std::string&) = 0;
    virtual void pindai(const std::string&) = 0;
    virtual void faks(const std::string&) = 0;
};

class PencetakSederhana : public MesinKantor {
public:
    void cetak(const std::string&) override { /* bekerja */ }
    void pindai(const std::string&) override { throw std::logic_error("tidak ada pemindai"); }
    void faks(const std::string&) override { throw std::logic_error("tidak ada faks"); }
};

Obatnya membelah: Pencetak, Pemindai, PengirimFaks sebagai antarmuka kecil yang masing-masing punya satu pekerjaan, lalu MesinMultifungsi mengimplementasikan ketiganya. Sama seperti SRP, ISP bukan jumlah method — antarmuka dengan enam method yang semua pemakainya memakai keenamnya tetap sehat. Kaidah pengukurnya: setiap pemakai antarmuka harus butuh semua methodnya.

DIP: Bergantung pada Abstraksi, Bukan Detail #

Dependency Inversion Principle kita bedah habis pada chapter Dependency Management, jadi cukup kalimat penguatnya: modul tingkat tinggi dan modul detail sama-sama menunjuk ke antarmuka — kebijakan tidak tahu nama class konkret, dan detail konkret yang menyesuaikan kontrak. Prinsip ini adalah perekat empat prinsip lain: SRP menghasilkan class kecil, OCP membutuhkan jalur ekspansi, ISP menjaga kontrak ramping, dan DIP menyambung semuanya dari luar tanpa kopling ke detail.

Lima Prinsip dalam Satu Program #

Program laporan komisi berikut menampung kelima prinsip dalam satu file yang bisa dijalankan — komentar di setiap bagian menandai prinsip mana yang sedang bekerja:

cpp
// solid-laporan.cpp — lima prinsip SOLID dalam satu alur laporan komisi
// Kompilasi: g++ -std=c++23 -Wall -Wextra solid-laporan.cpp -o solid-laporan

#include <algorithm>
#include <format>
#include <print>
#include <string>
#include <vector>

// ===== SRP: satu class, satu urusan — RekapPenjualan hanya tahu data penjualan =====
struct Transaksi {
    std::string kasir;
    int nominal;
};

class RekapPenjualan {
public:
    void tambah(const std::string& kasir, int nominal) {
        data_.push_back({kasir, nominal});
    }

    int totalKasir(const std::string& kasir) const {
        int total = 0;
        for (const Transaksi& t : data_) {
            if (t.kasir == kasir) {
                total += t.nominal;
            }
        }
        return total;
    }

    int total() const {
        int total = 0;
        for (const Transaksi& t : data_) {
            total += t.nominal;
        }
        return total;
    }

    const std::vector<Transaksi>& data() const { return data_; }

private:
    std::vector<Transaksi> data_;
};

// ===== DIP: pemformat dan pengirim hanya bergantung pada abstraksi =====
class KanalKirim {
public:
    virtual ~KanalKirim() = default;
    virtual void kirim(const std::string& baris) = 0;   // ISP: kontrak sekecil mungkin
};

class KanalKonsol : public KanalKirim {
public:
    void kirim(const std::string& baris) override {
        std::println("[konsol] {}", baris);
    }
};

// ===== OCP: kanal baru ditambahkan TANPA menyentuh class yang sudah ada =====
class KanalPita : public KanalKirim {
public:
    void kirim(const std::string& baris) override {
        std::println("[pita ] {}", baris);
    }
};

class PemformatLaporan {   // SRP: hanya tahu menyusun teks, tidak mengirim apa pun
public:
    explicit PemformatLaporan(const RekapPenjualan& rekap) : rekap_(rekap) {}

    std::vector<std::string> baris() const {
        std::vector<std::string> hasil;
        hasil.push_back("=== LAPORAN KOMISI ===");
        for (const std::string& nama : namaKasir()) {
            hasil.push_back(std::format("{}: Rp{}", nama, rekap_.totalKasir(nama)));
        }
        hasil.push_back(std::format("Total: Rp{}", rekap_.total()));
        return hasil;
    }

private:
    std::vector<std::string> namaKasir() const {   // urutan kemunculan pertama
        std::vector<std::string> nama;
        for (const Transaksi& t : rekap_.data()) {
            if (std::find(nama.begin(), nama.end(), t.kasir) == nama.end()) {
                nama.push_back(t.kasir);
            }
        }
        return nama;
    }

    const RekapPenjualan& rekap_;
};

class PengirimLaporan {   // SRP: hanya tahu mengirim, tidak menyusun apa pun
public:
    explicit PengirimLaporan(KanalKirim& kanal) : kanal_(kanal) {}

    void kirim(const std::vector<std::string>& barisLaporan) {
        for (const std::string& b : barisLaporan) {
            kanal_.kirim(b);   // LSP: implementasi apa pun dijamin bekerja di sini
        }
    }

private:
    KanalKirim& kanal_;
};

int main() {
    RekapPenjualan rekap;
    rekap.tambah("Siti", 150000);
    rekap.tambah("Dewi", 90000);
    rekap.tambah("Siti", 60000);

    PemformatLaporan pemformat(rekap);
    auto laporan = pemformat.baris();

    KanalKonsol konsol;
    PengirimLaporan pengirimKonsol(konsol);
    pengirimKonsol.kirim(laporan);

    KanalPita pita;                    // OCP: modul baru, nol penyuntingan
    PengirimLaporan pengirimPita(pita);
    pengirimPita.kirim(laporan);
    return 0;
}

Output yang Dihasilkan:

text
[konsol] === LAPORAN KOMISI ===
[konsol] Siti: Rp210000
[konsol] Dewi: Rp90000
[konsol] Total: Rp300000
[pita ] === LAPORAN KOMISI ===
[pita ] Siti: Rp210000
[pita ] Dewi: Rp90000
[pita ] Total: Rp300000

Bacakan programnya sebagai rangkaian prinsip: RekapPenjualan, PemformatLaporan, dan PengirimLaporan masing-masing satu urusan (SRP) dan sambungannya lewat konstruktor. KanalKirim hanya punya satu method yang semuanya dipakai pengirim (ISP), dan PengirimLaporan mengoperasikan KanalKirim& tanpa tahu class konkret (DIP). KanalPita adalah perluasan baru yang tidak menyunting satu baris class lama (OCP), dan keberhasilannya di main() — kanal apa pun yang diberikan tetap mencetak seluruh baris laporan — adalah janji substitusi (LSP). Tidak ada satupun class yang saling menyebut nama class konkret lainnya.

Tabel Ringkas dan Batas Praktiknya #

PrinsipInti satu kalimatSinyal pelanggaran paling umum
SRPSatu class, satu alasan untuk berubahClass berisi hitungan + cetak + simpan file
OCPPerluasan dengan menambah kode, bukan menyunting yang lamaRantai if bertambah setiap jenis baru
LSPTurunan menggantikan dasar tanpa kejutanOverride yang melempar atau mengosongkan method
ISPPemakai tidak dipaksa bergantung pada method yang tak dipakaiTurunan yang mengimplementasikan method kosong
DIPKebijakan dan detail sama-sama menunjuk ke abstraksiClass tinggi membangun class detail sendiri

Dengan kelima prinsip ini, kita punya cara menilai struktur class yang sudah dibangun. Pertanyaan berikutnya tentang cara menyusun perilaku: kapan perilaku ditambahkan lewat pewarisan, dan kapan lewat komposisi — plus dua pola susunan yang paling sering menyelamatkan desain C++ sehari-hari.

Referensi: Abstract classes (cppreference) , Virtual functions (cppreference) , std::format (cppreference) .