Semua field yang kita tulis sejauh ini dapat dibaca dan ditulis oleh kode mana pun di luar class: cukup sebut buku.stok = -50 dan state langsung rusak tanpa satu pun protes dari compiler. Untuk class latihan, kelalaian seperti itu hanya mengganggu; untuk saldo rekening bank, kelalaian semacam itu bencana. Encapsulation — pilar pertama OOP — menutup akses langsung ke data internal object dan memaksa seluruh perubahan melewati method yang kita rancang sendiri. Pada Chapter ini kita bangun class RekeningBank yang menjaga satu invarian sederhana namun krusial: saldo tidak pernah boleh negatif, dan jumlah transaksi harus positif.

Invarian dan Risiko Field Terbuka #

Invarian adalah kondisi yang harus selalu benar selama object hidup. Rekening bank yang sehat memiliki invarian “saldo ≥ 0 dan hanya berubah lewat transaksi sah”. Selama field saldo berstatus terbuka, tidak ada satu tempat pun yang bisa menegakkan invarian tersebut — setiap baris kode di seluruh program adalah calon pelanggar. Versi tanpa encapsulation tampak seperti ini:

java Versi tanpa encapsulation
class RekeningTerbuka {
    public long saldo;   // kode mana pun boleh menulis nilai apa pun
}

// di bagian lain program, tanpa perlawanan dari compiler:
// rekening.saldo = rekening.saldo - 1000000;  // saldo bisa negatif

Masalahnya bukan sekadar angka yang salah. Ketika saldo tiba-tiba bernilai aneh, kita kehilangan tempat bertanya: tidak ada titik kontrol yang log-nya bisa dicatat, tidak ada validasi yang bisa menolak nilai mustahil, dan setiap penambahan aturan harus disuntikkan ke semua titik penulisan tersebar itu. Encapsulation membalik situasinya dengan tiga langkah baku: jadikan field private, sediakan getter bila data perlu dibaca, dan sediakan method mutasi bila data boleh berubah.

Class RekeningBank dengan State Terlindungi #

Berikut class RekeningBank lengkap dengan perlindungannya. Perhatikan bahwa field pemilik dan saldo ditandai private, sedangkan method getPemilik, getSaldo, setor, dan tarik menjadi satu-satunya pintu menuju data tersebut:

class RekeningBank {
    private String pemilik;
    private long saldo;

    RekeningBank(String pemilik, long saldoAwal) {
        this.pemilik = pemilik;
        if (saldoAwal < 0) {
            saldoAwal = 0;
        }
        this.saldo = saldoAwal;
    }

    String getPemilik() {
        return pemilik;
    }

    long getSaldo() {
        return saldo;
    }

    void setor(long jumlah) {
        if (jumlah <= 0) {
            System.out.println("Setor " + jumlah + " ditolak: jumlah harus positif");
            return;
        }
        saldo = saldo + jumlah;
        System.out.println("Setor " + jumlah + " berhasil, saldo menjadi " + saldo);
    }

    void tarik(long jumlah) {
        if (jumlah <= 0) {
            System.out.println("Tarik " + jumlah + " ditolak: jumlah harus positif");
            return;
        }
        if (jumlah > saldo) {
            System.out.println("Tarik " + jumlah + " ditolak: saldo tidak cukup");
            return;
        }
        saldo = saldo - jumlah;
        System.out.println("Tarik " + jumlah + " berhasil, saldo menjadi " + saldo);
    }
}

Validasi tidak hanya duduk di method — constructor ikut menjaga gerbang paling awal dengan menyetel saldoAwal negatif menjadi 0. Dengan demikian, object tidak pernah sempat berada dalam keadaan melanggar invarian, bahkan selama beberapa mikrodetik setelah new.

Percobaan Menembus Invarian #

Program pemanggil berikut sengaja melakukan empat transaksi: dua yang sah, satu setor dengan jumlah negatif, dan satu penarikan yang melebihi saldo. Inilah saat kebenaran encapsulation diuji:

public class AplikasiBank {
    public static void main(String[] args) {
        RekeningBank rekening = new RekeningBank("Budi Santoso", 500000);

        System.out.println("Pemilik : " + rekening.getPemilik());
        System.out.println("Saldo   : " + rekening.getSaldo());

        rekening.setor(250000);
        rekening.setor(-100);
        rekening.tarik(1000000);
        rekening.tarik(100000);

        System.out.println("Saldo akhir: " + rekening.getSaldo());
    }
}

Output yang Dihasilkan:

