Mutable Objects Are Demons😈
Wed. Oct 14th, 2020 05:37 PM7 mins read
Mutable Objects Are Demons😈
Source: HuggingFace@TonyAssi - scary demon in chibi character

Mutable Objects ini sebenarnya masih turunan masalah dari Global Variable. Mutable Object adalah objek yang state-nya bisa berubah setelah objeknya dibuat. Sama seperti Global Variables, Mutable Objects memberikan keleluasaan untuk mengganti nilai state-nya kapan saja dan dimana saja yang bisa disalahgunakan. Penyalahgunaan tersebut secara teknis ga bisa dicegah, kecuali menggunakan Immutable Objects. Biasanya mindset developer secara default hampir selalu menggunakan Mutable Object saat melakukan design Class. Masalah yang sering terjadi akibat Mutable Objects mirip seperti pada Global Variables, yaitu gampang ditulis tapi susah di-maintain. Selain itu Mutable Objects juga perlu diperhatikan pada Concurrent Programming karena Threads diberi kebebasan untuk saling melakukan mutasi yang dapat berakibat Race Conditions. James Gosling aja selaku founder Java pernah bilang "I use immutables whenever I could". Beberapa objek di Java yang masih mutable sekarang sudah mulai Deprecated satu-persatu. Seperti Date dan Calendar yang merupakan mutable objek sekarang di Java versi terbaru sudah tidak dianjurkan lagi untuk dipakai. Date dan Calendar diganti dengan objek-objek Java time API yang lebih spesifik seperti LocalDate, LocalDateTime, ZonedDateTime, YearMonth, LocalTime, dan lainnya yang sudah immutable. Java mengadopsi objek-objek tersebut dari JodaTime sejak perilisan Java 8 beberapa tahun yang lalu. IntelliJ sendiri juga mulai memberi warning ketika ada code yang masih menggunakan Date atau Calendar.

Why Mutable Objects Are Demons?

Mutable Objects disebut bad design karena kebebasan untuk mengubah-ubah state dari Object tersebut setelah objek dibuat. Sekilas mungkin terlihat bagus dan simple sehingga mudah digunakan berkat kebebasan tersebut. Kebebasan tersebutlah yang membuat code sulit untuk di-maintain, apalagi yang me-maintain adalah bukan orang yang bikin code tersebut dan cenderung menjerumuskan.

Misalkan pada contoh kasus berikut (sorry kalau skenarionya awkward, yang penting poinnya dapet😅):

  1. Cari karyawan berdasarkan nama;
  2. Set nama pada response sesuai nama karyawan;
  3. Set status pada response tersebut menjadi:
    • "resigned" jika sudah tidak aktif;
    • "present" jika masih aktif;
  4. Set deskripsi pada response menjadi "employee is already gone" jika sudah tidak aktif;
  5. Return response;

Berikut contoh source code menggunakan Mutable Object:

public class EmployeeResponse{

	private String status;
	private String name;
	private String description;

	public String getStatus(){
		return this.status;
	}

	public String getName(){
		return this.name;
	}

	public String getDescription(){
		return this.description;
	}

	public void setStatus(String status){
		this.status = status;
	}

	public void setName(String name){
		this.name = name;
	}

	public void setDescription(String description){
		this.description = description;
	}
}

Berikut Processor Class-nya:

public class EmployeeProcessor{
	private final EmployeeRepo repo;

	public EmployeeProcessor(EmployeeRepo repo){
		this.repo = repo;
	}

	public EmployeeResponse proceed(String name){
		EmployeeResponse response = new EmployeeResponse();
		Employee employee = repo.getEmployeeByName(name);
		setResponseEmployeeStatus(response, employee);

		response.setName(employee.getName());
		List<Integer> blackListIds = repo.getBlackListIds();
		if(!employee.isActive()){
			response.setDescription("employee is already gone");
		}
		return response;
	}

