Getter dan setter lahir dari kebiasaan yang baik, yaitu menutup field, tetapi kebiasaan itu sering berhenti di tengah jalan ketika setiap field langsung diberi pasangan metode ambil dan isi tanpa dipikirkan ulang. Topik ini menempatkan invarian (invariant) sebagai penentu keputusan, yaitu syarat yang wajib benar pada setiap keadaan objek. Kasus yang dipakai adalah kelas Rekening dengan aturan saldo tidak boleh negatif, ditelusuri dari field terbuka, setter berpemeriksaan, sampai operasi yang benar-benar menjaga aturan.
Invarian dan bahaya field terbuka#
Invarian adalah kondisi yang harus selalu terpenuhi agar objek dianggap sah, misalnya saldo rekening tidak boleh negatif, stok barang tidak boleh kurang dari nol, dan tanggal tidak boleh berada di luar rentang bulannya. Invarian tidak tertulis di tipe data, sehingga penegaknya harus berupa kode. Field yang terbuka tidak menyediakan satu titik pun untuk menegakkan apa pun, dan setiap penugasan langsung berpotensi merusak kondisi tersebut.
public class UjiSaldo {
public static void main(String[] args) {
Rekening rekening = new Rekening();
rekening.nomor = "201-04-771";
rekening.saldo = -50000;
System.out.println("Nomor : " + rekening.nomor);
System.out.println("Saldo : " + rekening.saldo);
}
}
class Rekening {
String nomor;
double saldo;
}Nomor : 201-04-771
Saldo : -50000.0Objek Rekening di atas kini menyimpan saldo negatif, dan tidak ada satu baris kode dalam kelas itu yang bisa dipersalahkan karena memang tidak ada yang bertugas menjaga aturan. Setiap pembaca field harus menerima kemungkinan nilai mustahil, dan setiap perbaikan menuntut perburuan ke seluruh kode yang menulis ke field tersebut. Titik kendali tunggal adalah jawabannya, dan titik itu paling sering diwujudkan sebagai setter.
Setter dengan pemeriksaan dan batasnya#
Setter yang menyertakan pemeriksaan memindahkan penjagaan ke dalam kelas. Versi berikut menolak nilai negatif dengan cara mengabaikannya, sehingga saldo tetap 0.0 ketika nilai mustahil dikirim.
public class UjiSetSaldo {
public static void main(String[] args) {
Rekening rekening = new Rekening();
rekening.setSaldo(-50000);
System.out.println("Setelah percobaan pertama : " + rekening.getSaldo());
rekening.setSaldo(250000);
System.out.println("Setelah percobaan kedua : " + rekening.getSaldo());
}
}
class Rekening {
private double saldo;
double getSaldo() {
return saldo;
}
void setSaldo(double nilaiBaru) {
if (nilaiBaru >= 0) {
saldo = nilaiBaru;
}
}
}Setelah percobaan pertama : 0.0
Setelah percobaan kedua : 250000.0Invarian tidak lagi dilanggar, tetapi desain ini masih menyisakan dua kelemahan yang nyata. Pertama, penolakan bersifat diam, sehingga pemanggil yang mengirim -50000 tidak diberi tahu bahwa operasinya gagal dan saldo yang terbaca 0.0 bisa disalahartikan sebagai saldo sungguhan. Kedua, setSaldo adalah pintu yang terlalu umum, karena pemanggil bebas menimpa saldo dengan angka berapa pun yang lolos pemeriksaan, kendati di dunia nyata tidak ada nasabah yang mengganti saldo secara langsung. Yang benar-benar terjadi adalah penyetoran dan penarikan, dan operasi itulah yang seharusnya menjadi pintu.
Melindungi invarian lewat operasi#
Versi Rekening berikut menutup field sepenuhnya, lalu membuka operasi yang bermakna. Tidak ada setSaldo, karena mengganti saldo bukan operasi yang wajar. Penyetoran memeriksa jumlah masuk, penarikan mengembalikan boolean sebagai laporan keberhasilan, dan kedua operasi itu satu-satunya jalur yang bisa mengubah saldo.
public class Rekening {
private String nomor;
private double saldo;
Rekening(String nomor) {
this.nomor = nomor;
}
public String getNomor() {
return nomor;
}
public double getSaldo() {
return saldo;
}
public void setor(double jumlah) {
if (jumlah > 0) {
saldo = saldo + jumlah;
}
}
public boolean tarik(double jumlah) {
if (jumlah > 0 && saldo >= jumlah) {
saldo = saldo - jumlah;
return true;
}
return false;
}
public static void main(String[] args) {
Rekening rekening = new Rekening("201-04-771");
rekening.setor(500000);
boolean cukup = rekening.tarik(150000);
System.out.println("Penarikan 150000 berhasil : " + cukup);
boolean ditolak = rekening.tarik(600000);
System.out.println("Penarikan 600000 berhasil : " + ditolak);
System.out.println("Saldo akhir : " + rekening.getSaldo());
}
}Penarikan 150000 berhasil : true
Penarikan 600000 berhasil : false
Saldo akhir : 350000.0Percobaan menarik 600000 ditolak karena melewati saldo, dan invarian saldo tidak negatif kini tak mungkin dilanggar dari luar kelas. Getter tetap ada untuk nomor dan saldo karena membaca nilai tidak merusak apa pun, sedangkan penulisan hanya lewat setor dan tarik. Perhatikan pula bahwa nilai kembalian tarik berfungsi sebagai laporan, sehingga penolakan tidak pernah berlalu diam-diam selama pemanggil menampung dan memeriksanya.
Memilih pintu yang dibuka#
Getter umumnya lebih aman daripada setter karena membaca tidak mengubah keadaan, tetapi getter juga bisa bocor bila yang dikembalikan adalah struktur yang bisa diubah. Getter yang mengembalikan array internal secara langsung, misalnya, tetap memungkinkan pihak luar menimpa elemennya tanpa melewati metode apa pun, sehingga penjagaan di dalam kelas terlewati begitu saja. Keluarga tipe yang aman dikembalikan apa adanya antara lain String, angka primitif, dan boolean karena isinya tidak dapat dimodifikasi.
Cara menimbangnya sederhana. Hitung pintu yang dibuka, lalu pertanyaan untuk setiap pintunya adalah apa yang terjadi bila isi field berganti struktur di kemudian hari. Getter getSaldo() bertipe double dapat dipertahankan meskipun saldo kelak berganti menjadi objek uang dengan mata uang, sedangkan pintu yang mengekspos penyimpanan mentah akan memaksa perubahan menyebar ke semua pemanggil. Semakin sedikit pintu, semakin bebas detail internal berubah.
Kesalahan umum#
- Membuat getter dan setter untuk semua field sekaligus lewat alat bantu IDE. Langkah itu hanya menamai ulang akses langsung tanpa menegakkan satu invarian pun. Tinjau setiap field, lalu buka pintu yang benar-benar dibutuhkan saja.
- Mengabaikan nilai kembalian
booleandari operasi sepertitarik. Penolakan berlalu tanpa diketahui, dan keadaan yang dianggap sudah berubah ternyata tetap. Tampung hasilnya dan tangani kasus gagalnya secara eksplisit. - Menolak nilai tidak sah dengan hanya mencetak pesan. Pencetakan tidak menghentikan alur program, sehingga operasi yang gagal tetap dianggap berhasil oleh pemanggil. Gunakan nilai kembalian atau lempar
IllegalArgumentException. - Mengembalikan array atau objek internal yang dapat diubah dari getter. Pihak luar dapat mengubahnya tanpa melewati penjagaan. Kembalikan salinan atau sediakan metode terbatas.
Ringkasan#
- Invarian adalah syarat yang wajib benar pada setiap keadaan objek, dan field terbuka tidak menyediakan titik mana pun untuk menegakkannya.
- Setter berpemeriksaan lebih baik daripada field terbuka, tetapi penolakan yang diam dan pintu yang terlalu umum tetap menjadi kelemahannya.
- Operasi bermakna seperti
setordantarikmenjaga invarian sekaligus menyatakan maksud pemanggil, sedangkan getter cukup untuk kebutuhan membaca. - Getter yang mengembalikan struktur dapat diubah tetap membocorkan kendali, sehingga pilihannya adalah salinan atau operasi terbatas.
Referensi#
- Returning a Value from a Method — Oracle — nilai kembalian sebagai jalur pelaporan hasil operasi kepada pemanggil.
- Controlling Access to Members of a Class — Oracle
— dasar menyembunyikan field dengan
privatesebelum membuka metode tertentu.