Global Variables Are Evil👿
Mon. Oct 12th, 2020 09:44 PM6 mins read
Global Variables Are Evil👿
Source: HuggingFace@TonyAssi - creepy face

Kita sering mendengar bahwa code yang baik adalah yang gampang dibaca, gampang dimengerti, dan gampang dijaga. Object Oriented Programming (OOP) adalah salah satu orientasi pemrograman populer yang dapat memenuhi kriteria tersebut, IMHO. Namun, sebelum OOP booming, orang-orang terlebih dulu menggunakan pendekatan prosedural seperti pada bahasa C. Kebiasaan saat menggunakan pendekatan prosedural inilah yang sering terbawa-bawa saat melakukan coding menggunakan bahasa-bahasa OOP. Beberapa pendekatan prosedural pada OOP ga selamanya buruk, seperti penggunaan static method pada Utility Classes untuk perintah-perintah pure function. Walaupun ada juga yang memanfaatkan static method tersebut untuk melakukan perintah non-pure function yang menggunakan dependency luar, dan tentu ini akan menyulitkan Unit Testing. Tapi untuk sekedar pure function tanpa dependency luar, static method masih worth it lah walau debatable. Hmm... jadi out of topic nih😅.

Why Global Variables Are Evil?

Kembali ke topik, kebiasaan ngoding secara prosedural ini sering terbawa saat ngoding OOP dengan menggunakan global variables. Ok, di satu sisi mungkin orang-orang beranggapan menggunakan global variables itu sangat gampang digunakan. Ya, gw juga setuju, tapi saking gampangnya malah jadi sulit di-maintain🙈. Global variable yang buruk adalah ketika sebuah variable dapat diakses dan diubah oleh berbagai method kapanpun dan dimanapun. Hal ini dapat membuat code jadi rentan terhadap bugs dan akan sangat menyulitkan saat melakukan debugging. Apalagi yang paling parah ketika terjadi race condition (berpacu dalam perubahan state objek) ketika objek tersebut diakses secara asynchronous.

Langsung ke contoh kasus aja untuk lebih jelasnya. Misalkan kasusnya seperti ini:

  1. Get data employee berdasarkan nama;
  2. Jika employee tersebut masuk daftar blacklist:
    • Set Response status menjadi "blocked";
    • Set Response description berdasarkan alasan blacklist;
  3. Jika employee tersebut sudah wafat:
    • Set response status menjadi "dead";
    • Description tidak perlu diisi;
  4. Jika employee tersebut bukan termasuk blacklist dan masih hidup:
    • Set response status menjadi "present";
    • Description tidak perlu diisi;
  5. Set Response name dengan nama employee;

Berikut contoh source code mengguanakan global variable:

public class EmployeeProcessor{
	private final EmployeeRepo repo;

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

	private String status;
	private String description;

	public void processEmployeByName(String name){
		status = "present";
		Employee employee = repo.getEmployeeByName(name);
		//...
		//any other codes...
		//...
		blacklistCheck(employee);
		//...
		//any other codes...
		//...
		deadCheck(employee);
		//...
		//any other codes...
		//...

		Response response = Response.builder()
				.name(name)
				.status(status)
				.description(description)
				.build();
		System.out.println("response = " + response);
	}

	private void deadCheck(Employee employee){
		if(employee.isDead()){
			status = "retired";
		}
	}

	private void blacklistCheck(Employee employee){
		List<Integer> blackListIds = repo.getBlackListIds();

		if(blackListIds.contains(employee.getId())){
			description = repo.getMistakeById(employee.getId());
			status = "blocked";
		}
	}
}

Code di atas dibuat secara minimalis untuk menghemat artikel agar fokus ke inti masalah. Imajinasikan saja itu sebuah use case yang cukup kompleks😉. Sekilas mungkin penggunaan global terlihat lebih simple, mudah untuk ditulis. Anggap saja code di atas dibuat oleh engineer bernama Dono pada jaman dahulu. Kemudian terjadi improvement, misalkan jadi begini:

  1. Get data employee berdasarkan nama;
  2. Jika employee tersebut masuk daftar blacklist:
    • Set response status menjadi "blocked";
    • Set response description berdasarkan alasan blacklist;
  3. Jika employee tersebut sudah wafat:
    • Set response status menjadi "dead";
    • Description tidak perlu diisi;
  4. Jika employee tersebut inactive:
    • Set response status menjadi "resigned";
    • Set response description menjadi "employee is already resigned";
  5. Jika employee tersebut bukan termasuk blacklist, masih hidup dan masih aktif:
    • Set response status menjadi "present";
    • Description tidak perlu diisi;
  6. Set response name dengan nama employee;

Engineer yang bertugas melakukan improvement adalah Kasino, karena Dono sudah resign. Nah, dari improvement tersebut code-nya jadi begini:

public class EmployeeProcessor{
	private final EmployeeRepo repo;

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

	private String status;
	private String description;

	public void processEmployeeByName(String name){
		status = "present";
		Employee employee = repo.getEmployeeByName(name);
		//...
		//any other codes...
		//...
		blacklistCheck(employee);
		//...
		//any other codes...
		//...
		deadCheck(employee);
		//...
		//any other codes...
		//...
		activeCheck(employee);
		//...
		//any other codes...
		//...

		Response response = Response.builder()
				.name(name)
				.status(status)
				.description(description)
				.build();
		System.out.println("response = " + response);
	}

	private void activeCheck(Employee employee){
		if(!employee.isActive()){
			status = "resigned";
			description = "employee is already resigned";
		}
	}

	private void deadCheck(Employee employee){
		if(employee.isDead()){
			status = "retired";
		}
	}

