Gw kembali membahas seri Design Pattern selanjutnya yang belum sempat dibahas, yaitu Visitor Design Pattern. Agak males nyari contoh masalah real world-nya😅. Selain itu karena keterbatasan waktu juga sih, cukup sulit mencari waktu senggang akhir-akhir ini. Visitor Design Pattern ini merupakan salah satu Design Pattern yang agak kompleks designnya, tapi kalau kita paham konsepnya akan terasa manfaatnya ketika menghadapi objek yang banyak variannya.
Visitor Design Pattern adalah Behavioral Design Pattern yang memisahkan algoritma masing-masing varian objek ke dalam method spesifik di satu object sehingga lebih mudah dimaintain.
Design Pattern
Use Case
Kita akan membuat algoritma kalkulasi multiple diskon yang memiliki varian rules berbeda. Varian diskonnya kurang lebih seperti berikut:
- Diskon berdasarkan value;
- Diskon berdasarkan persentase;
- Diskon berdasarkan persentase dengan maksimum value;
- Diskon berdasarkan value dengan minimum pembelian dalam rupiah;
- Diskon berdasarkan persentase dengan minimum jumlah pembelian produk;
Biar ga kepanjangan kita coba pakai diskon berdasarkan value dan persentase dulu aja😅. Itu pun sebenarnya banyak lagi rules terkait diskon lainnya🤭. Algoritmanya adalah setiap diskon akan dikalkulasi menggunakan harga pembelian awal.
Contoh Code
DiscountEntity Class
public class DiscountEntity{
private final BigDecimal discountValue;
private final String type;
public DiscountEntity(BigDecimal discountValue, String type){
this.discountValue = discountValue;
this.type = type;
}
public BigDecimal getDiscountValue(){
return discountValue;
}
public String getType(){
return type;
}
}
Contoh penggunaan
public static void main(String[] args){
List<DiscountEntity> discountEntities = getDiscountEntities();
BigDecimal initialPrice = BigDecimal.valueOf(100_000);
BigDecimal calculatedPrice = initialPrice;
for(DiscountEntity discountEntity : discountEntities){
BigDecimal discountValue = discountEntity.getDiscountValue();
if("percentage".equalsIgnoreCase(discountEntity.getType())){
BigDecimal discountAmount = initialPrice.multiply(discountValue)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN);
calculatedPrice = calculatedPrice.subtract(discountAmount);
} else if("amount".equalsIgnoreCase(discountEntity.getType())){
calculatedPrice = calculatedPrice.subtract(discountValue);
}
}
System.out.println("finalPrice = Rp" + calculatedPrice);
//result should be Rp89000 (100000 - (100000 * 10%) - 1000)
}
private static List<DiscountEntity> getDiscountEntities(){
List<DiscountEntity> discountEntities = new ArrayList<>();
discountEntities.add(new DiscountEntity(BigDecimal.valueOf(10), "percentage"));
discountEntities.add(new DiscountEntity(BigDecimal.valueOf(1000), "amount"));
return discountEntities;
}
Dari code di atas kita udah bisa mengimplementasikan algoritma diskon secara sederhana. Produk seharga Rp100.000 dikurangi diskon 10% dan dikurangi diskon Rp1000 = Rp89.000.
Masalah
Karena kita baru menggunakan dua jenis varian diskon, semuanya masih terlihat baik-baik saja. Tapi kalau misalkan kita ingin menambahkan varian-varian diskon lainnya, tentu akan membuat codenya jadi berantakan dan susah dibaca. Akan banyak if-else atau switch-case di dalamnya yang rentan terhadap konflik dan membuat line of codenya membludak🫣. Code dengan switch-case atau if-else yang panjang hanya akan menambah beban hidup engineer selanjutnya yang akan memaintain code tersebut🥲.
Solusi
Untuk meringankan beban maintenance selanjutnya, kita akan implementasi menggunakan Visitor Design Pattern😎.
DiscountEntity Interface
public interface DiscountEntity{
void calculate(DiscountVisitor discountVisitor);
}
DiscountVisitor Interface
public interface DiscountVisitor{
BigDecimal getFinalPrice();
void visitPercentageDiscount(PercentageDiscountEntity discountEntity);
void visitAmountDiscount(AmountDiscountEntity discountEntity);
void visitPercentageWithMaximumValueDiscount(PercentageWithMaximumValueDiscountEntity discountEntity);
}
AmountDiscountEntity Class
public class AmountDiscountEntity implements DiscountEntity{
private final BigDecimal discountValue;
public AmountDiscountEntity(BigDecimal discountValue){
this.discountValue = discountValue;
}
public BigDecimal getDiscountValue(){
return discountValue;
}
@Override
public void calculate(DiscountVisitor discountVisitor){
discountVisitor.visitAmountDiscount(this);
}
}
PercentageDiscountEntity Class
public class PercentageDiscountEntity implements DiscountEntity{
private final BigDecimal percentageValue;
public PercentageDiscountEntity(BigDecimal percentageValue){
this.percentageValue = percentageValue;
}
public BigDecimal getPercentageValue(){
return percentageValue;
}
@Override
public void calculate(DiscountVisitor discountVisitor){
discountVisitor.visitPercentageDiscount(this);
}
}
PercentageWithMaximumValueDiscountEntity Class
public class PercentageWithMaximumValueDiscountEntity implements DiscountEntity{
private final BigDecimal percentageValue;
private final BigDecimal maximumDiscountValue;
public PercentageWithMaximumValueDiscountEntity(BigDecimal percentageValue, BigDecimal maximumDiscountValue){
this.percentageValue = percentageValue;
this.maximumDiscountValue = maximumDiscountValue;
}
public BigDecimal getPercentageValue(){
return percentageValue;
}
public BigDecimal getMaximumDiscountValue(){
return maximumDiscountValue;
}
@Override
public void calculate(DiscountVisitor discountVisitor){
discountVisitor.visitPercentageWithMaximumValueDiscount(this);
}
}
DiscountCalculationVisitor Class
public class DiscountCalculationVisitor implements DiscountVisitor{
private final BigDecimal initialPrice;
private BigDecimal calculatedPrice;
public DiscountCalculationVisitor(BigDecimal initialPrice){
this.initialPrice = initialPrice;
this.calculatedPrice = initialPrice;
}
@Override
public BigDecimal getFinalPrice(){
return this.calculatedPrice;
}
@Override
public void visitPercentageDiscount(PercentageDiscountEntity discountEntity){
BigDecimal percentageValue = discountEntity.getPercentageValue();
BigDecimal discountValue = initialPrice.multiply(percentageValue).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN);
this.calculatedPrice = this.calculatedPrice.subtract(discountValue);
}
@Override
public void visitAmountDiscount(AmountDiscountEntity discountEntity){
BigDecimal discountValue = discountEntity.getDiscountValue();
this.calculatedPrice = this.calculatedPrice.subtract(discountValue);
}
@Override
public void visitPercentageWithMaximumValueDiscount(PercentageWithMaximumValueDiscountEntity discountEntity){
BigDecimal percentageValue = discountEntity.getPercentageValue();
BigDecimal discountValue = initialPrice.multiply(percentageValue).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN);
if(discountValue.compareTo(discountEntity.getMaximumDiscountValue()) > 0){
discountValue = discountEntity.getMaximumDiscountValue();
}
this.calculatedPrice = this.calculatedPrice.subtract(discountValue);
}
}
Contoh penggunaan
public static void main(String[] args){
DiscountVisitor discountVisitor = new DiscountCalculationVisitor(BigDecimal.valueOf(100_000));
List<DiscountEntity> discountEntities = getDiscountEntities();
for(DiscountEntity discountEntity : discountEntities){
discountEntity.calculate(discountVisitor);
}
BigDecimal finalPrice = discountVisitor.getFinalPrice();
System.out.println("finalPrice = Rp" + finalPrice);
//result should be Rp88000 (100000 - (100000 * 10%) - 1000 - 1000)
}
private static List<DiscountEntity> getDiscountEntities(){
List<DiscountEntity> discountEntities = new ArrayList<>();
discountEntities.add(new PercentageDiscountEntity(BigDecimal.valueOf(10)));
discountEntities.add(new PercentageWithMaximumValueDiscountEntity(BigDecimal.valueOf(10), BigDecimal.valueOf(1000)));
discountEntities.add(new AmountDiscountEntity(BigDecimal.valueOf(1000)));
return discountEntities;
}
Kita membuat interface DiscountEntity dengan varian implementasi diskonnya. Kita perlu sebuah class yang menjadi tempat kumpulan method yang mengatur varian algoritma diskon tersebut, yaitu DiscountVisitor dengan implementasi DiscountCalculationVisitor. Saat melakukan kalkulasi kita hanya eksekusi masing-masing entitas diskon menggunakan class Visitor. Penggunaannya jadi lebih ringkas dan rapi. Produk dengan harga Rp100.000 dikurangi diskon 10% dan dikurangi diskon 10% dengan nilai maksimal Rp1000 lalu dikurangi diskon Rp1000 = Rp88.000.
Kenapa menggunakan Visitor Design Pattern?
Dengan menggunakan Visitor Design Pattern kita memisahkan algoritma masing-masing varian ke dalam satu objek dengan method terpisah. Suatu saat ketika ada penambahan varian baru seperti PercentageWithMaximumValue, PercentageWithMinimumQuantity, AmountWithMinimumPrice, ataupun varian algoritma diskon lainnya, kita tinggal bikin class baru dengan mengimplementasi DiscountEntity dan tambahkan algoritmanya di dalam method baru pada DiscountVisitor. Kita ga perlu melakukan perombakan setiap ada perubahan atau penambahan algoritma baru pada code yang udah ada, kita hanya fokus pada varian terkait saja. Maintenance jadi lebih mudah karena ga akan senggol-senggolan dengan algoritma varian lain. Ini cocok untuk code dengan multiple varian yang bisa di-apply. Pada contoh di atas, kita melakukan kalkulasi berdasarkan initial price, jika suatu saat ada rules lain yang mengharuskan untuk kalkulasi secara sequential berdasarkan calculated price sebelumnya, kita tinggal bikin class implementasi DiscountVisitor yang baru, seperti DiscountSequentialCalculationVisitor. Dengan begitu perubahannya ga akan mengubah rules yang sudah ada sebelumnya😎.
Verdict
Kalau dilihat sekilas, design pattern ini memang sedikit kompleks, tapi berbanding lurus dengan benefitnya. Visitor Design Pattern menyelesaikan permasalahan dengan memisahkan algoritma masing-masing varian ke dalam satu objek. Codenya jadi lebih mudah dimaintain karena masing-masing varian algoritmanya dipisah ke masing-masing method spesifik di dalam class Visitor sehingga lebih aman dari konflik. Karena Visitornya dalam bentuk interface, tiap ada implementasi Visitor yang baru tinggal bikin implementasi baru, sehingga rules diskonnya juga interchangeable. Salah satu contoh penerapan dari Visitor Design Pattern adalah pada library QueryDSL, masing-masing Expression pada QueryDSL memiliki beberapa varian seperti SubQueryExpression, Operation, Constant, dan varian-varian lainnya. Algoritma masing-masing variannya diolah menggunakan Visitor Design Pattern.