text
Pemilik : Budi Santoso
Saldo   : 500000
Setor 250000 berhasil, saldo menjadi 750000
Setor -100 ditolak: jumlah harus positif
Tarik 1000000 ditolak: saldo tidak cukup
Tarik 100000 berhasil, saldo menjadi 650000
Saldo akhir: 650000

Dua transaksi tidak sah ditolak dengan pesan yang jelas, dan saldo akhir tetap konsisten dengan transaksi yang diterima. Mari kita bedah lapisan-lapisan pengamanannya:

  1. Baris 4 (new RekeningBank("Budi Santoso", 500000)): saldo awal sah, sehingga constructor mengisinya apa adanya. Bila argumen bernilai negatif, constructor menyetelnya menjadi 0 sebelum field diisi — gerbang pertama invarian.
  2. Baris 10 (rekening.setor(-100);): method setor menemukan jumlah <= 0, mencetak penolakan, lalu menjalankan return — pernyataan sisanya yang mengubah saldo tidak pernah tereksekusi. Field tetap utuh.
  3. Baris 11 (rekening.tarik(1000000);): jumlah penarikan positif sehingga lolos pemeriksaan pertama, tetapi jumlah > saldo bernilai true (1000000 lebih besar dari 750000), sehingga penarikan ditolak di pemeriksaan kedua. Tanpa pemeriksaan ini, saldo akan menjadi -250000 dan invarian dilanggar.
  4. Baris 12 (rekening.tarik(100000);): kedua pemeriksaan lolos; saldo = saldo - jumlah berjalan dan saldo turun menjadi 650000 — perubahan state hanya terjadi setelah seluruh syarat terpenuhi.

Mengapa Bukan Sekadar Field public #

Kita bisa saja meniadakan getter dan setter lalu membiarkan semua field terbuka, dan program kecil masih akan tampak berjalan normal. Encapsulation justru membayar dirinya ketika program membesar. Pertama, satu titik validasi: aturan “jumlah harus positif” ditulis sekali di setor, bukan diulang di ratusan pemanggil. Kedua, kebebasan evolusi internal: kita dapat mengubah representasi saldo dari long menjadi BigDecimal tanpa merusak satu pun pemanggil, karena tidak ada yang menyentuh field langsung. Ketiga, akuntabilitas: bila saldo mencurigakan, satu class saja yang perlu diperiksa.

Penolakan compiler juga bekerja di arah yang membantu. Kode luar yang mencoba rekening.saldo ditolak dengan pesan saldo has private access in RekeningBank — pelanggaran kebijakan tertangkap sebelum program sempat berjalan. Java menyediakan empat tingkat pembatasan akses (private hingga public) yang tidak hanya berlaku untuk field, tetapi juga untuk method, constructor, bahkan class itu sendiri. Peta lengkap keempat tingkat tersebut adalah pokok bahasan Chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

AplikasiBank.java

Unduh berkas
java
public class AplikasiBank {
    public static void main(String[] args) {
        RekeningBank rekening = new RekeningBank("Budi Santoso", 500000);

        System.out.println("Pemilik : " + rekening.getPemilik());
        System.out.println("Saldo   : " + rekening.getSaldo());

        rekening.setor(250000);
        rekening.setor(-100);
        rekening.tarik(1000000);
        rekening.tarik(100000);

        System.out.println("Saldo akhir: " + rekening.getSaldo());
    }
}

RekeningBank.java

Unduh berkas
java
class RekeningBank {
    private String pemilik;
    private long saldo;

    RekeningBank(String pemilik, long saldoAwal) {
        this.pemilik = pemilik;
        if (saldoAwal < 0) {
            saldoAwal = 0;
        }
        this.saldo = saldoAwal;
    }

    String getPemilik() {
        return pemilik;
    }

    long getSaldo() {
        return saldo;
    }

    void setor(long jumlah) {
        if (jumlah <= 0) {
            System.out.println("Setor " + jumlah + " ditolak: jumlah harus positif");
            return;
        }
        saldo = saldo + jumlah;
        System.out.println("Setor " + jumlah + " berhasil, saldo menjadi " + saldo);
    }

    void tarik(long jumlah) {
        if (jumlah <= 0) {
            System.out.println("Tarik " + jumlah + " ditolak: jumlah harus positif");
            return;
        }
        if (jumlah > saldo) {
            System.out.println("Tarik " + jumlah + " ditolak: saldo tidak cukup");
            return;
        }
        saldo = saldo - jumlah;
        System.out.println("Tarik " + jumlah + " berhasil, saldo menjadi " + saldo);
    }
}