	private void blacklistCheck(Employee employee){
		List<Integer> blackListIds = repo.getBlackListIds();

		if(blackListIds.contains(employee.getId())){
			description = repo.getMistakeById(employee.getId());
			status = "blocked";
		}
	}
}

Kasino mengikuti pendekatan global variable seperti yang dilakukan Dono. Anggaplah code tersebut awalnya baik-baik saja. Lalu Kasino juga resign. Saat production, terjadi bugs. Employee yang sudah wafat ternyata statusnya menjadi "resigned". Tugas bug fixing dilimpahkan ke engineer yang baru saja join, sebut saja namanya Indro. Tugasnya hanya sekedar mengecek kenapa status employee yang sudah wafat jadi "resigned" dan memperbaikinya. Nah, disinilah masalah muncul. Tugas yang aslinya sederhana akan jadi ribet karena Indro harus memeriksa semua method yang mengakses variable status karena itu global dan semua method di dalamnya berhak melakukan perubahan. Ga hanya itu, Indro juga "dipaksa" untuk memeriksa keseluruhan code untuk mengetahui alurnya dari A-Z, dari variable status pertama kali di-assign hingga variable status di-set ke Response. Anggaplah code di atas cukup kompleks, selain method-method di atas juga method-method lainnya yang berseliweran di sana. Tentu ini cukup melelahkan.

Sekarang coba perhatikan code berikut yang ditulis menggunakan local variable:

public class EmployeeProcessor{
	private final EmployeeRepo repo;

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

	public void processEmployeeByName(String name){
		Employee employee = repo.getEmployeeByName(name);
		//...
		//any other codes...
		//...
		List<Integer> blackListIds = repo.getBlackListIds();
		//...
		//any other codes...
		//...
		String description = getDescription(employee, blackListIds);
		//...
		//any other codes...
		//...
		String status = getStatus(employee, blackListIds);
		//...
		//any other codes...
		//...

		Response response = Response.builder()
				.name(name)
				.status(status)
				.description(description)
				.build();
		System.out.println("response = " + response);
	}

	private String getDescription(Employee employee, List<Integer> blackListIds){
		String description = null;
		if(blackListIds.contains(employee.getId())){
			description = repo.getMistakeById(employee.getId());
		} else if(!employee.isActive()){
			description =  "employee is already resigned";
		}
		return description;
	}

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

Seandainya saja Dono menggunakan pendekatan Object Oriented seperti di atas, otomatis si Kasino akan ter-guided untuk melakukan pendekatan yang sama tanpa diajari dan ga akan bisa melakukan pendekatan global variable karena batasan scope dari masing-masing code sudah sesuai fungsinya, Single Responsibility Principle. Tentu pekerjaan Indro selaku bug fixer akan lebih ringan. Dia tinggal melakukan pengecekan pada method yang bertugas melakukan assign variable status tanpa harus meneliti code dari A-Z untuk sebuah bug fixing yang sederhana. Ga nyampe 5 menit langsung ketemu solusinya😃. Mau improvement apapun juga jadi lebih bersih sehingga mudah dipahami dan di-maintain.

When to Use Global Variables?

Global variables memang seringkali menimbulkan masalah saat digunakan. Tapi ga selalu global variable itu buruk. Inti permasalahan dari global variable adalah objeknya dapat diganti di sembarang tempat. Untuk itu variable dengan "final" keyword adalah pengecualian. Biasanya ini digunakan pada constant (static final) atau saat melakukan Dependency Injection (seperti EmployeeRepo pada kasus di atas). Nah, untuk urusan yang satu ini justru dianjurkan karena variable-nya "read-only". Kecuali pada objek yang mutable, "final" keyword saja ga cukup karena meskipun objeknya ga bisa di-assign ulang tapi properties atau elemennya masih dapat berubah. Oh ya, gw juga udah membahas tentang Mutable Objects. Lazy Singleton Pattern pun diperbolehkan menggunakan Mutable Global Variable, walaupun penggunaannya diperdebatkan. Pengecualian juga termasuk pada saat melakukan unit testing karena pada masing-masing @Test method harusnya independent.

Summary

Gw ga berfatwa bahwa Global Variables itu sepenuhnya haram. Ada kalanya global variable itu bagus untuk hal tertentu. Seperti saat Dependency Injection, Constant, Unit Testing, atau juga Singleton Pattern. Global Variables memberikan keleluasaan untuk mengganti nilainya, namun keleluasaan inilah yang bisa disalahgunakan dan dapat menjerumuskan di kemudian hari. Mungkin ada yang berpendapat "Tinggal dijelasin aja masing-masing codenya". Masalahnya, ini akan jadi kebiasaan dan berlanjut turun-temurun, seperti kasus di atas dari Dono kemudian ditiru Kasino, dan yang apes bug fixing adalah Indro. Lebih baik bikin code yang lebih jelas dan to-the-point. Bukankah code yang baik itu code yang bisa dibaca tanpa perlu penjelasan dari A sampai Z? Atau ada juga yang bilang "Kan ada code review untuk menghindari bugs". Yang melakukan code review manusia biasa juga, bisa saja hit and miss karena bakal susah dianalisa manusia biasa. Itu juga belum tentu reviewernya peka. Daripada sibuk melakukan review pada code di atas yang sebenarnya bisa diatasi jika menggunakan pendekatan yang lebih baik, lebih worth it untuk invest waktu melakukan hal lain. Secara umum memang sebaiknya dihindari. Sesuai judul, mostly Global Variable itu: J A H A T 👿.

© 2024 · Ferry Suhandri