
Encapsulation merupakan salah satu dari 4 pondasi utama pada OOP selain Abstraction untuk struktur behavior yang konsisten, Inheritance untuk pewarisan behavior, dan Polymorphism untuk fleksibilitas dalam penggunaan varian yang berbeda. Semuanya merupakan topik yang mainstream di internet. Tapi gw liat di beberapa sosial media hal-hal terkait konsep ini masih sering ditanyakan. Beberapa juga masih banyak yang kebalik terkait Access Modifier dan Access Level. Kayaknya menarik juga untuk gw bahas di blog. 3 di antaranya udah sering gw bahas di beberapa post, meskipun ga secara eksklusif seperti pada tulisan tentang Programming to Interface dan Pilih Inheritance atau Composition. Jadi kali ini gw hanya akan fokus membahas Encapsulation saja. Encapsulation yaitu membungkus data atau function ke dalam sebuah unit. Encapsulation tersebut Access Levelnya bisa dikontrol menggunakan 3 Access Modifier.
Encapsulation
Misalkan kita punya class untuk bikin minuman. Behaviornya di antaranya adalah putBoilWater()
, putSugar()
, putTeaBag()
, putIce()
, dan putCoffee()
.
public class DrinkMaker{
public void putBoilWater(){
System.out.println("Put boil water");
}
public void putSugar(){
System.out.println("Put sugar");
}
public void putTeaBag(){
System.out.println("Put tea bag");
}
public void putIce(){
System.out.println("Put ice");
}
public void putCoffee(){
System.out.println("Put coffee");
}
}
Dengan class di atas, jika ada orang yang ingin bikin kopi, maka mereka perlu memilih behavior mana aja yang dibutuhkan. Begitu juga kalau ingin bikin teh, mereka juga perlu memilih sendiri behaviornya. Ini ribet kalau harus milih satu-persatu. Dengan Encapsulation yang baik, kita bisa membungkus beberapa behavior menjadi beberapa public method yang diperlukan saja, seperti servePlainIceTea()
, serveSweetHotTea()
, dan serveHotCoffee()
. Sisanya dijadikan private method. Hasilnya seperti ini:
public class DrinkMaker{
private void putBoilWater(){
System.out.println("Put boil water");
}
private void putSugar(){
System.out.println("Put sugar");
}
private void putTeaBag(){
System.out.println("Put tea bag");
}
private void putIce(){
System.out.println("Put ice");
}
private void putCoffee(){
System.out.println("Put coffee");
}
public void serveHotCoffee(){
putSugar();
putCoffee();
putBoilWater();
}
public void servePlainIceTea(){
putTeaBag();
putIce();
putBoilWater();
}
public void serveSweetHotTea(){
putSugar();
putTeaBag();
putBoilWater();
}
}
Dengan begini, kalau mau bikin teh panas yang manis tinggal panggil method serveSweetHotTea()
, kalau mau bikin teh tawar dingin tinggal panggil servePlainIceTea()
, begitu juga saat mau bikin kopi tinggal panggil serveHotCoffee()
. Ga perlu atur behaviornya satu-persatu seperti sebelumnya.
Access Level
Encapsulation bisa dikontrol Access Levelnya menggunakan Access Modifier. Terdapat 4 Access Level yang bisa kita atur menggunakan 3 keyword Access Modifier:
private
. Menggunakan Access Modifierprivate
artinya unit tersebut hanya bisa diakses oleh unit member itu sendiri;protected
. Menggunakan Access Modifierprotected
artinya unit tersebut hanya bisa diakses oleh unit member itu sendiri beserta turunan dari unit tersebut, dan unit dari package yang sama;public
. Menggunakan Access Modifierpublic
artinya unit tersebut bisa diakses oleh unit member mana pun;- Jika tidak menuliskan Access Modifier apa pun pada unit, artinya unit tersebut hanya bisa diakses oleh unit member itu sendiri beserta unit dari package yang sama. Ini adalah default Access Level yang juga sering disebut sebagai Access Level package-private;
Kenapa menggunakan Encapsulation dan Access Level?
Dengan Encapsulation kita bisa menyederhanakan code untuk melakukan sesuatu yang diperlukan saja. Seperti pada code di atas, orang yang ingin bikin kopi tinggal panggil method serveHotCoffee()
saja tanpa perlu repot-repot ngurusin internal behaviornya. Yang penting kopinya jadi setelah panggil method tersebut. Dengan Access Level kita bisa menyembunyikan detail internal yang ga dibutuhkan. Ini juga sering jadi pertanyaan, “kenapa ga semuanya dibikin public
aja?”. Misalkan semuanya dibikin public
, tentu akan membingungkan orang yang ingin menggunakan class tersebut karena akan banyak muncul pilihan yang sebenarnya ga dibutuhkan. Method seperti putSugar()
sendiri tidak ada artinya jika dieksekusi terpisah di luar class DrinkMaker
, ga akan menghasilkan apa-apa. Mubazir dong😕. Apalagi kalau misalkan internal behavior itu mengandung hal-hal sensitif yang bisa berdampak menjadi bugs jika diakses sembarangan dari luar😱. Makanya best practice-nya adalah menggunakan Access Modifier private
by default, kecuali benar-benar dibutuhkan untuk diakses dari luar unit. Access Levelnya dinaikkan secara bertahap jika memang dibutuhkan di luar unit. Jangan tulis Access Modifier (Access Level package-private) jika juga dibutuhkan untuk diakses oleh unit di package yang sama. Gunakan Access Modifier protected
jika juga dibutuhkan untuk diakses oleh unit turunannya. Pilihan terakhir, jika itu memang dibutuhkan secara luas baru lah gunakan Access Modifier public
.
Encapsulation & Access Level pada Class Data
Hal-hal terkait Access Level yang sering dipertanyakan adalah tentang Accessor dan Mutator pada Class Data. Seringkali kita melihat pada sebuah class yang menyimpan data doang, kita menggunakan Getter sebagai Accessor dan Setter sebagai Mutator dengan Access Modifier public. Sedangkan property pada class tersebut menggunakan Access Modifier private. Pendekatan ini sebenarnya konvensi pada OOP, di mana akses terhadap global variable harus diminimalkan kecuali constant. Sehingga untuk mengakses variable property tersebut harus lewat Accessor dan Mutator. Selain itu, dengan Accessor kita bisa mengontrol data yang ingin ditampilkan. Misalkan data tersebut berupa List yang dapat dimutasi, maka kita bisa menggunakan immutable List pada Accessor saat diakses agar data List yang diambil tidak dapat diubah oleh unit yang mengaksesnya lewat Accessor. Ini dapat meminimalkan bugs yang terjadi akibat mutasi data di sembarang tempat. Misalkan pada class Student terdapat property list skills di dalamnya. Jadi, nanti setelah objek Student dibuat beserta skillnya, daftar skillnya ga bisa diubah lagi lewat objek Student.
public class Student{
private final List<String> skills;
public Student(List<String> skills){
this.skills = skills;
}
public List<String> getSkills(){
return Collections.unmodifiableList(this.skills);
}
}
Begitu juga dengan Mutator, kita bisa mengontrol data yang ingin di-set jika terjadi maintenance. Misalkan sebelumnya kita menggunakan Date untuk menyimpan tanggal lahir. Itu sudah dipakai di banyak tempat. Lalu kita ingin mengganti itu menggunakan LocalDate. Kita bisa melakukan proses transisi secara perlahan dengan melakukan mutasi pada tipe yang baru menggunakan LocalDate pada Mutator Date agar saat Mutator pada Date diakses, dia juga ikut memutasi property pada LocalDate. Jadi meskipun code legacy masih menggunakan Mutator pada Date, dia juga akan mengganti value pada LocalDate. Kita bisa tambahkan anotasi @Deprecated
pada Mutator dan Accessor Date sebagai warning agar ke depannya ga ada lagi yang menggunakan itu dan beralih pada LocalDate.
public class Student{
private Date birthDay;
private LocalDate birthDate;
@Deprecated
public Date getBirthDay(){
return birthDay;
}
@Deprecated
public void setBirthDay(Date birthDay){
this.birthDay = birthDay;
this.birthDate = birthDay.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
public LocalDate getBirthDate(){
return birthDate;
}
public void setBirthDate(LocalDate birthDate){
this.birthDate = birthDate;
}
}
Kendala yang sering dialami saat menggunakan Accessor dan Mutator adalah kita harus generate sendiri secara manual untuk masing-masing property sehingga cukup ribet untuk hal sederhana😕. Untungnya pada IDE biasanya udah ada tools untuk generate Accessor dan Mutator sehingga lebih gampang. Atau kalau mau lebih simple dan clean, bisa menggunakan plugin Lombok. Bahkan untuk Java modern kita malah ga perlu generate lagi dan tanpa Lombok karena bisa pake Record. Dengan Record, di dalamnya sudah otomatis include Accessor beserta kalkulasi hash code. Bedanya dengan class biasa, Record ini ga ada Mutator. Jika ingin memodifikasi Accessor kita bisa override sendiri.
public record Student(String name, LocalDate birthDate, List<String> skills){
@Override
public List<String> skills(){
return Collections.unmodifiableList(skills);
}
}
Verdict
Dengan Encapsulation dan Access Level kita bisa menyederhanakan code dan membatasi akses agar hanya yang diperlukan saja yang bisa diakses pada suatu unit tanpa perlu menelanjangi behavior internal ke luar. Terdapat 3 keyword Access Modifier untuk mengontrol Access Level, yaitu public
, protected
, dan private
. Jika Access Modifier tidak ditulis, maka Access Level defaultnya adalah package-private. Best practice-nya, Access Level dikontrol secara bertahap. Gunakan Access Modifier dari yang paling strict seperti private
, dan naikkan Access Levelnya secara bertahap jika memang dibutuhkan kontrol yang lebih luas. Access Level dan Encapsulation juga sering diaplikasikan pada Accessor dan Mutator Class Data untuk mempermudah maintenance.