	private void setResponseEmployeeStatus(EmployeeResponse response, Employee employee){
		if(!employee.isActive()){
			response.setStatus("resigned");
		} else {
			response.setStatus("present");
		}
	}

}

Pada code di atas, antara Mutator (setter) dan Object Initialization (new keyword) dapat dibuat terpisah. Developer diberi kebebasan untuk memutasi objek dimanapun. Object EmployeeResponse bisa saja dibuat terpisah jauh dari Mutatornya. Anggap saja code tersebut dibikin oleh engineer bernama Eko dulunya. Kemudian terjadi improvement seperti berikut:

  1. Cari karyawan berdasarkan nama;
  2. Set nama pada response sesuai nama karyawan;
  3. Set status pada response tersebut menjadi:
    • "resigned" jika sudah tidak aktif;
    • "present" jika masih aktif;
    • "blocked" jika terdapat dalam status blacklist;
    • "retired" jika karyawan tersebtu sudah pensiun;
  4. Set deskripsi pada response menjadi "employee is already gone" jika sudah tidak aktif;
  5. Return response;

Engineer yang bertugas melakukan improvement adalah Parto, karena Eko sudah resign duluan. Berdasarkan code yang sudah dibuat oleh Eko sebelumnya, Parto melakukan perubahan seperti berikut pada Processor Class:

public class EmployeeProcessor{
	private final EmployeeRepo repo;

	public EmployeeProcessor(EmployeeRepo repo){
		this.repo = repo;
	}

	public EmployeeResponse proceed(String name){
		EmployeeResponse response = new EmployeeResponse();
		Employee employee = repo.getEmployeeByName(name);
		setResponseEmployeeStatus(response, employee);

		response.setName(employee.getName());
		List<Integer> blackListIds = repo.getBlackListIds();
		setAnotherResponseEmployeeStatus(response, employee, blackListIds);
		if(!employee.isActive()){
			response.setDescription("employee is already gone");
		}
		return response;
	}

	private void setResponseEmployeeStatus(EmployeeResponse response, Employee employee){
		if(!employee.isActive()){
			response.setStatus("resigned");
		} else {
			response.setStatus("present");
		}
	}

	private void setAnotherResponseEmployeeStatus(EmployeeResponse response, Employee employee, List<Integer> blackListIds){
		if(blackListIds.contains(employee.getId())){
			response.setStatus("blocked");
		}
		if(employee.isRetired()){
			response.setStatus("retired");
		}
	}

}

Parto juga bebas melakukan mutasi object sama seperti yang dilakukan Eko. Karena mutable, bisa aja dong si Parto memutasinya di sembarang tempat. Lalu Parto juga ikut resign. Ternyata terdapat sebuah bugs, status karyawan yang di-blacklist harusnya tetap "blocked" meskipun sudah pensiun. Tugas tersebut di-assign ke Akri, engineer yang baru join. Disinilah masalah muncul, Akri yang masih baru akan kesulitan untuk memahami code tersebut. Akri "dipaksa" untuk menganalisa keseluruhan code dari A sampai Z hanya untuk melakukan bug fixing pada status employee.

Masalah seperti ini tidak akan terjadi jika seandainya Eko dan Parto menggunakan Immutable Object pada EmployeeResponse. Code-nya akan jadi seperti ini:

public final class EmployeeResponse{

	private final String status;
	private final String name;
	private final String description;

	public EmployeeResponse(String status, String name, String description){
		this.status = status;
		this.name = name;
		this.description = description;
	}

	public String getStatus(){
		return this.status;
	}

	public String getName(){
		return this.name;
	}

	public String getDescription(){
		return this.description;
	}

}

Processor-nya otomatis pasti akan menjadi seperti ini:

public class EmployeeProcessor{
	private final EmployeeRepo repo;

	public EmployeeProcessor(EmployeeRepo repo){
		this.repo = repo;
	}

