Program yang selama ini kita tulis masih berukuran ratusan baris — masih bisa dipeluk sambil dibaca. Program sungguhan berukuran puluhan ribu baris, ditulis beberapa orang, diubah bertahun-tahun, dan tidak ada satu pun manusia yang memegang seluruh isinya di kepala. Kunci bertahan di ukuran itu adalah modularitas: program dipecah menjadi bagian-bagian yang masing-masing punya satu tanggung jawab, antarmuka yang jelas, dan bisa dipahami tanpa membaca sisanya. Section ini membahas ilmu di balik pembagian itu — dan chapter pembuka dimulai dari pertanyaan paling praktis: bagian mana yang boleh dipisah, dan bagaimana caranya tanpa merusak program.

Kriteria Memecah: Responsibility, Kohesi, Coupling #

Tiga kata kunci memandu setiap pemisahan. Yang pertama adalah responsibility (tanggung jawab): satu modul menjawab satu pertanyaan tentang program — “apa isinya menu”, “apa yang dipesan pelanggan”, atau “bagaimana struk ditampilkan” — bukan ketiganya sekaligus. Kalau deskripsi modul kita butuh kata “dan” lebih dari sekali, biasanya itu dua modul yang masih menempel.

Kedua, kohesi (cohesion): seberapa erat isi satu modul saling melayani tujuan yang sama. Fungsi hargaDari() dan data daftar menu punya kohesi tinggi — keduanya hanya berguna untuk pertanyaan yang sama. Sebaliknya, sebuah class bernama Util berisi fungsi hitung pajak, format tanggal, dan baca file punya kohesi rendah: tiga topik yang kebetulan tidur satu kamar. Kohesi tinggi adalah target; kita ingin bagian yang berubah bersama tinggal bersama.

Ketiga, coupling (kopling): seberapa banyak satu modul harus tahu tentang isi modul lain. Modul yang mengakses variable global milik modul lain, atau membaca field private lewat friend, koplingnya erat — mengubah satu berarti menyiapkan diri memperbaiki yang lain. Desain yang sehat mengejar kombinasi kohesi tinggi, coupling rendah: setiap modul padat urusan sendiri, dan antar modul hanya bertukar data lewat antarmuka yang kecil. Dua sisa kriteria itu akan kita perdalam sepanjang section ini; sekarang mari kita buktikan dengan refactor nyata.

Satu main() yang Mengerjakan Segalanya #

Program kafe berikut adalah kejadian sehari-hari: fungsi main() menyimpan data menu, memproses pesanan, menghitung pajak, sekaligus mencetak struk. Ia benar, teruji, dan lolos review — sampai kebutuhan pertama datang:

cpp
// pesanan-monolitik.cpp — SEBELUM: semuanya hidup di dalam satu fungsi main()
// Kompilasi: g++ -std=c++23 -Wall -Wextra pesanan-monolitik.cpp -o pesanan-monolitik

#include <print>
#include <string>
#include <vector>

int main() {
    // 1) Data menu
    std::vector<std::string> namaMenu = {"Kopi Susu", "Roti Bakar", "Teh Hijau"};
    std::vector<int> hargaMenu = {18000, 12000, 8000};

    // 2) Pesanan pelanggan
    std::vector<std::string> namaDipesan = {"Kopi Susu", "Roti Bakar"};
    std::vector<int> jumlahDipesan = {2, 1};

    // 3) Logika perhitungan: cari harga lalu jumlahkan
    int subtotal = 0;
    for (std::size_t i = 0; i < namaDipesan.size(); ++i) {
        for (std::size_t j = 0; j < namaMenu.size(); ++j) {
            if (namaDipesan[i] == namaMenu[j]) {
                subtotal += hargaMenu[j] * jumlahDipesan[i];
                break;
            }
        }
    }
    int pajak = subtotal * 11 / 100;

    // 4) Presentasi: cetak struk
    std::println("===== KAFE SAMPURNA =====");
    for (std::size_t i = 0; i < namaDipesan.size(); ++i) {
        int hargaBaris = 0;
        for (std::size_t j = 0; j < namaMenu.size(); ++j) {
            if (namaDipesan[i] == namaMenu[j]) {
                hargaBaris = hargaMenu[j] * jumlahDipesan[i];
                break;
            }
        }
        std::println("{:<11} x{}  Rp{}", namaDipesan[i], jumlahDipesan[i], hargaBaris);
    }
    std::println("------------------------");
    std::println("{:<11}    Rp{}", "Subtotal", subtotal);
    std::println("{:<11}    Rp{}", "Pajak 11%", pajak);
    std::println("{:<11}    Rp{}", "Total", subtotal + pajak);
    return 0;
}

