Selama empat belas chapter terakhir kita membangun kotak peralatan OOP: private dan public, constructor yang menjamin state awal, const yang melindungi object, friend yang membuka pintu terpilih. Chapter ini merangkai semuanya menjadi satu prinsip yang menjadi tujuan akhir alat-alat itu: encapsulation, seni menyembunyikan detail internal dan mengekspos maksud, bukan mesinnya. Class yang ter-encapsulasi dengan baik membuat pemakainya tidak perlu tahu bagaimana data disimpan; mereka hanya perlu tahu operasi apa yang bisa dilakukan. Prinsip ini bukan soal estetika, melainkan strategi agar class tetap mudah diubah tanpa mematahkan kode yang memakainya.

Menyembunyikan Detail, Menyediakan Antarmuka #

Encapsulation memiliki dua sisi yang saling melengkapi. Sisi pertama adalah penyembunyian: data member dan helper function yang hanya menopang mekanisme internal kita taruh di bagian private, sehingga tidak ada kode luar yang bisa bergantung padanya. Sisi kedua adalah antarmuka: bagian public berisi operasi-operasi yang menyatakan apa yang class ini mampu lakukan — setor(), tarik(), getAlamat() — dan itulah satu-satunya cara dunia luar berinteraksi.

Pembedaan ini menghasilkan kontrak yang jelas. Antarmuka adalah janji yang kita jaga lama: tanda tangannya tidak berubah-ubah karena banyak kode lain memanggilnya. Detail internal adalah wilayah bebas: kita boleh mengganti std::string menjadi cache yang lebih canggih, atau menambahkan penghitung statistik, tanpa satu pun pemanggil perlu tahu. Semakin sedikit yang terlihat dari luar, semakin besar kebebasan kita mengubah isi.

Bandingkan dengan class yang semua member-nya public: setiap kode luar boleh membaca dan menulis field apa pun secara langsung. Begitu ada seratus tempat yang menyentuh field itu, mengubah field tersebut berarti memeriksa seratus lokasi sekaligus. Encapsulation menutup kemungkinan itu sejak awal.

Getter dan Setter yang Bermakna #

Getter dan setter sering dianggap formalitas: satu fungsi membaca field, satu fungsi mengisi field, tanpa logika apa pun. Bentuk seperti itu memang sah, tetapi nilainya tipis — ia hanya menambah ketikan tanpa menambah jaminan. Getter dan setter yang benar-benar berharga adalah yang bermakna: setter memvalidasi nilai sebelum menerima, menolak data yang akan membuat object tidak konsisten; getter menyajikan data dalam bentuk yang aman dibaca, misalnya mengembalikan const std::string& agar tidak terjadi salinan yang tidak perlu.

Setter bermakna juga menjadi tempat efek samping yang terukur: memperbarui field turunan, mencatat perubahan, atau memicu notifikasi. Karena semua perubahan lewat satu pintu, efek itu dijamin terjadi — tidak ada jalur yang lolos begitu saja. Jika suatu field memang tidak boleh diubah dari luar sama sekali, kita tidak menulis setter-nya; tidak ada aturan yang mewajibkan setiap field punya pasangan lengkap.

Invariant: Janji yang Selalu Berlaku #

Invariant class adalah kondisi yang harus benar pada setiap saat object hidup — misalnya “email pada AkunEmail selalu memuat @”, atau “saldo tidak pernah negatif”. Invariant itulah yang membuat method-method lain boleh ditulis dengan percaya diri: method pengiriman email tidak perlu memeriksa ulang apakah alamatnya valid, karena constructor dan setter sudah menjaminnya.

Menjaga invariant butuh dua langkah yang bekerja bersama. Pertama, semua data yang menjadi bagian invariant ditaruh di private, sehingga tidak ada jalur bypass. Kedua, setiap jalur masuk — constructor dan setter — menjalankan validasi yang sama. Perhatikan pada contoh di bawah: constructor tidak menyalin kode validasi, melainkan memanggil setAlamat() yang sama dipakai pemanggil. Satu aturan, satu tempat, nol celah.

Bila invariant dilanggar, sebaiknya penolakan terjadi sedini mungkin — di setter, bukan saat method lain sudah terlanjur memakai data buruk. Kegagalan cepat seperti ini membuat bug mudah dilacak karena jejaknya masih dekat dengan penyebabnya.

Contoh Lengkap: Validasi di Satu Pintu #

Program berikut merangkai ketiga ide di atas: data disembunyikan, setter memvalidasi, dan invariant “alamat selalu valid” dijaga oleh satu pintu yang sama:

cpp
#include <iostream>
#include <string>

