Design Pattern 101 — Decorator Pattern
Decorator Pattern เป็น design pattern ในกลุ่มของ Structural Pattern ของ GoF เป็น pattern ที่ทำให้เราสามารถเพิ่มเติม behavior ของ object ได้โดยไม่ต้องมีการแก้ไข class ตลอดเวลา ในบทความนี้จะมาทบทวนและเรียนรู้ว่า Decorator Pattern จะแก้ปัญหาอะไรได้บ้าง
Decorator Pattern
Problem
ร้านกาแฟ Cafe’ Amazing เป็นร้านกาแฟที่เติบโตเร็วมาก มีอยู่ในทุกปั๊มน้ำมัน ทั่วประเทศ เนื่องจากเป็นร้านกาแฟที่เติบโตเร็วมากทำให้อาจจะมีเมนูเครื่องดื่ม เพิ่มขึ้นเรื่อยๆ แต่ตอนเริ่มต้นธุรกิจได้ออกแบบ class ไว้แบบนี้
class Beverage
เป็น superclass ของเครื่องดื่มทั้งหมด
public abstract class Beverage {
protected String description = "Unknown Beverage";
public String getDescription() {
return description;
}
public abstract double cost();
}
เครื่องดื่มมี Espresso
, Decaf
, DarkRoast
และ HouseBlend
ทั้งหมด สืบทอดมาจาก Beverage
class ของเครื่องดื่มแต่ละชนิดจะ implement method cost()
ซึ่งเป็นราคาของเครื่องดื่มแค่ละชนิด
public class Espresso extends Beverage {
public Espresso() {
description = "Espresso";
}
public double cost() {
return 1.99;
}
}public class Decaf extends Beverage {
public Decaf() {
description = "Decaf";
}
public double cost() {
return 1.05;
}
}public class DarkRoast extends Beverage {
public DarkRoast() {
description = "Dark Roast";
}
public double cost() {
return 0.99;
}
}public class HouseBlend extends Beverage {
public HouseBlend() {
description = "House Blend";
}
public double cost() {
return 0.89;
}
}
ร้ายยังมีบริการเครื่องปรุงเพิ่มในแก้วกาแฟ เช่น นมสด (milk), นมถั่วเหลือง (soy) , มอคค่า (mocha) และ วิวคลีม (whip) ดังนั้นจะต้องใส่ สถานะ และ ราคา ของเครื่องปรุงเข้าไปใน superclass และ implement method cost()
เพื่อคิดราคาของเครื่องปรุงแต่ละแก้ว
public abstract class Beverage {
protected String description = "Unknown Beverage";
// Adding for condiment
boolean milk;
boolean soy;
boolean mocha;
boolean whip;
private double milkCost = 0.10;
private double soyCost = 0.15;
private double mochaCost = 0.20;
private double whipCost = 0.10;
public String getDescription() {
return description;
}
public double cost() {
double condimentCost = 0;
if(isMilk()) {
condimentCost += milkCost;
} else if(isSoy()) {
condimentCost += soyCost;
} else if(isMocha()) {
condimentCost += mochaCost;
} else if(isWhip()) {
condimentCost += whipCost;
}
return condimentCost;
}
// getter/setter
}
และจะต้องแก้ไข method cost()
ของเครื่องดื่มแต่ละชนิด โดยคำนวณราคาของเครื่องปรุงด้วย
public class Espresso extends Beverage {
public Espresso() {
description = "Espresso";
}
public double cost() {
return 1.99 + super.cost();
}
}public class Decaf extends Beverage {
public Decaf() {
description = "Decaf";
}
public double cost() {
return 1.05 + super.cost();
}
}public class DarkRoast extends Beverage {
public DarkRoast() {
description = "Dark Roast";
}
public double cost() {
return 0.99 + super.cost();
}
}public class HouseBlend extends Beverage {
public HouseBlend() {
description = "House Blend";
}
public double cost() {
return 0.89 + super.cost();
}
}
จะเห็นได้ว่าถ้าเราเพิ่มเครืองปรุงอีกเราต้องแก้ไขเพิ่มเติมใน superclass Beverage
เสมอ ทำให้ขัดต่อหลักการ Open/Closed ของ SOLID principle
Solution
ได้เวลาของ Decorator Pattern มาช่วยทำให้เราเพิ่มเติมเครื่องปรุงพร้อมทั้งคิดราคาของเครื่องดื่มด้วยพร้อมกัน
ทำให้เราสามารถลบ code ส่วนของเครื่องปรุงออกไปจาก superclass Beverage
ออกไป และลบส่วนของคิดราคาเครื่องปรุงใน method code()
ของเครื่องดื่มชนิดต่างๆ
สร้าง abstract class CondimentDecorator
สืบทอดมาจาก Beverage
และเปลี่ยนให้ method getDescription()
เป็น abstract method เพื่อให้แสดงรายละเอียดของเครื่องปรุง
public abstract class CondimentDecorator extends Beverage {
protected Beverage beverage;
public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}
public abstract String getDescription();
}
ใช้ class CondimentDecorator
สร้าง decorator class สำหรับเครื่องปรุงชนิดต่างๆ โดย inject object Beverage
ใน constructor และ implement ใส่รายละเอียดของเครื่องปรุงใน getDescription()
และ ราคาเครื่องปรุงใน cost()
โดยรวมกับ object Beverage
public class Milk extends CondimentDecorator {
public Milk(Beverage beverage) {
super(beverage);
}
public String getDescription() {
return beverage.getDescription() + ", Mocha";
}
public double cost() {
return 0.10 + beverage.cost();
}
}public class Soy extends CondimentDecorator {
public Soy(Beverage beverage) {
super(beverage);
}
public String getDescription() {
return beverage.getDescription() + ", Soy";
}
public double cost() {
return 0.15 + beverage.cost();
}
}public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
super(beverage);
}
public String getDescription() {
return beverage.getDescription() + ", Mocha";
}
public double cost() {
return 0.20 + beverage.cost();
}
}public class Whip extends CondimentDecorator {
public Whip(Beverage beverage) {
super(beverage);
}
public String getDescription() {
return beverage.getDescription() + ", Whip";
}
public double cost() {
return 0.10 + beverage.cost();
}
}
เอา decorator มาใช้ด้วยการ inject object ของ Beverage
เข้าไปใน decorator class และสุดท้ายก็เรียกใช้ method getDescription()
จะแสดงรายละเอียด และ method cost()
ของทั้งหมดทั้งเครื่องดื่มและเครื่องปรุง
public static void main(String[] args) {
Beverage beverage = new DarkRoast();
beverage = new Mocha(beverage);
beverage = new Whip(beverage);
System.out.println(beverage.getDescription() + " $" + beverage.cost());
}>> Output
Dark Roast, Mocha, Whip $1.29
Decorator Pattern จะเพิ่มความรับผิดชอบเพิ่มเติมกับ object แบบ dynamic, decorator มีทางเลือกที่ยืดหยุ่นเพื่อสร้าง subclass สำหรับขยายความสามารถ
สรุป
จากการที่ได้ทบทวนและเรียนรู้ Decorator Pattern นั้นทำให้เราสามารถเพิ่มเติมความสามารถหรือพฤติกรรมของ object ได้แบบ dynamic โดยไม่ต้องแก้ไข class เดิม ซึ่ง decorator จะเรียกใช้งานความสามารถนั้นซ้อนๆ กันไป ทำให้ความสามารถสมบูรณ์ตามที่ต้องการ และทำให้เข้ากันได้กับหลักการ Open/Closed ของ SOLID