Design Pattern 101 — Decorator Pattern

Phayao Boonon
4 min readAug 18, 2019

--

Photo by James Lee on Unsplash

Decorator Pattern เป็น design pattern ในกลุ่มของ Structural Pattern ของ GoF เป็น pattern ที่ทำให้เราสามารถเพิ่มเติม behavior ของ object ได้โดยไม่ต้องมีการแก้ไข class ตลอดเวลา ในบทความนี้จะมาทบทวนและเรียนรู้ว่า Decorator Pattern จะแก้ปัญหาอะไรได้บ้าง

https://refactoring.guru/design-patterns/decorator

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

--

--

Phayao Boonon
Phayao Boonon

Written by Phayao Boonon

Software Engineer 👨🏻‍💻 Stay Hungry Stay Foolish

No responses yet