Chapter Design Patterns ditutup dengan pertanyaan perakitan: object yang saling bergantung itu disambung dari mana? Jawabannya adalah teknik bernama dependency injection (DI): class tidak membangun object yang ia butuhkan, melainkan menerimanya dari luar — biasanya lewat parameter constructor. Teknik ini kelihatan kecil, tetapi ia yang mengubah layanan yang menempel pada database menjadi layanan yang bisa diuji dalam milidetik. Yang lebih baik lagi: DI di C++ tidak butuh framework sama sekali — constructor, abstract class, dan reference adalah seluruh peralatannya, semuanya sudah Anda kuasai. Chapter ini membuktikannya dengan satu sistem yang diuji dua kali: sekali dengan implementasi nyata, sekali dengan test double.
Constructor Injection: Dependensi Masuk dari Pintu Depan #
Pola dasarnya sudah kita padatkan di chapter Dependency Management: class pekerja mendeklarasikan apa yang ia butuhkan sebagai parameter constructor, dan pilihan konkret dibuat di tempat perakitan. Bandingkan dua wajah konstruksinya:
// TANPA injection: layanan mengunci pilihannya sendiri
class LayananPesanan {
public:
LayananPesanan() : repo_() {} // database nyata, selalu
private:
RepositoriDatabase repo_; // tipe konkret, tidak bisa diganti
};
// DENGAN constructor injection: layanan hanya menyatakan kebutuhannya
class LayananPesanan {
public:
explicit LayananPesanan(RepositoriPesanan& repo) : repo_(repo) {}
private:
RepositoriPesanan& repo_; // antarmuka, isinya dipilih dari luar
};Perbedaannya bukan sekadar gaya. Versi pertama membawa seluruh kebutuhannya di dalam badan — mengujinya berarti membawa database ikut jalan. Versi kedua menyatakan kebutuhan dalam bentuk antarmuka, dan siapa pun yang merakit — main(), factory, atau program test — yang mengisi. Antarmuka juga ditulis dari sisi pemakainya: RepositoriPesanan berisi persis dua method yang dibutuhkan layanan, bukan seluruh kemampuan database. Prinsip penyusunannya: kebutuhan kecil di antarmuka, pilihan besar di tempat perakitan.
Satu Sistem, Dua Perakitan: Nyata dan Palsu #
Program berikut menghidupkan seluruh alurnya: antarmuka RepositoriPesanan, implementasi nyata RepositoriMemori, test double RepositoriPalsu, dan LayananPesanan yang menerima yang mana pun tanpa berubah:
// di-pesanan.cpp — dependency injection: interface, implementasi nyata, dan test double
// Kompilasi: g++ -std=c++23 -Wall -Wextra di-pesanan.cpp -o di-pesanan
#include <print>
#include <string>
#include <vector>
// ===== Antarmuka: kontrak yang dibutuhkan LayananPesanan =====
class RepositoriPesanan {
public:
virtual ~RepositoriPesanan() = default;
virtual void simpan(const std::string& pelanggan, int total) = 0;
virtual int totalTerakhir(const std::string& pelanggan) const = 0;
};
// ===== Implementasi nyata: versi in-memory (pengganti database untuk demo) =====
class RepositoriMemori : public RepositoriPesanan {
public:
void simpan(const std::string& pelanggan, int total) override {
data_.push_back({pelanggan, total});
}
int totalTerakhir(const std::string& pelanggan) const override {
int hasil = -1; // -1: pelanggan belum pernah memesan
for (const Baris& b : data_) {
if (b.pelanggan == pelanggan) {
hasil = b.total;
}
}
return hasil;
}
private:
struct Baris {
std::string pelanggan;
int total;
};
std::vector<Baris> data_;
};
// ===== Test double: stub dengan jawaban yang dijadwalkan + catatan interaksi =====
class RepositoriPalsu : public RepositoriPesanan {
public:
void simpan(const std::string&, int) override { dipanggilSimpan_ = true; }
int totalTerakhir(const std::string&) const override { return jawaban_; }
void setJawaban(int jawaban) { jawaban_ = jawaban; }
bool simpanPernahDipanggil() const { return dipanggilSimpan_; }
private:
int jawaban_ = -1;
bool dipanggilSimpan_ = false;
};
// ===== Sistem yang diuji: hanya kenal antarmuka, dependensi masuk lewat constructor =====
class LayananPesanan {
public:
explicit LayananPesanan(RepositoriPesanan& repo) : repo_(repo) {}
int proses(const std::string& pelanggan, int total) {
int totalTerakhir = repo_.totalTerakhir(pelanggan);
int bayar = total;
if (totalTerakhir >= 0) {
bayar = total * 95 / 100; // diskon tetap 5% untuk pelanggan kembali
}
repo_.simpan(pelanggan, bayar);
return bayar;
}
private:
RepositoriPesanan& repo_;
};
// ===== Mini harness uji — pola yang sama dengan chapter Unit Testing =====
static int lulus = 0;
static int gagal = 0;
void cek(bool kondisi, const std::string& deskripsi) {
if (kondisi) {
++lulus;
std::println("[ LULUS ] {}", deskripsi);
} else {
++gagal;
std::println("[ GAGAL ] {}", deskripsi);
}
}
int main() {
// --- perakitan: implementasi nyata diinjeksi lewat constructor ---
RepositoriMemori repo;
LayananPesanan layanan(repo);
std::println("== demo dengan implementasi nyata ==");
std::println("pesanan pertama Budi : Rp{}", layanan.proses("Budi", 100000));
std::println("pesanan kedua Budi : Rp{} (diskon pelanggan kembali)",
layanan.proses("Budi", 100000));
// --- unit test: fake repository diinjeksi, class yang diuji tidak berubah ---
std::println("== unit test dengan fake repository ==");
RepositoriPalsu palsu;
LayananPesanan layananUji(palsu);
palsu.setJawaban(-1); // skenario 1: belum pernah memesan
cek(layananUji.proses("Budi", 100000) == 100000,
"pelanggan baru membayar penuh");
cek(palsu.simpanPernahDipanggil(), "pesanan disimpan ke repositori");
palsu.setJawaban(250000); // skenario 2: sudah pernah memesan
cek(layananUji.proses("Siti", 100000) == 95000,
"pelanggan kembali mendapat diskon 5%");
std::println("ringkasan: {} lulus, {} gagal", lulus, gagal);
return gagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
== demo dengan implementasi nyata ==
pesanan pertama Budi : Rp100000
pesanan kedua Budi : Rp95000 (diskon pelanggan kembali)
== unit test dengan fake repository ==
[ LULUS ] pelanggan baru membayar penuh
[ LULUS ] pesanan disimpan ke repositori
[ LULUS ] pelanggan kembali mendapat diskon 5%
ringkasan: 3 lulus, 0 gagalMari kita bedah bagian yang menentukan:
RepositoriPesanan(antarmuka): dua method, dua kebutuhan layanan — membaca riwayat dan menyimpan. NamatotalTerakhirmengembalikan-1untuk pelanggan baru; nilai sentinel ini adalah bagian dari kontrak, dan kedua implementasi sama-sama mematuhinya (LSP dari chapter SOLID Principles bekerja di sini).RepositoriMemori(implementasi nyata): dalam proyek sungguhan class ini mengecek database lewat library klien; untuk demo,std::vectorcukup menjelaskan perannya. Yang penting: ia bisa diberikan ke layanan tanpa layanan tahu bedanya.RepositoriPalsu(test double):totalTerakhir()tidak menghitung apa pun — ia mengembalikan jawaban yang dijadwalkan lewatsetJawaban(). Begitu pulasimpan(), yang tidak menyimpan melainkan mencatat bahwa ia dipanggil. Dengan dua kemampuan kecil ini, seluruh cabang aturan bisnis bisa dipanggil satu per satu secara sadar.LayananPesanan::proses(): aturan bisnisnya murni — baca riwayat, hitung diskon 5%, simpan hasil. Tidak ada satu baris pun yang tahu ada database, vector, atau test double; ia hanya berbicara dengan antarmuka.- Dua perakitan di
main():LayananPesanan layanan(repo)danLayananPesanan layananUji(palsu)adalah class yang sama persis dengan bahan berbeda. Tidak ada#ifdef TESTING, tidak ada subclass khusus untuk pengujian — injection menggantikan semuanya. - Hasil testnya: cabang pelanggan baru teruji dengan
setJawaban(-1), cabang diskon teruji dengansetJawaban(250000), dan aspek ketiga yang sering terlupakan ikut teruji — interaksinya:simpanPernahDipanggil()membuktikan layanan memang menulis ke repositori, bukan hanya menghitung.
Test Double: Stub, Fake, dan Keluarganya #
Istilah kolektifnya adalah test double — stand-in yang menggantikan object nyata saat pengujian. Beberapa ragam yang lazim dibedakan literature:
| Ragam | Peran | Wujud di program ini |
|---|---|---|
| Stub | Mengembalikan jawaban tetap yang dijadwalkan | totalTerakhir() versi RepositoriPalsu |
| Spy | Stub + mencatat panggilan yang diterima | simpanPernahDipanggil() |
| Fake | Implementasi ringan yang berperilaku mirip asli | RepositoriMemori (in-memory) |
| Mock | Double yang ekspektasi panggilannya dinyatakan dulu | Ranah framework (GoogleMock) |
Perbedaan praktisnya: stub dan spy dipakai untuk mengatur kondisi serta membuktikan interaksi; fake dipakai saat logic yang diuji butuh perilaku nyata — misalnya menguji “dua pesanan berturut-turut dari pelanggan yang sama” lebih wajar dijalankan di RepositoriMemori daripada di stub, karena butuh state yang benar. Untuk proyek kecil dan menengah, menulis double sendiri seperti di atas justru sering lebih cepat dan lebih jujur daripada memasang framework — dan tanpa framework pun kaidahnya sama: double mengimplementasikan antarmuka yang sama, sehingga class yang diuji tidak pernah berubah.
DI Tanpa Framework: main() sebagai Composition Root #
Di banyak bahasa, DI identitas dengan framework — kontainer yang memindai class dan menyambung semuanya otomatis. C++ komunitasnya cenderung menolak langkah itu, dan alasannya jujur: constructor injection yang eksplisit sudah memberi semua manfaatnya — substitusi, pengujian, kopling yang terlihat — tanpa langkah tersembunyi di belakang layar. Yang dibutuhkan hanyalah disiplin satu tempat: seluruh perakitan object terjadi di composition root (biasanya main()), dan sisa program hanya menerima object jadi. Semakin dalam object dibangun sendiri di tengah kode (RepositoriDatabase repo; di dalam fungsi bisnis), semakin sulit diganti — aturan praktisnya: class membuat object yang ia bungkus saja; object yang ia panggil, ia terima dari luar.
Bergantung yang lewat pintu depan, teruji dengan double yang bisa diatur — struktur class kita kini siap dipajang ke pemakai lain. Pertanyaan berikutnya bergeser ke bentuk luar class dan function itu sendiri: nama, parameter, return type, dan kontrak yang membuat pemakai tak perlu membaca implementasi — seni mendesain API.
Referensi: Abstract classes (cppreference) , GoogleTest — GitHub .