	public EmployeeResponse proceed(String name){
		Employee employee = repo.getEmployeeByName(name);
		List<Integer> blackListIds = repo.getBlackListIds();

		String description = employee.isActive() ? null : "employee is already resigned";
		String status = getStatus(employee, blackListIds);

		return new EmployeeResponse(status, name, description);
	}

	private String getStatus(Employee employee, List<Integer> blackListIds){
		if(blackListIds.contains(employee.getId())){
			return "blocked";
		} else if(employee.isRetired()){
			return  "retired";
		} else if(!employee.isActive()){
			return "resigned";
		}
		return "present";
	}
}

Dengan code seperti di atas, mustahil bagi Eko dan Parto untuk melakukan mutasi sembarangan. Proses pembuatan objek dan pengisian nilai pun hanya bisa dilakukan pada satu tempat, ga akan bisa terpisah-pisah seperti code sebelumnya. Selain itu code di atas juga akan ter-guided untuk menerapkan Single Responsibility Principle untuk mendapatkan masing-masing nilainya. Jadi kalau masih ada tambahan kondisi lagi dalam menentukan nilai, cukup di satu tempat saja dan lebih aman dari bugs yang tidak perlu. Kalau misalkan ada bug atau perubahan lagi, Akri tinggal menuju method yang bertanggung-jawab mengembalikan nilai tersebut untuk menelusurinya daripada harus membaca keseluruhan code.

Builder Design Pattern

Pada kasus di atas sebenarnya menimbulkan masalah baru. Kita harus menghafal semua urutan parameter pada constructor dan rentan salah assign nilai. Kalau tiga doang masih mending, kalau udah sepuluh parameter pusing kepala pastinya🤯. Tapi tenang, masalah tersebut bisa diselesaikan dengan Builder Design Pattern.

Code-nya jadi seperti berikut:

public final class EmployeeResponse{

	private final String status;
	private final String name;
	private final String description;

	EmployeeResponse(String status, String name, String description){
		this.status = status;
		this.name = name;
		this.description = description;
	}

	public static EmployeeResponseBuilder builder(){
		return new EmployeeResponseBuilder();
	}

	public String getStatus(){
		return this.status;
	}

	public String getName(){
		return this.name;
	}

	public String getDescription(){
		return this.description;
	}

	public static class EmployeeResponseBuilder{
		private String status;
		private String name;
		private String description;

		EmployeeResponseBuilder(){
		}

		public EmployeeResponse.EmployeeResponseBuilder status(String status){
			this.status = status;
			return this;
		}

		public EmployeeResponse.EmployeeResponseBuilder name(String name){
			this.name = name;
			return this;
		}

		public EmployeeResponse.EmployeeResponseBuilder description(String description){
			this.description = description;
			return this;
		}

		public EmployeeResponse build(){
			return new EmployeeResponse(status, name, description);
		}

	}
}

Pada Processor code-nya akan seperti ini:

public class EmployeeProcessor{
	private final EmployeeRepo repo;

	public EmployeeProcessor(EmployeeRepo repo){
		this.repo = repo;
	}

	public EmployeeResponse proceed(String name){
		Employee employee = repo.getEmployeeByName(name);
		List<Integer> blackListIds = repo.getBlackListIds();

		String description = employee.isActive() ? null : "employee is already resigned";
		String status = getStatus(employee, blackListIds);

		return EmployeeResponse.builder()
				.description(description)
				.name(name)
				.status(status)
				.build();
	}

	private String getStatus(Employee employee, List<Integer> blackListIds){
		if(blackListIds.contains(employee.getId())){
			return "blocked";
		} else if(employee.isRetired()){
			return  "retired";
		} else if(!employee.isActive()){
			return "resigned";
		}
		return "present";
	}
}

Project Lombok the Savior

