Chapter lalu kita memecah program menjadi modul dengan bertanya “apa tanggung jawab tiap bagian”. Pertanyaan yang lebih tajam lagi: pekerjaan mana yang sebenarnya berbeda jenis, bukan hanya berbeda tempat? Program hampir selalu punya tiga jenis pekerjaan yang sifatnya jauh berbeda: membaca dan mengubah data mentah menjadi bentuk yang bisa diolah (parsing), menghitung kesimpulan dari data itu (logika), dan menyajikan hasilnya kepada manusia (presentasi). Ketiganya berubah karena alasan yang berbeda — format input berubah, aturan bisnis berubah, selera tampilan berubah — dan karena itu keduanya seharusnya tidak tinggal serumah. Prinsipnya bernama separation of concerns (pemisahan urusan), dan chapter ini membuktikannya dengan refactor nyata.
Tiga Urusan yang Sering Menempel di Satu Loop #
Perhatikan program laporan pengeluaran berikut. Ia benar dan output-nya rapi, tetapi satu loop di dalam main() melakukan tiga hal sekaligus: memecah teks "Teh;5000" menjadi nama dan angka (parsing), menjumlahkan dan mencatat nilai terbesar (logika), sekaligus mencetak setiap baris laporan (presentasi):
// laporan-campur.cpp — SEBELUM: parsing, logika, dan pencetakan menyatu di main()
// Kompilasi: g++ -std=c++23 -Wall -Wextra laporan-campur.cpp -o laporan-campur
#include <print>
#include <string>
#include <vector>
int main() {
std::vector<std::string> baris = {
"Teh;5000", "Kopi;7000", "Roti;4000", "Sandal;25000"
};
// Satu loop yang tiga urusan sekaligus: memecah teks, menghitung,
// dan mencetak — semuanya di dalam badan main().
std::println("==== LAPORAN PENGELUARAN ====");
int total = 0;
int terbesar = 0;
std::string keteranganTerbesar;
for (const std::string& satu : baris) {
std::size_t posisi = satu.find(';');
std::string nama = satu.substr(0, posisi);
int jumlah = std::stoi(satu.substr(posisi + 1));
total += jumlah;
if (jumlah > terbesar) {
terbesar = jumlah;
keteranganTerbesar = nama;
}
std::println("{:<18}Rp{:>7}", nama, jumlah);
}
std::println("-----------------------------");
std::println("{:<18}{:>9}", "Jumlah item", baris.size());
std::println("{:<18}Rp{:>7}", "Total", total);
std::println("{:<18}Rp{:>7} ({})", "Terbesar", terbesar, keteranganTerbesar);
return 0;
}Output yang Dihasilkan:
==== LAPORAN PENGELUARAN ====
Teh Rp 5000
Kopi Rp 7000
Roti Rp 4000
Sandal Rp 25000
-----------------------------
Jumlah item 4
Total Rp 41000
Terbesar Rp 25000 (Sandal)Masalahnya belum terlihat hari ini — masalahnya muncul saat kebutuhan datang. Format input berganti dari ; ke koma? Kita harus menyunting loop yang isinya juga logika dan pencetakan, lalu mengetes ulang semuanya. Laporan harus versi HTML selain terminal? Logika perhitungan harus disalin ke program kedua, karena logika itu menempel pada pencetakannya. Gejala klasiknya satu: setiap perubahan menyentuh tempat yang sama, tanpa peduli urusannya apa.
Membelah Menjadi Tiga Lapisan #
Versi terpisah membagi program menjadi tiga namespace, plus satu lapisan kecil di tengahnya: struct data yang menjadi bahasa percakapan antar lapisan. Perhatikan pola arus datanya: teks mentah masuk ke parsing dan keluar sebagai std::vector<Pengeluaran>; vector itu masuk ke analisis dan keluar sebagai HasilAnalisis; struct itu masuk ke laporan dan keluar sebagai teks di layar:
// laporan-terpisah.cpp — SESUDAH: parsing, analisis, dan presentasi jadi tiga lapisan
// Kompilasi: g++ -std=c++23 -Wall -Wextra laporan-terpisah.cpp -o laporan-terpisah
#include <print>
#include <string>
#include <vector>
// ===== Data yang mengalir antar lapisan =====
struct Pengeluaran {
std::string keterangan;
int jumlah;
};
// ===== Lapisan 1: parsing — teks mentah menjadi data =====
namespace parsing {
std::vector<Pengeluaran> dariTeks(const std::vector<std::string>& baris) {
std::vector<Pengeluaran> hasil;
for (const std::string& satu : baris) {
std::size_t posisi = satu.find(';');
hasil.push_back({satu.substr(0, posisi),
std::stoi(satu.substr(posisi + 1))});
}
return hasil;
}
}
// ===== Lapisan 2: analisis — data menjadi kesimpulan =====
struct HasilAnalisis {
int jumlahItem;
int total;
int terbesar;
std::string keteranganTerbesar;
};
namespace analisis {
HasilAnalisis ringkas(const std::vector<Pengeluaran>& daftar) {
HasilAnalisis h{0, 0, 0, "-"};
h.jumlahItem = static_cast<int>(daftar.size());
for (const Pengeluaran& p : daftar) {
h.total += p.jumlah;
if (p.jumlah > h.terbesar) {
h.terbesar = p.jumlah;
h.keteranganTerbesar = p.keterangan;
}
}
return h;
}
}
// ===== Lapisan 3: presentasi — kesimpulan menjadi teks =====
namespace laporan {
void cetak(const std::vector<Pengeluaran>& daftar, const HasilAnalisis& h) {
std::println("==== LAPORAN PENGELUARAN ====");
for (const Pengeluaran& p : daftar) {
std::println("{:<18}Rp{:>7}", p.keterangan, p.jumlah);
}
std::println("-----------------------------");
std::println("{:<18}{:>9}", "Jumlah item", h.jumlahItem);
std::println("{:<18}Rp{:>7}", "Total", h.total);
std::println("{:<18}Rp{:>7} ({})", "Terbesar", h.terbesar, h.keteranganTerbesar);
}
}
// ===== Pengujian per bagian: setiap lapisan diuji sendiri-sendiri =====
void ujiParsing() {
auto daftar = parsing::dariTeks({"A;100", "B;250"});
std::println("[uji] parsing : {} item -> '{}' {}",
daftar.size(), daftar[1].keterangan, daftar[1].jumlah);
}
void ujiAnalisis() {
std::vector<Pengeluaran> data = {{"A", 100}, {"B", 250}};
auto h = analisis::ringkas(data);
std::println("[uji] analisis: total {} terbesar {} ({})",
h.total, h.terbesar, h.keteranganTerbesar);
}
void ujiLaporan() {
std::vector<Pengeluaran> data = {{"X", 90}};
laporan::cetak(data, analisis::ringkas(data));
}
int main() {
ujiParsing(); // parsing diuji tanpa analisis dan laporan
ujiAnalisis(); // analisis diuji tanpa parsing dan pencetakan
ujiLaporan(); // laporan diuji dengan data buatan yang kecil
std::println();
// Alur utama: data mengalir satu arah melalui ketiga lapisan
std::vector<std::string> baris = {
"Teh;5000", "Kopi;7000", "Roti;4000", "Sandal;25000"
};
auto daftar = parsing::dariTeks(baris);
laporan::cetak(daftar, analisis::ringkas(daftar));
return 0;
}Output yang Dihasilkan:
[uji] parsing : 2 item -> 'B' 250
[uji] analisis: total 350 terbesar 250 (B)
==== LAPORAN PENGELUARAN ====
X Rp 90
-----------------------------
Jumlah item 1
Total Rp 90
Terbesar Rp 90 (X)
==== LAPORAN PENGELUARAN ====
Teh Rp 5000
Kopi Rp 7000
Roti Rp 4000
Sandal Rp 25000
-----------------------------
Jumlah item 4
Total Rp 41000
Terbesar Rp 25000 (Sandal)Mari kita bedah keputusan pentingnya:
struct Pengeluarandanstruct HasilAnalisis: kedua struct adalah kontrak antar lapisan.parsingtidak tahu adalaporan;laporantidak tahu adaparsing— keduanya hanya sepakat tentang bentuk data yang lewat.namespace parsing: satu-satunya tempat karakter;disebut. Format input berubah? Hanya namespace ini yang disentuh — dan dua lapisan lain tidak pernah tahu ada perubahan itu.namespace analisis: murni data masuk, struct keluar, tanpa satu barisstd::println. Inilah yang membuatnya bisa diuji dan dipakai ulang di mana saja — misalnya di program yang hasilnya dikirim ke file, bukan ke layar.namespace laporan: sebaliknya, lapisan ini hanya tahu menampilkan. Aturan perhitungan berubah?laporantidak disentuh sama sekali.- Tiga function
uji...(): inilah buah pemisahan yang paling praktis — setiap lapisan punya pengujian sendiri yang berdiri tanpa lapisan lain, persis pola unit testing dari Section Debugging dan Code Quality. Di versi campur, menguji “apakah total dihitung benar” berarti menjalankan seluruh program termasuk pencetakannya. main()sebagai perakit:main()kini hanya menyambung lapisan — teks keparsing, hasilnya keanalisis, hasilnya kelaporan. Blok laporan akhir pada output tercetak identik dengan versi campur: perilaku program tidak berubah, hanya strukturnya.
Arah Dependensi: Panah yang Hanya Menunjuk ke Bawah #
Pemisahan urusan bisa divisualisasikan sebagai arah panah dependensi — siapa butuh siapa untuk bisa dikompilasi:
Diagram ini punya satu sifat yang harus dijaga: semua panah menunjuk ke bawah, dan tidak ada panah yang menunjuk kembali ke atas. parsing tidak boleh memanggil analisis, analisis tidak boleh mencetak, laporan tidak boleh memecah teks. Begitu satu panah menunjuk naik, dua lapisan itu saling menahan: mengubah yang bawah memaksa perubahan di yang atas, dan menguji yang bawah butuh yang atas ikut dibangun. Kaidah yang sama yang muncul lagi di chapter berikutnya dengan nama yang lebih formal: dependency direction.
Tiga lapisan di atas adalah kasus paling umum, tetapi kaidahnya umum juga: setiap kali dua potong kode berubah karena alasan yang berbeda, mereka dua urusan — pisahkan. Setelah terpisah, pertanyaan berikutnya justru lebih menarik: bagaimana mengatur relasi antar modul yang sudah terpisah, sehingga perubahan pada satu modul tidak menyeret modul lain? Itulah topik dependency management.
Referensi: Namespaces (cppreference) , std::stoi (cppreference) .