- เพิ่มความสามารถให้ Object ด้วยการห่อเป็นชั้น ๆ
- 1. ปัญหาที่พบเมื่อใช้ Inheritance กับทุกชุดตัวเลือก
- 2. Decorator ทำงานอย่างไร
- 3. องค์ประกอบหลักของ Pattern
- 4. Workshop: ระบบสั่งพิซซ่า
- 5. อ่านโค้ดทีละส่วน
- 6. ประกอบความสามารถตามตัวเลือกขณะทำงาน
- 7. เพิ่ม Decorator ใหม่โดยไม่แก้คลาสเดิม
- 8. ลำดับการห่อมีผลหรือไม่
- 9. ตัวอย่างใกล้เคียงใน Java I/O
- 10. Decorator ต่างจาก Pattern ใกล้เคียงอย่างไร
- 11. ประโยชน์และต้นทุน
- 12. ข้อควรระวังในการนำไปใช้
- 13. แนวทางตรวจสอบความถูกต้อง
- 14. แบบฝึกหัด
- 15. ทบทวนแนวคิด
- เอกสารอ้างอิง
#เพิ่มความสามารถให้ Object ด้วยการห่อเป็นชั้น ๆ

Decorator Pattern เป็นรูปแบบการออกแบบเชิงโครงสร้าง (Structural Design Pattern) ที่ใช้เพิ่มความรับผิดชอบให้ object แต่ละตัว โดยนำ object เดิมไปห่อด้วย object อีกตัวที่ใช้งานผ่านสัญญาเดียวกัน แล้วส่งต่อการทำงานพร้อมเพิ่มพฤติกรรมที่ต้องการ
ตัวอย่างในบทความนี้คือระบบสั่งพิซซ่า: เริ่มจากพิซซ่าพื้นฐาน เพิ่มชีส เพิ่มเห็ด หรือเพิ่มชีสสองครั้งได้ โดยไม่ต้องสร้างคลาสใหม่สำหรับทุกชุดท็อปปิง
แนวคิดสำคัญ: ผู้เรียกใช้ทำงานผ่าน interface เดิม ส่วนความสามารถเพิ่มเติมอยู่ใน object ที่ห่อซ้อนกัน
กลุ่มผู้อ่าน: ผู้เรียน Java ที่เข้าใจ interface, inheritance และ composition เบื้องต้น
เครื่องมือ: JDK 17 ขึ้นไป ไม่ต้องติดตั้งไลบรารีเพิ่มเติม
ผลลัพธ์: โปรแกรมคำนวณราคาพิซซ่า พร้อมอธิบายโครงสร้าง การเรียกซ้อน และข้อควรระวัง
#1. ปัญหาที่พบเมื่อใช้ Inheritance กับทุกชุดตัวเลือก
สมมติว่าเรามีพิซซ่าพื้นฐานและท็อปปิงหลายชนิด หากสร้าง subclass สำหรับทุกชุด จะเริ่มมีชื่อคลาสเช่น
CheesePizzaMushroomPizzaCheeseMushroomPizzaDoubleCheesePizzaCheeseMushroomOlivePizza
เมื่อมีตัวเลือกอิสระ n ชนิด และแต่ละชนิดเลือกได้เพียงมีหรือไม่มี จะมีชุดตัวเลือกได้ถึง 2^n ชุด รวมชุดที่ไม่เพิ่มอะไรเลย ถ้ารองรับปริมาณซ้ำและลำดับด้วย จำนวนกรณีก็เพิ่มขึ้นอีก
Decorator แยกความสามารถแต่ละอย่างเป็นคลาสของตนเอง แล้วประกอบ object ตามคำสั่งซื้อ ทำให้ไม่จำเป็นต้องสร้าง subclass ให้ทุกชุดตัวเลือก
| วิธีออกแบบ | สิ่งที่สร้างเพิ่มเมื่อมีท็อปปิงใหม่ | ผลต่อการประกอบตัวเลือก |
|---|---|---|
| Subclass สำหรับทุกชุด | คลาสของชุดที่มีท็อปปิงนั้น | ชุดตัวเลือกผูกอยู่กับชนิดคลาส |
| Decorator | คลาสท็อปปิงที่ห่อ Pizza ได้ |
ประกอบแต่ละคำสั่งซื้อขณะโปรแกรมทำงาน |
| ข้อมูลท็อปปิงในรายการ | ข้อมูลหรือกฎคำนวณ | เหมาะเมื่อความแตกต่างเป็นเพียงชื่อและราคา |
ข้อสังเกตในการออกแบบ: ถ้าท็อปปิงมีเพียงชื่อและราคา การใช้รายการข้อมูลอาจเรียบง่ายกว่า ตัวอย่างพิซซ่าในบทความนี้มีไว้ช่วยให้เข้าใจกลไกของ Pattern ส่วน Decorator จะมีคุณค่ามากขึ้นเมื่อแต่ละชั้นมีพฤติกรรมจริง เช่น buffering, logging หรือการแปลงข้อมูล
#2. Decorator ทำงานอย่างไร
Decorator ที่ใช้ในบทความนี้มีคุณสมบัติหลักสามอย่าง
- ใช้ interface เดียวกับ object ที่ถูกห่อ
- เก็บ reference ไปยัง object นั้น
- ส่งต่อการเรียก แล้วเพิ่มพฤติกรรมก่อน หลัง หรือรอบการทำงานเดิม
ตัวอย่าง:
Pizza order = new Mushroom(
new Cheese(
new MargheritaPizza()));
อ่านนิพจน์จากด้านในออกมาได้ว่า สร้างพิซซ่าพื้นฐาน ห่อด้วยชีส แล้วห่อด้วยเห็ด
เมื่อเรียก order.price() การเรียกจะเริ่มจากชั้นนอกสุดคือ Mushroom ส่งต่อไปยัง Cheese และ MargheritaPizza จากนั้นผลลัพธ์จะถูกบวกเพิ่มระหว่างส่งกลับออกมา
| ชั้นที่ส่งผลลัพธ์กลับ | การคำนวณ | ราคา |
|---|---|---|
MargheritaPizza |
ราคาพื้นฐาน | 100.00 บาท |
Cheese |
100.00 + 25.00 | 125.00 บาท |
Mushroom |
125.00 + 15.00 | 140.00 บาท |
order ยังคงเป็นชนิด Pizza ผู้เรียกใช้จึงไม่ต้องตรวจว่าเพิ่มท็อปปิงชนิดใดไว้บ้างเพื่อเรียก price()
#3. องค์ประกอบหลักของ Pattern
| บทบาท | คลาสในตัวอย่าง | หน้าที่ |
|---|---|---|
| Component | Pizza |
กำหนดสัญญาร่วม ได้แก่ description() และ price() |
| Concrete Component | MargheritaPizza |
ให้พฤติกรรมพื้นฐาน |
| Base Decorator | PizzaDecorator |
ใช้สัญญาเดียวกัน เก็บ object ที่ถูกห่อ และส่งต่อการทำงาน |
| Concrete Decorator | Cheese และ Mushroom |
เพิ่มรายละเอียดและราคา |
Base Decorator เป็นวิธีลดโค้ดซ้ำที่นิยมใช้ แต่ไม่ใช่ข้อบังคับของ Pattern สามารถให้ Concrete Decorator implement Pizza และเก็บ Pizza โดยตรงได้เช่นกัน
#แผนภาพโครงสร้างคลาส
classDiagram
direction TB
class Pizza {
<<interface>>
+description() String
+price() BigDecimal
}
class MargheritaPizza
class PizzaDecorator {
<<abstract>>
#Pizza wrapped
+description() String
+price() BigDecimal
}
class Cheese
class Mushroom
Pizza <|.. MargheritaPizza
Pizza <|.. PizzaDecorator
PizzaDecorator o--> Pizza : wraps
PizzaDecorator <|-- Cheese
PizzaDecorator <|-- Mushroom
เส้น implements ทำให้เรียกผ่านสัญญาเดียวกัน ส่วนความสัมพันธ์ wraps ทำให้ Decorator ส่งต่อการทำงานไปยัง object อีกตัวได้ จึงเป็นการใช้ inheritance ร่วมกับ composition โดยให้ composition เป็นกลไกประกอบความสามารถ
#4. Workshop: ระบบสั่งพิซซ่า
กำหนดราคาเพื่อใช้ทดลองดังนี้
| รายการ | ราคา |
|---|---|
| Margherita | 100.00 บาท |
| เพิ่มชีสหนึ่งชั้น | 25.00 บาท |
| เพิ่มเห็ดหนึ่งชั้น | 15.00 บาท |
ในตัวอย่างใช้ BigDecimal และสร้างค่าจากข้อความ เช่น new BigDecimal("25.00") เพื่อระบุค่าทศนิยมโดยตรง หลีกเลี่ยงการนำค่าประมาณจาก double เข้ามาโดยไม่ตั้งใจ ตามพฤติกรรม constructor ที่ระบุใน Java API [3]
#ขั้นตอนที่ 1: สร้างไฟล์ DecoratorDemo.java
คัดลอกโค้ดทั้งหมดต่อไปนี้ลงในไฟล์เดียว โค้ดมี public class เพียงตัวเดียว ส่วนคลาสอื่นมีขอบเขตระดับ package เพื่อให้เริ่มทดลองได้ง่าย
import java.math.BigDecimal;
import java.util.Objects;
public class DecoratorDemo {
public static void main(String[] args) {
Pizza base = new MargheritaPizza();
Pizza cheese = new Cheese(base);
Pizza loaded = new Mushroom(cheese);
Pizza doubleCheese = new Cheese(new Cheese(base));
Pizza reversed = new Cheese(new Mushroom(base));
print("Base", base);
print("Cheese", cheese);
print("Loaded", loaded);
print("Double cheese", doubleCheese);
print("Reversed", reversed);
}
private static void print(String label, Pizza pizza) {
System.out.println(label + ": " + pizza.description()
+ " = " + pizza.price().toPlainString() + " THB");
}
}
interface Pizza {
String description();
BigDecimal price();
}
final class MargheritaPizza implements Pizza {
@Override
public String description() {
return "Margherita";
}
@Override
public BigDecimal price() {
return new BigDecimal("100.00");
}
}
abstract class PizzaDecorator implements Pizza {
protected final Pizza wrapped;
protected PizzaDecorator(Pizza wrapped) {
this.wrapped = Objects.requireNonNull(wrapped, "wrapped pizza");
}
@Override
public String description() {
return wrapped.description();
}
@Override
public BigDecimal price() {
return wrapped.price();
}
}
final class Cheese extends PizzaDecorator {
private static final BigDecimal EXTRA = new BigDecimal("25.00");
Cheese(Pizza wrapped) {
super(wrapped);
}
@Override
public String description() {
return super.description() + " + Cheese";
}
@Override
public BigDecimal price() {
return super.price().add(EXTRA);
}
}
final class Mushroom extends PizzaDecorator {
private static final BigDecimal EXTRA = new BigDecimal("15.00");
Mushroom(Pizza wrapped) {
super(wrapped);
}
@Override
public String description() {
return super.description() + " + Mushroom";
}
@Override
public BigDecimal price() {
return super.price().add(EXTRA);
}
}
#ขั้นตอนที่ 2: คอมไพล์และรัน
ตรวจสอบว่าติดตั้ง JDK แล้ว:
java -version
javac -version
คอมไพล์และเรียกโปรแกรม:
javac -encoding UTF-8 DecoratorDemo.java
java DecoratorDemo
หรือรัน source file โดยตรงด้วย JDK 17 ขึ้นไป:
java DecoratorDemo.java
#ขั้นตอนที่ 3: ตรวจสอบผลลัพธ์
Base: Margherita = 100.00 THB
Cheese: Margherita + Cheese = 125.00 THB
Loaded: Margherita + Cheese + Mushroom = 140.00 THB
Double cheese: Margherita + Cheese + Cheese = 150.00 THB
Reversed: Margherita + Mushroom + Cheese = 140.00 THB
base ยังคงมีราคา 100.00 บาท เพราะตัวอย่างนี้สร้าง wrapper ใหม่โดยไม่แก้สถานะของ object เดิม การไม่แก้สถานะดังกล่าวเป็นคุณสมบัติของ implementation นี้ ไม่ใช่สิ่งที่ Decorator Pattern รับประกันให้โดยอัตโนมัติ
#5. อ่านโค้ดทีละส่วน
#Component: สัญญาที่ผู้เรียกใช้มองเห็น
interface Pizza {
String description();
BigDecimal price();
}
ทั้งพิซซ่าพื้นฐานและท็อปปิงมีสัญญานี้ร่วมกัน จึงสามารถใช้ Cheese แทน Pizza ในจุดที่ต้องการคำนวณราคาได้
#Concrete Component: งานพื้นฐาน
MargheritaPizza ให้ชื่อและราคาของพิซซ่าที่ไม่เพิ่มท็อปปิง คลาสนี้ไม่จำเป็นต้องรู้ว่ามี Cheese หรือ Mushroom อยู่ในระบบ
#Base Decorator: เก็บ object และส่งต่อ
protected final Pizza wrapped;
protected PizzaDecorator(Pizza wrapped) {
this.wrapped = Objects.requireNonNull(wrapped, "wrapped pizza");
}
ใช้ชนิด Pizza แทนการผูกกับ MargheritaPizza ทำให้ห่อได้ทั้งพิซซ่าพื้นฐานและ Decorator อีกชั้น ส่วน requireNonNull ทำให้ตรวจพบการประกอบ object ที่ผิดตั้งแต่ constructor
final ป้องกันการเปลี่ยน reference หลังสร้าง wrapper แต่ไม่ได้ทำให้ object ที่ถูกอ้างถึงกลายเป็น immutable เสมอไป
#Concrete Decorator: ส่งต่อแล้วเพิ่มงาน
@Override
public BigDecimal price() {
return super.price().add(EXTRA);
}
super.price() เรียก implementation ใน PizzaDecorator ซึ่งส่งต่อไปยัง wrapped.price() จากนั้นชั้นปัจจุบันจึงบวกราคาของตนเอง
ถ้าคืนค่าเพียง EXTRA โดยไม่ใช้ราคาจาก object ด้านใน ราคาของชั้นก่อนหน้าจะหายไป
#6. ประกอบความสามารถตามตัวเลือกขณะทำงาน
ในระบบจริง ตัวเลือกอาจมาจากหน้าเว็บหรือ request ของ API สามารถสร้างคำสั่งซื้อแบบมีเงื่อนไขได้
เพิ่มเมธอดต่อไปนี้ภายในคลาส DecoratorDemo:
private static Pizza createOrder(boolean addCheese, boolean addMushroom) {
Pizza order = new MargheritaPizza();
if (addCheese) {
order = new Cheese(order);
}
if (addMushroom) {
order = new Mushroom(order);
}
return order;
}
จากนั้นเรียกภายใน main:
Pizza order = createOrder(true, true);
print("Selected", order);
ผลลัพธ์:
Selected: Margherita + Cheese + Mushroom = 140.00 THB
ตัวแปร order ถูกเปลี่ยนให้อ้างถึง wrapper ชั้นใหม่ แต่ยังคงใช้ชนิด Pizza ตลอด เมธอดประกอบคำสั่งซื้อเป็นจุดที่รับผิดชอบการเลือกชั้น ส่วนแต่ละ Decorator รับผิดชอบพฤติกรรมของตน
#7. เพิ่ม Decorator ใหม่โดยไม่แก้คลาสเดิม
สมมติต้องการเพิ่มมะกอกราคา 20.00 บาท ให้เพิ่มคลาสนี้ไว้ต่อท้ายไฟล์ DecoratorDemo.java:
final class Olive extends PizzaDecorator {
private static final BigDecimal EXTRA = new BigDecimal("20.00");
Olive(Pizza wrapped) {
super(wrapped);
}
@Override
public String description() {
return super.description() + " + Olive";
}
@Override
public BigDecimal price() {
return super.price().add(EXTRA);
}
}
เพิ่มโค้ดภายใน main เพื่อทดลอง:
Pizza special = new Olive(
new Mushroom(
new Cheese(
new MargheritaPizza())));
print("Special", special);
ผลลัพธ์:
Special: Margherita + Cheese + Mushroom + Olive = 160.00 THB
ไม่ต้องแก้ MargheritaPizza, Cheese หรือ Mushroom แต่ต้องปรับจุดประกอบคำสั่งซื้อให้เลือกใช้ Olive ได้ นี่คือขอบเขตที่ชัดเจนของหลัก Open/Closed ในตัวอย่างนี้: ส่วนพฤติกรรมเดิมคงที่ ส่วนความสามารถใหม่เพิ่มผ่านคลาสใหม่
#8. ลำดับการห่อมีผลหรือไม่
พิจารณาสองคำสั่งนี้:
Pizza first = new Mushroom(new Cheese(new MargheritaPizza()));
Pizza second = new Cheese(new Mushroom(new MargheritaPizza()));
ทั้งสองราคา 140.00 บาท เพราะตัวอย่างนี้บวกค่าคงที่เหมือนกัน แต่ description() เรียงข้อความต่างกัน
อย่าสรุปว่า Decorator สลับลำดับได้เสมอ หากแต่ละชั้นทำงานต่างชนิดกัน ผลลัพธ์อาจเปลี่ยน เช่น ส่วนลดหรือการแปลงข้อมูล
| ลำดับการคำนวณจากราคาตั้งต้น 100 บาท | วิธีคิด | ราคาสุดท้าย |
|---|---|---|
| ลด 10 บาท แล้วลด 10% | (100 − 10) × 0.90 | 81.00 บาท |
| ลด 10% แล้วลด 10 บาท | (100 × 0.90) − 10 | 80.00 บาท |
หากเมธอดของแต่ละชั้นเรียกชั้นด้านในก่อน แล้วค่อยใช้ส่วนลดของตนเอง ชั้นที่อยู่ด้านในจะคำนวณส่วนลดก่อนชั้นนอก สำหรับระบบราคาเงินจริงควรกำหนดลำดับ กติกาค่าต่ำสุด และการปัดเศษให้ชัดเจน
#9. ตัวอย่างใกล้เคียงใน Java I/O
Java API ระบุว่า FilterInputStream ห่อ input stream อีกตัวหนึ่ง แล้วอาจแปลงข้อมูลหรือเพิ่มความสามารถ ขณะที่ BufferedInputStream เป็น subclass ที่เพิ่ม buffering [1][2]
เมื่อนำมาเทียบกับ Decorator Pattern จะเห็นรูปแบบเดียวกัน: ใช้ชนิด stream ร่วมกัน เก็บ stream ด้านใน และประกอบความสามารถด้วย wrapper การจัดบทบาทนี้เป็นการอธิบายโครงสร้างจาก API
สร้างไฟล์ IoDecoratorDemo.java:
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
public class IoDecoratorDemo {
public static void main(String[] args) throws IOException {
byte[] bytes = {0, 0, 0, 42};
try (DataInputStream input = new DataInputStream(
new BufferedInputStream(
new ByteArrayInputStream(bytes)))) {
System.out.println(input.readInt());
}
}
}
รัน:
javac IoDecoratorDemo.java
java IoDecoratorDemo
ผลลัพธ์:
42
| Object | บทบาทในตัวอย่าง |
|---|---|
ByteArrayInputStream |
เป็นแหล่งข้อมูลไบต์ในหน่วยความจำ |
BufferedInputStream |
เพิ่มการอ่านผ่าน buffer |
DataInputStream |
เพิ่มเมธอดอ่านข้อมูลพื้นฐาน เช่น readInt() |
ตัวอย่างใช้ข้อมูลในหน่วยความจำเพื่อให้รันได้ทันที Buffering จะเห็นประโยชน์ชัดขึ้นเมื่อแหล่งข้อมูลมีต้นทุนการอ่าน เช่น ไฟล์ การเรียก readInt() ครั้งเดียวในตัวอย่างนี้มีไว้แสดงการประกอบ object ไม่ใช่การวัดประสิทธิภาพ
try-with-resources ปิด stream ชั้นนอกเมื่อจบ block และ stream wrapper ในชุดนี้ส่งต่อการปิดไปยังชั้นด้านใน ควรตรวจสัญญาของ wrapper ที่ใช้งานจริงเสมอ โดยเฉพาะเมื่อห่อ resource ที่ใช้ร่วมกัน [1]
#10. Decorator ต่างจาก Pattern ใกล้เคียงอย่างไร
| Pattern | เป้าหมายหลัก | วิธีคิดเมื่อออกแบบ |
|---|---|---|
| Decorator | เพิ่มพฤติกรรมให้ object ผ่านสัญญาเดิมและประกอบซ้อนกันได้ | “เพิ่มความสามารถอีกชั้น” |
| Adapter | ทำให้ interface ที่ต่างกันใช้งานร่วมกันได้ | “แปลงสัญญาให้ตรงกับที่ผู้ใช้ต้องการ” |
| Proxy | ควบคุมการเข้าถึง object เช่น lazy loading หรือ remote access | “จัดการการเข้าถึงตัวจริง” |
| Composite | จัดกลุ่ม object เป็นโครงสร้างต้นไม้และปฏิบัติต่อรายตัวกับกลุ่มผ่านสัญญาร่วม | “จัดการส่วนย่อยและองค์รวม” |
| Strategy | สลับอัลกอริทึมที่ object ใช้งาน | “เลือกวิธีทำงาน” |
Decorator กับ Proxy อาจมีโครงสร้างคล้ายกันมาก ควรพิจารณาเจตนาและหน้าที่จริง ส่วน logging, caching หรือ security ไม่ได้ชี้ว่าเป็น Pattern ใดโดยอัตโนมัติ ต้องดูการออกแบบและเหตุผลที่เพิ่ม wrapper นั้น
Decorator ของ GoF ใน Java ก็ไม่ใช่กลไก annotation @... หรือคำว่า decorator ในภาษาอื่นโดยตรง แม้อาจมีแนวคิดเพิ่มพฤติกรรมที่คล้ายกัน
#11. ประโยชน์และต้นทุน
| ประเด็น | สิ่งที่ได้ | สิ่งที่ต้องดูแล |
|---|---|---|
| การขยายความสามารถ | เพิ่ม Decorator ใหม่ได้ | จุดประกอบ object ต้องเลือกใช้ชั้นใหม่ |
| การใช้ซ้ำ | ใช้ชั้นเดียวกันกับ Component หลายชนิดได้ | ทุกชนิดต้องรักษาสัญญาร่วม |
| การประกอบ | เลือกชุดความสามารถขณะทำงานได้ | ลำดับและการซ้ำอาจมีความหมาย |
| การดูแลโค้ด | แยกหน้าที่ของแต่ละชั้นได้ | มีคลาสและเส้นทางเรียกเพิ่มขึ้น |
| การทดสอบ | ตรวจพฤติกรรมแต่ละชั้นและการประกอบได้ | ต้องเลือกกรณีผสมที่เสี่ยงผิดจริง |
ถ้ามี k ชั้นและแต่ละชั้นส่งต่อเมธอดหนึ่งครั้งพร้อมทำงาน O(1) เวลาของการเรียกนั้นจะเป็น O(k) และใช้พื้นที่เก็บ wrapper O(k) ส่วนการต่อ string ยาวขึ้นทีละชั้นอาจมีต้นทุนคัดลอกสะสมมากกว่านั้น จึงไม่ควรเหมารวมว่าเมธอดทุกตัวมีต้นทุนเท่ากัน
#12. ข้อควรระวังในการนำไปใช้
#รักษาสัญญาของ Component
Decorator ควรยังใช้แทน Component ในขอบเขตสัญญาที่กำหนดไว้ได้ เช่น price() ควรคืนราคาของรายการที่ห่อรวมกับความสามารถของชั้นปัจจุบัน โดยไม่เปลี่ยนไปทำงานที่ไม่เกี่ยวข้อง
#แยก Reference ออกจากสถานะ
ตัวอย่างเก็บ wrapped เป็น final แต่ถ้า Component ด้านในเปลี่ยนสถานะได้ wrapper ที่แชร์ Component ตัวเดียวกันก็อาจเห็นการเปลี่ยนแปลงนั้นด้วย
#ระวังการเพิ่มซ้ำ
new Cheese(new Cheese(base)) เพิ่มชีสสองครั้งและคิดเงินสองครั้ง ตัวอย่างนี้อนุญาต แต่ในระบบแจ้งเตือน การห่อชั้นส่ง SMS ซ้ำอาจทำให้ส่งข้อความซ้ำ ต้องกำหนดกฎการประกอบให้ตรงกับงาน
#อย่าซ่อนความล้มเหลวโดยไม่มีนโยบาย
เมื่อชั้นด้านในโยน exception ชั้นนอกต้องตัดสินใจตามสัญญาว่าจะส่งต่อ แปลง หรือจัดการอย่างไร หากเพิ่ม retry รอบงานที่มีผลข้างเคียง อาจทำให้เกิดงานซ้ำ ควรออกแบบเรื่อง idempotency ตามระบบนั้น
#กำหนดอายุของ Resource
เมื่อ Decorator ห่อ stream, connection หรือ resource อื่น ต้องระบุว่าการปิดชั้นนอกปิดชั้นในด้วยหรือไม่ และใครเป็นเจ้าของ resource
#เข้าใจว่ากำลังใช้ Object คนละตัว
new Cheese(base) เป็น object ใหม่ มี identity ต่างจาก base และไม่ได้เป็น instance ของ MargheritaPizza โดยตรง จึงควรเรียกผ่าน Pizza หลีกเลี่ยงการ cast เพื่อค้นหาทุกชั้น
#ไม่ใช้ Pattern เกินความจำเป็น
หากมีความสามารถเพิ่มเติมเพียงอย่างเดียวและไม่ต้องประกอบซ้อน เมธอดธรรมดาหรือคลาส wrapper เดียวอาจพอ หากเพิ่มเพียงข้อมูลท็อปปิง รายการข้อมูลอาจดูแลง่ายกว่า Decorator หลายสิบคลาส
#13. แนวทางตรวจสอบความถูกต้อง
สำหรับตัวอย่างในบทความนี้ ให้ตรวจพฤติกรรมที่สังเกตได้ดังต่อไปนี้
| กรณี | ผลที่ควรได้ |
|---|---|
| พิซซ่าพื้นฐาน | 100.00 บาท |
| เพิ่มชีส | 125.00 บาท |
| เพิ่มเห็ด | 115.00 บาท |
| เพิ่มชีสและเห็ด | 140.00 บาท |
| เพิ่มชีสสองครั้ง | 150.00 บาท |
| เพิ่มชีส เห็ด และมะกอก | 160.00 บาท |
| สลับลำดับชีสกับเห็ด | ราคาเท่าเดิม แต่รายละเอียดเรียงต่างกัน |
อ่านราคาจาก base หลังสร้าง wrapper |
ยังเป็น 100.00 บาทใน implementation นี้ |
สร้าง new Cheese(null) |
เกิด NullPointerException ตอนสร้าง |
เมื่อตรวจค่าตัวเลขของ BigDecimal ให้เลือกตามเจตนา: compareTo เปรียบเทียบค่าตัวเลข ขณะที่ equals พิจารณา scale ด้วย เช่น 140.0 และ 140.00 มีค่าตัวเลขเท่ากัน แต่ equals ให้ผลเป็น false [3]
ตัวอย่างเงื่อนไขตรวจค่าตัวเลขภายใน main:
if (loaded.price().compareTo(new BigDecimal("140.00")) != 0) {
throw new AssertionError("Loaded pizza should cost 140.00");
}
สำหรับระบบที่มีผลข้างเคียง ควรเพิ่มกรณีตรวจลำดับการเรียก จำนวนครั้งที่เรียก และพฤติกรรมเมื่อชั้นด้านในล้มเหลวด้วย
#14. แบบฝึกหัด
- เพิ่ม
Oliveแล้วตรวจราคาชีส + เห็ด + มะกอกว่าได้ 160.00 บาท - เพิ่ม
ExtraSauceราคา 10.00 บาท โดยใช้PizzaDecoratorเดิม - เปลี่ยน
createOrderให้รับจำนวนชีส แล้วใช้ loop ประกอบหลายชั้น - ออกแบบส่วนลด 10% และส่วนลด 10 บาท ทดลองสลับลำดับ พร้อมกำหนดการปัดเศษให้ชัดเจน
- อธิบายว่าเหตุใด
new Cheese(base)จึงไม่เปลี่ยนbase.price()ในตัวอย่างนี้ และจะเกิดอะไรขึ้นหากbaseมี setter เปลี่ยนราคา
เกณฑ์ตรวจงาน: ความสามารถใหม่ต้องรับ Pizza เป็น object ที่ห่อ ส่งต่อพฤติกรรมที่ยังต้องใช้ และยังเรียกผ่านชนิด Pizza ได้
#15. ทบทวนแนวคิด
Decorator ใช้การห่อ object ที่มีสัญญาร่วมกันเพื่อเพิ่มพฤติกรรมเป็นชั้น ๆ ช่วยให้ประกอบความสามารถของแต่ละ instance ขณะโปรแกรมทำงานได้ โดยไม่สร้าง subclass สำหรับทุกชุดตัวเลือก
สำหรับตัวอย่างนี้ Pizza คือสัญญา MargheritaPizza คือพฤติกรรมพื้นฐาน และ Cheese กับ Mushroom คือชั้นที่ส่งต่อพร้อมเพิ่มงาน เมื่อใช้งานจริงต้องใส่ใจกับลำดับ การเพิ่มซ้ำ สถานะที่แชร์กัน และสัญญาเรื่องความล้มเหลวหรือ resource
#เอกสารอ้างอิง
[1] Oracle. FilterInputStream — Java SE 21 API. อธิบายการห่อ stream การส่งต่อ และการปิด stream ด้านใน
[2] Oracle. BufferedInputStream — Java SE 21 API. อธิบายการเพิ่มความสามารถ buffering ให้ input stream
[3] Oracle. BigDecimal — Java SE 21 API. อธิบาย constructor, การคำนวณทศนิยม และความแตกต่างระหว่าง equals กับ compareTo
หมายเหตุ: โค้ดพิซซ่าเป็นตัวอย่างที่จัดทำขึ้นเพื่อการเรียนรู้ ส่วนตัวอย่าง Java I/O อ้างอิงโครงสร้างและพฤติกรรมจากเอกสาร Oracle ข้างต้น