Code di atas sebenarnya masih menimbulkan masalah baru, code-nya jadi verbose😤. Tapi tenang, masih ada solusinya, yaitu dengan menambahkan 3rd Party Library, yaitu Project Lombok. Library ini cukup populer untuk memangkas verbosity pada Java. Code yang verbose tadi akan di-generate otomatis oleh Lombok saat compile sesuai annotasi yang diberikan. Secara teknis Java ga bisa memodifikasi Class menggunakan annotasi, tapi Project Lombok bisa melakukan itu karena dibuat menggunakan Scala (sepupu Java, sama-sama menggunakan JVM).

Code-nya akan jadi seperti ini:

@Builder
@Value
public class EmployeeResponse{

	String status;
	String name;
	String description;
}

Dengan begini code-nya jadi lebih singkat tanpa boilerplate🤩.

Immutable Collection

Gw pernah post juga tentang optimasi code sebelumnya. Di sana gw juga ada membahas tentang Immutable Collection. Gw menggunakan method-method seperti Collections.emptyList(), Collections.emptySet(), Collections.emptyMap(), Collections.singleton(), Collections.singletonMap(), dan Collections.singletonList() sebagai contoh. Sama seperti Object, Collection juga seringkali dibikin Mutable dan elemennya bebas diganti di mana saja meskipun sudah dikasih 'final' keyword. Walaupun ini ga dianjurkan sih dan jarang juga dilakukan, tapi dengan Mutable Collection tentu saja hal ini tidak dapat dicegah. Solusinya dengan Immutable Collection. Biasanya orang-orang menggunakan Arrays.asList(...) dalam menggunakan constant List. Memang sih, Arrays.asList(...) tidak dapat menambahkan elemen baru, tapi itu dapat melakukan perubahan elemen. Makanya ga bisa disebut Immutable juga, Semi-Mutable mungkin lebih tepatnya. Untuk saat ini satu-satunya cara menggunakan Immutable Collection adalah dengan membungkus Mutable Collection dengan Collections.unmodifiableCollection(), Collections.unmodifiableList(), Collections.unmodifiableSet(), atau Collections.unmodifiableMap() pada akhir statement.

Contohnya seperti berikut:

private List<Integer> getBlackListIds(){
	List<Integer> blackListIds = repo.getBlackListIds();
	return Collections.unmodifiableList(blackListIds);
}

Dengan begitu akan terjadi error setiap melakukan mutasi elemen pada Collection.

There's Always an Exception

Selalu ada pengecualian pada setiap best practice. Salah satunya adalah Objek yang digunakan sebagai Entity yang berhubungan dengan Database. Walaupun ada juga sih yang berpendapat untuk membuat ulang objek tersebut ketika melakukan update data. Tapi buat gw sih, ini bisa dijadikan pengecualian dengan syarat mutasi hanya dilakukan pada satu scope method. Debatable sih🤔.

Summary

Secara umum Mutable Objects sebisa mungkin dihindari walaupun ada pengecualian seperti untuk Entity. Secara pribadi gw udah menerapkan Immutable Objects pada setiap aktivitas per-coding-an sehari-hari. Memang menurut gw guide seperti ini benefitnya cukup bagus dan membantu code jadi lebih bersih. Engineer yang bikin maupun yang akan melakukan maintain akan ter-guided dengan sendirinya untuk membuat code yang lebih bersih tanpa harus diberi tahu lewat code review. Oh ya, karena code dari Lombok di-generate saat compile, jadi ga akan masalah pada saat runtime. Code-nya masih sama seperti code yang dibuat manual. Ga akan ada penalti waktu saat runtime. Penalti waktu hanya terjadi saat compile. Selain Lombok, Builder Pattern juga bisa di-generate menggunakan plugin yang tersedia di marketplace-nya IDE. Kalau menurut gw sih, mending kena penalti waktu saat compile sih daripada bikin sakit kepala saat development. Oleh karena itu, penggunaan Mutable Objects dapat membuat bug-fixer kesetanan😈.

© 2024 · Ferry Suhandri