class AkunEmail {
private:
    std::string alamat;
    int upayaDitolak = 0;   // detail internal, tak perlu diketahui pemanggil

    // Helper private: hanya class ini yang memakainya
    static bool formatValid(const std::string& kandidat) {
        std::size_t pos = kandidat.find('@');
        return pos != std::string::npos && pos > 0 && pos + 1 < kandidat.length();
    }

public:
    AkunEmail(const std::string& alamatAwal) {
        setAlamat(alamatAwal);   // constructor memakai pintu validasi yang sama
    }

    // Getter: membaca state tanpa membuka jalur pengubahan
    const std::string& getAlamat() const { return alamat; }
    bool validKah() const { return formatValid(alamat); }

    // Setter bermakna: satu-satunya pintu mengubah email
    bool setAlamat(const std::string& kandidat) {
        if (!formatValid(kandidat)) {
            ++upayaDitolak;
            std::cout << "  ditolak: \"" << kandidat << "\" tidak memenuhi aturan\n";
            return false;
        }
        alamat = kandidat;
        return true;
    }
};

int main() {
    AkunEmail akun("[email protected]");
    std::cout << "Email awal : " << akun.getAlamat() << "\n";

    akun.setAlamat("budi-tanpa-at");      // ditolak, state tidak berubah
    std::cout << "Setelah 1  : " << akun.getAlamat() << "\n";

    akun.setAlamat("[email protected]");    // diterima
    std::cout << "Setelah 2  : " << akun.getAlamat() << "\n";
    std::cout << "Valid? " << (akun.validKah() ? "ya" : "tidak") << "\n";

    return 0;
}

Output yang Dihasilkan:

text
Email awal : [email protected]
  ditolak: "budi-tanpa-at" tidak memenuhi aturan
Setelah 1  : [email protected]
Setelah 2  : [email protected]
Valid? ya

Mari kita bedah:

  1. Baris 6-7 (alamat, upayaDitolak): keduanya private. upayaDitolak bahkan tidak terekspos lewat getter apa pun — contoh nyata bahwa ada detail internal yang memang untuk mata kita sendiri.
  2. Baris 10-13 (formatValid()): helper static karena tidak menyentuh state object tertentu. Ia private juga: aturan format adalah detail; pemanggil cukup melihat setAlamat() yang mengembalikan bool.
  3. Baris 16-18 (constructor): constructor mendelegasikan pengisian awal ke setAlamat(). Tidak ada jalur kedua yang bisa lupa validasi — invariant mulai dijaga sejak detik pertama object hidup.
  4. Baris 21-22 (getter): getAlamat() mengembalikan const std::string& — referensi konstan, tanpa salinan, tanpa kemungkinan diubah lewat hasil kembalian. validKah() memberi cara mengecek state tanpa membuka alamat mentah.
  5. Baris 25-33 (setter): nilai yang gagal aturan tidak mengubah alamat sama sekali — hanya penghitung internal yang bertambah. Baris Setelah 1 pada output membuktikannya: usaha mengganti email ditolak, state tetap utuh.
  6. Baris 40-45 (dalam main()): pemanggil tidak pernah menyentuh akun.alamat secara langsung; semua interaksi lewat antarmuka. Compiler akan menolak dengan error is private within this context bila dicoba — jaminan yang ditegakkan mesin, bukan sekadar kesepakatan.

Keuntungan Pemeliharaan #

Manfaat terbesar encapsulation baru terasa saat class perlu berubah. Katakanlah besok kita ingin menyimpan alamat dalam bentuk terenkripsi, atau menambahkan daftar alamat lama sebagai riwayat: selama getAlamat() dan setAlamat() tetap berperilaku sama, seluruh kode pemanggil tidak perlu disentuh. Tanpa encapsulation, perubahan sekecil itu merambat ke setiap file yang membaca field-nya.

Encapsulation juga mempersempit area pencarian bug. Bila invariant “alamat selalu valid” dilanggar, pelakunya hanya bisa ada di constructor dan setter — satu-dua tempat — karena tidak ada jalur lain yang bisa menulis alamat. Laporan pengujian pun lebih mudah dibaca: antarmuka yang kecil dan bermakna lebih mudah diuji menyeluruh daripada tumpukan field terbuka.

Dengan ini fondasi OOP kita utuh: class yang menyembunyikan detail dan menawarkan antarmuka. Chapter berikutnya melangkah ke cara menyusun class dari class lain — composition, di mana satu object memiliki object lain sebagai bagian internalnya, dan bagian itu ikut dibangun otomatis bersama tuannya.

Rujukan resmi: Classes (cppreference) , Member access (cppreference) .