Output yang Dihasilkan:

text
===== KAFE SAMPURNA =====
Kopi Susu   x2  Rp36000
Roti Bakar  x1  Rp12000
------------------------
Subtotal       Rp48000
Pajak 11%      Rp5280
Total          Rp53280

Coba perhatikan gejalanya, karena gejala ini yang akan kita cari di kode sendiri kelak. Pencarian harga ditulis dua kali — sekali di blok perhitungan, sekali di blok pencetakan — karena kedua blok sama-sama butuh dan tidak ada tempat berbagi. Data menu tersebar sebagai dua std::vector paralel yang harus dijaga tetap sinkron secara manual. Dan pertanyaan “berapa harga Kopi Susu” tidak punya rumah: jawabannya berhamburan di dalam loop main(). Keempat masalah ini satu akar: terlalu banyak tanggung jawab di satu tempat.

Memecah Menjadi Tiga Unit #

Versi modularnya tidak menambah kemampuan apa pun — output-nya persis sama — tetapi setiap tanggung jawab kini punya rumah. Perhatikan juga komentar di setiap kepala class: kalau kalimat “unit ini tahu …” tidak bisa ditulis dalam satu kalimat pendek, pemecahan kita belum selesai:

cpp
// pesanan-modular.cpp — SESUDAH: tanggung jawab dipecah menjadi tiga unit
// Kompilasi: g++ -std=c++23 -Wall -Wextra pesanan-modular.cpp -o pesanan-modular

#include <print>
#include <string>
#include <vector>

// ===== Unit 1: Menu — tahu daftar barang dan harganya =====
struct ItemMenu {
    std::string nama;
    int harga;
};

class Menu {
public:
    void tambah(const std::string& nama, int harga) {
        item_.push_back({nama, harga});
    }

    int hargaDari(const std::string& nama) const {
        for (const ItemMenu& item : item_) {
            if (item.nama == nama) {
                return item.harga;
            }
        }
        return 0;   // pencarian gagal: kembalikan 0, bukan mencetak error
    }

private:
    std::vector<ItemMenu> item_;
};

// ===== Unit 2: Pesanan — tahu daftar barang yang dipesan =====
struct BarisPesanan {
    std::string nama;
    int jumlah;
};

class Pesanan {
public:
    void tambah(const std::string& nama, int jumlah) {
        baris_.push_back({nama, jumlah});
    }

    int subtotal(const Menu& menu) const {
        int total = 0;
        for (const BarisPesanan& b : baris_) {
            total += menu.hargaDari(b.nama) * b.jumlah;
        }
        return total;
    }

    const std::vector<BarisPesanan>& baris() const { return baris_; }

private:
    std::vector<BarisPesanan> baris_;
};

// ===== Unit 3: Struk — tahu cara menampilkan hasilnya =====
class Struk {
public:
    Struk(const Menu& menu, const Pesanan& pesanan)
        : menu_(menu), pesanan_(pesanan) {}

    void cetak() const {
        int subtotal = pesanan_.subtotal(menu_);
        int pajak = subtotal * 11 / 100;

        std::println("===== KAFE SAMPURNA =====");
        for (const BarisPesanan& b : pesanan_.baris()) {
            int hargaBaris = menu_.hargaDari(b.nama) * b.jumlah;
            std::println("{:<11} x{}  Rp{}", b.nama, b.jumlah, hargaBaris);
        }
        std::println("------------------------");
        std::println("{:<11}    Rp{}", "Subtotal", subtotal);
        std::println("{:<11}    Rp{}", "Pajak 11%", pajak);
        std::println("{:<11}    Rp{}", "Total", subtotal + pajak);
    }

private:
    const Menu& menu_;
    const Pesanan& pesanan_;
};

