Penggunaan Chain of Responsibility (CoR) sebenarnya tidak cukup populer karena cukup kompleks untuk system yang sederhana. Tapi menurut gw ini layak untuk dipertimbangkan ketika system yang kita bangun mulai kompleks. Apalagi kalau kita ingin mengembangkan library pihak ketiga yang nantinya akan dipakai oleh banyak orang. Tentu kita harus membuat library yang portable dan fleksibel.
Chain of Responsibility Design Pattern adalah Behavioral Design Pattern yang membuat rangkaian handler yang fleksibel untuk memproses request secara berantai satu-persatu, dan masing-masing handler menentukan apakah requestnya akan dilanjutkan prosesnya ke handler selanjutnya atau tidak.
Design Pattern
Use Case
Contoh kasusnya kita akan membuat library yang menghandle data user yang akan dipakai oleh banyak orang. Di dalam objek tersebut kita akan menambahkan validasi tersebut terhadap request yang dikirim, seperti validasi email, nama, berat, tinggi, dan tanggal lahir.
Contoh Code
User Class
public class User{
private String email;
private String name;
private float height;
private float weight;
private LocalDate birthDate;
public String getEmail(){
return email;
}
public void setEmail(String email){
this.email = email;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public float getHeight(){
return height;
}
public void setHeight(float height){
this.height = height;
}
public float getWeight(){
return weight;
}
public void setWeight(float weight){
this.weight = weight;
}
public LocalDate getBirthDate(){
return birthDate;
}
public void setBirthDate(LocalDate birthDate){
this.birthDate = birthDate;
}
}
Contoh penggunaan
public static void main(String[] args){
User user = new User();
user.setEmail("fer@fer.com");
user.setName("ferry");
user.setHeight(170);
user.setWeight(60);
user.setBirthDate(LocalDate.now());
if(user.getEmail() == null){
System.out.println("email is null");
} else if(!user.getEmail().contains("@") || !user.getEmail().contains(".")){
System.out.println("invalid email " + user.getEmail());
} else if(user.getWeight() < 40 || user.getHeight() < 150){
System.out.println("user is too small");
} else if(user.getName() == null || user.getName().length() < 3){
System.out.println("name is too short");
} else {
System.out.println("user " + user.getName() + " is valid");
}
}
Masalah
Sekilas tidak ada yang salah. Kita hanya menambahkan branching if-else untuk melakukan validasi tiap data. Kalau kita develop untuk aplikasi sendiri memang tidak ada yang salah. Masalahnya adalah kalau kita develop sebagai library pihak ketiga, dan kita ingin memungkinkan client untuk menambah atau mengurangi validasi. Tentu itu tidak bisa kalau client tersebut tidak memiliki akses terhadap code kita. Selain itu melakukan branching if-else yang terlalu banyak juga dapat mengakibatkan code smell kalau ada tambahan-tambahan validasi lainnya karena codenya akan membengkak. Untuk itu solusinya kita perlu bikin code tersebut fleksibel menggunakan Chain of Responsibility Design Pattern😎.
Solusi
UserValidation Interface
public interface UserValidation{
void nextValidation(UserValidation validation);
boolean isValid(User user);
}
EmailValidation Class
public class EmailValidation implements UserValidation{
private UserValidation next;
@Override
public void nextValidation(UserValidation validation){
this.next = validation;
}
@Override
public boolean isValid(User user){
if(user.getEmail() == null){
System.out.println("email is null");
return false;
}
if(!user.getEmail().contains("@") || !user.getEmail().contains(".")){
System.out.println("invalid email " + user.getEmail());
return false;
}
System.out.println("email validation is passed");
return next == null || next.isValid(user);
}
}
BodyValidation Class
public class BodyValidation implements UserValidation{
private UserValidation next;
@Override
public void nextValidation(UserValidation validation){
this.next = validation;
}
@Override
public boolean isValid(User user){
if(user.getWeight() <= 40 || user.getHeight() <= 150){
System.out.println("user is too small");
return false;
}
System.out.println("body validation is passed");
return next == null || next.isValid(user);
}
}
NameValidation Class
public class NameValidation implements UserValidation{
private UserValidation next;
@Override
public void nextValidation(UserValidation validation){
this.next = validation;
}
@Override
public boolean isValid(User user){
if(user.getName() == null || user.getName().length() < 3){
System.out.println("name is too short");
return false;
}
System.out.println("name validation is passed");
return next == null || next.isValid(user);
}
}
UserHandler Class
public class UserHandler{
private UserValidation userValidation;
private UserValidation lastUserValidation;
public UserHandler addValidation(UserValidation validation){
if(this.userValidation == null){
this.userValidation = validation;
} else {
this.lastUserValidation.nextValidation(validation);
}
this.lastUserValidation = validation;
return this;
}
public void handle(User user){
if(this.userValidation == null || this.userValidation.isValid(user)){
System.out.println("user " + user.getName() + " is valid");
}
}
}
Contoh penggunaan
public static void main(String[] args){
UserHandler userHandler = new UserHandler()
.addValidation(new EmailValidation())
.addValidation(new BodyValidation())
.addValidation(new NameValidation());
User user = new User();
user.setEmail("fer@fer.com");
user.setHeight(170);
user.setName("ferry");
user.setWeight(40);
user.setBirthDate(LocalDate.now());
userHandler.handle(user);
}
Sekarang kita kelompokkan masing-masing validasi menggunakan interface UserValidation. Kita memiliki class UserHandler untuk handle user dan melakukan validasi. Validation kita bikin secara berantai, jadi nanti setiap selesai satu validasi maka lanjut ke validasi berikutnya sampai selesai. Kalau ada validasi yang tidak lolos maka proses akan dihentikan dan tidak akan dilanjutkan ke proses selanjutnya. Saat selesai dan semua validasinya lolos baru kita handle request tersebut. Dengan Chain of Responsibility, client bisa menentukan validasi apa aja yang diinginkan tanpa harus memiliki akses dan mengubah code UserHandler. Design Pattern ini juga memenuhi Single Responsibility Principle dan Open/Close Principle.
Kenapa menggunakan Chain of Responsibility Design Pattern?
Chain of Responsibility Design Pattern digunakan ketika ingin membangun code yang memproses request dengan dieksekusi secara berantai namun fleksibel. Setiap ada penggantian rantai eksekusi client bisa mengubahnya tanpa mengubah strukur code. Bahkan saat client perlu menambahkan validasi baru yang belum ada di library, misalnya client ingin menambahkan validasi tanggal lahir, maka tinggal bikin class baru seperti BirthDateValidation dan mengimplementasi interface UserValidation. Lalu register class tersebut ke UserHandler. Dengan begitu client dapat menggunakan library kita secara fleksibel.
Verdict
Chain of Responsibility Design Pattern memang ga sepopuler Design Pattern lainnya. Tapi cukup berguna bila ingin membuat library pihak ketiga yang fleksibel digunakan client. Penggunaannya sedikit kompleks dibanding if-else biasa. Untuk itu, jika hanya untuk develop diri sendiri saja pakai if-else juga udah cukup. Contoh penggunaan Chain of Responsibility bisa kita lihat pada library Spring web dan Spring security ketika menambahkan layer filter sebelum request masuk ke controller pada class GenericFilterBean dan SecurityFilterChain. Kita bisa dengan bebas menambahkan atau memodifikasi filter web tanpa harus mengubah langsung pada library Spring.