int main() {
    Menu menu;
    menu.tambah("Kopi Susu", 18000);
    menu.tambah("Roti Bakar", 12000);
    menu.tambah("Teh Hijau", 8000);

    Pesanan pesanan;
    pesanan.tambah("Kopi Susu", 2);
    pesanan.tambah("Roti Bakar", 1);

    Struk struk(menu, pesanan);
    struk.cetak();
    return 0;
}

Output yang Dihasilkan:

text
===== KAFE SAMPURNA =====
Kopi Susu   x2  Rp36000
Roti Bakar  x1  Rp12000
------------------------
Subtotal       Rp48000
Pajak 11%      Rp5280
Total          Rp53280

Mari kita bedah keputusan desainnya:

  1. struct ItemMenu dan struct BarisPesanan: data yang berubah bersama dikemas bersama. Dua std::vector paralel yang dulu harus dijaga sinkronnya kini menjadi satu object yang tidak mungkin terpecah.
  2. Menu::hargaDari(): pencarian harga kini ditulis sekali. Duplikasi di versi monolitik hilang bukan karena kita rajin, melainkan karena pencarian itu akhirnya punya satu rumah — gejala “kode yang sama di dua tempat” hampir selalu sembuh sendiri setelah kohesi diperbaiki.
  3. Menu::hargaDari() mengembalikan 0, bukan mencetak error: class data tidak mengurus presentasi. Ini coupling rendah dalam praktik: Menu bisa dipakai program tanpa layar — misalnya di dalam test — tanpa ikut memunculkan teks.
  4. Pesanan::subtotal(const Menu& menu): perhitungan menerima Menu sebagai parameter const reference — Pesanan tidak menyimpan atau memiliki Menu, ia hanya meminjamnya saat dihitung. Kopling tetap ada, tetapi longgar dan terlihat di antarmuka.
  5. Field private dengan trailing underscore (item_, baris_): konvensi penanda anggota yang akan kita pakai sepanjang section; isi internal tidak terlihat dari luar — materi Encapsulation yang kini bekerja untuk kita.
  6. main() menjadi rakitan, bukan resep: tiga baris deklarasi object lalu tiga panggilan setup. main() kini membaca seperti daftar belanja komponen, dan itulah tujuan akhirnya — program sebagai komposisi modul, bukan satu fungsi panjang.

Modul sebagai Unit Komposisi dan Pengujian #

Nilai sesungguhnya dari pemecahan ini muncul ketika program berkembang. Menu berubah format? Hanya Menu yang disentuh. Struk harus versi HTML? Tulis class baru yang menerima Menu dan Pesanan yang sama — dua unit lain tidak tahu dan tidak peduli. Itulah makna modul sebagai unit komposisi: program baru dirakit dengan menggabungkan bagian yang sudah teruji, bukan menulis ulang dari awal.

Nilai keduanya adalah modul sebagai unit pengujian. Fungsi subtotal() kini bisa dipanggil langsung dari program tes kecil — buat Menu, buat Pesanan, bandingkan hasilnya dengan angka yang diharapkan — tanpa menyalakan seluruh program dan tanpa melihat satu baris struk pun. Di versi monolitik, pengujian seperti itu mustahil: satu-satunya cara menjalankan logika adalah menjalankan main() secara utuh, lengkap dengan pencetakannya. Recap dari Section Build System: di proyek nyata, setiap unit di atas biasanya berpindah ke header dan source file sendiri — menu.h/menu.cpp, pesanan.h/pesanan.cpp — sehingga masing-masing dikompilasi terpisah dan diuji terpisah. Di chapter ini keduanya sengaja tinggal satu file agar contohnya bisa dijalankan apa adanya; cara memecahnya ke file terpisah sudah Anda kuasai.

Pemecahan menjadi modul baru setengah pekerjaan; setengah sisanya adalah memutuskan apa yang dipajang di antarmuka masing-masing modul dan apa yang disembunyikan. Itulah pasangan konsep berikutnya — abstraction dan encapsulation.

Referensi: Classes (cppreference) , struct (cppreference) .