Decorator
เพิ่มความสามารถให้ object แบบไดนามิกด้วยการห่อ
เวลาต้องการเพิ่มพฤติกรรมให้ object เช่น logging, caching, validation หรือ compression วิธีแรกที่หลายคนนึกถึงคือสร้าง subclass ใหม่ แต่ถ้าต้องรองรับหลายพฤติกรรมที่ผสมกันได้อิสระ (เช่น จะ log อย่างเดียว, จะ cache อย่างเดียว, หรือจะทั้ง log ทั้ง cache พร้อมกัน) การไล่สร้าง subclass จะเกิด combinatorial explosion — จำนวน class ระเบิดขึ้นแบบ exponential ตามจำนวนพฤติกรรมที่ต้องผสม
Decorator แก้ปัญหานี้ด้วยแนวคิดง่าย ๆ: สร้าง class wrapper ที่มี interface เดียวกับ object ต้นฉบับ แล้วให้ wrapper นั้นถือ reference ไปยัง object ที่มัน “ห่อ” อยู่ (เรียกว่า composition ไม่ใช่ inheritance) เวลาถูกเรียกใช้งาน decorator จะทำพฤติกรรมเสริมของตัวเองก่อนหรือหลัง แล้วค่อย delegate การเรียกไปยัง object ข้างในต่อ
เพราะ decorator กับ object ต้นฉบับมี interface เดียวกัน ผู้เรียกจึงไม่รู้และไม่จำเป็นต้องรู้ว่ากำลังคุยกับของจริงหรือของที่ถูกห่ออยู่ — และที่สำคัญคือ decorator หลายตัวสามารถห่อซ้อนกันได้ไม่จำกัดชั้น ทำให้ผสมพฤติกรรมต่าง ๆ เข้าด้วยกันได้อย่างอิสระในเวลารัน (runtime) โดยไม่ต้องแตะ code เดิมเลย
แนวคิดนี้เป็นวิธีที่ดีเยี่ยมในการทำตาม SRP เพราะ cross-cutting concern อย่าง Logging, Validation, Authorization ถูกดึงออกมาไว้ใน decorator เฉพาะทางแทนที่จะยัดรวมอยู่ใน class หลัก และช่วยบรรลุ OCP เพราะเพิ่ม function ให้ class เดิมได้โดยไม่ต้องแก้ไขมัน
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”Decorator ประกอบด้วยองค์ประกอบหลัก 4 ส่วน: Component (interface ร่วม), ConcreteComponent (ของจริง), Decorator (abstract class ที่ถือ reference ไปยัง Component และ implement interface เดียวกัน) และ ConcreteDecorator (class decorator เฉพาะทางแต่ละตัวที่เพิ่มพฤติกรรมของตัวเอง)
classDiagram
class IService {
<<interface>>
+Do()
}
class RealService {
+Do()
}
class ServiceDecorator {
<<abstract>>
-IService inner
+Do()
}
class LoggingDecorator {
+Do()
}
class ValidationDecorator {
+Do()
}
IService <|.. RealService
IService <|.. ServiceDecorator
ServiceDecorator o-- IService : inner
ServiceDecorator <|-- LoggingDecorator
ServiceDecorator <|-- ValidationDecorator
ข้อสังเกตสำคัญของโครงสร้างนี้คือ ServiceDecorator implement IService เดียวกับที่มันถือ reference อยู่ข้างใน (inner) — ความสัมพันธ์แบบวงกลมนี้เองที่ทำให้ห่อซ้อนกันได้หลายชั้น เช่น
new LoggingDecorator( new ValidationDecorator( new RealService() ) )เวลาเรียก Do() ที่ตัวนอกสุด มันจะไล่ผ่านทุกชั้นเข้าไปจนถึง RealService แล้วผลลัพธ์ก็ไหลย้อนกลับออกมาผ่านทุกชั้นเช่นกัน
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”การทำงานของ Decorator คือห่วงโซ่ของการ delegate ทีละชั้น แต่ละชั้นมีโอกาสแทรกพฤติกรรม “ก่อน” (before) และ “หลัง” (after) การเรียก inner object
sequenceDiagram
participant Client
participant Logging as LoggingDecorator
participant Validation as ValidationDecorator
participant Real as RealService
Client->>Logging: Do()
Logging->>Logging: บันทึก log ก่อนเรียก
Logging->>Validation: Do()
Validation->>Validation: ตรวจสอบข้อมูลก่อนเรียก
Validation->>Real: Do()
Real-->>Validation: ผลลัพธ์
Validation-->>Logging: ผลลัพธ์
Logging->>Logging: บันทึก log หลังเรียก
Logging-->>Client: ผลลัพธ์
จุดที่ควรสังเกต:
- ผู้เรียก (Client) ไม่รู้จำนวนหรือชนิดของ decorator ที่ห่ออยู่ — มันเห็นแค่
IServiceเดียวเท่านั้น - ลำดับการห่อมีผลต่อพฤติกรรม — ห่อ
Logging(Validation(...))กับValidation(Logging(...))ให้ผลต่างกัน เช่น ถ้า validation ล้มเหลวและ throw exception การห่อแบบแรก log จะถูกบันทึกก่อนที่ validation จะทำงาน ส่วนแบบหลัง log จะไม่ถูกบันทึกเลยถ้า validation ล้มเหลวก่อน - แต่ละ decorator ควรทำหน้าที่เดียว ตาม SRP — อย่ายัด logging กับ caching ไว้ใน decorator ตัวเดียวกัน ให้แยกเป็นคนละตัวแล้วค่อยห่อรวมกัน
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างนี้แสดงการห่อ IOrderService ด้วย decorator สองชั้น คือ logging และ caching โดยไม่แก้ code OrderService เดิมเลย
public interface IOrderService{ Order GetById(int id); void Place(Order order);}
// ConcreteComponent — logic ทางธุรกิจล้วน ๆ ไม่ปนกับ cross-cutting concernpublic class OrderService : IOrderService{ private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository) => _repository = repository;
public Order GetById(int id) => _repository.Find(id);
public void Place(Order order) => _repository.Save(order);}
// Decorator ฐาน — ถือ inner object และ implement interface เดียวกันpublic abstract class OrderServiceDecorator : IOrderService{ protected readonly IOrderService Inner;
protected OrderServiceDecorator(IOrderService inner) => Inner = inner;
public virtual Order GetById(int id) => Inner.GetById(id);
public virtual void Place(Order order) => Inner.Place(order);}
// ConcreteDecorator #1 — เพิ่ม logging ก่อน/หลัง delegatepublic class LoggingOrderService : OrderServiceDecorator{ private readonly ILogger _log;
public LoggingOrderService(IOrderService inner, ILogger log) : base(inner) => _log = log;
public override void Place(Order order) { _log.Info($"กำลังสั่งซื้อออเดอร์ {order.Id}"); // พฤติกรรมเสริมก่อนเรียก Inner.Place(order); // delegate ไปของเดิม _log.Info($"สั่งซื้อออเดอร์ {order.Id} สำเร็จ"); // พฤติกรรมเสริมหลังเรียก }}
// ConcreteDecorator #2 — เพิ่ม caching เฉพาะฝั่งอ่านpublic class CachingOrderService : OrderServiceDecorator{ private readonly IMemoryCache _cache;
public CachingOrderService(IOrderService inner, IMemoryCache cache) : base(inner) => _cache = cache;
public override Order GetById(int id) { // ถ้ามีใน cache แล้วไม่ต้อง delegate ไป inner เลย if (_cache.TryGetValue(id, out Order cached)) return cached;
var order = Inner.GetById(id); _cache.Set(id, order, TimeSpan.FromMinutes(5)); return order; }}
// ผู้เรียกใช้: ห่อซ้อนกันได้อิสระ — เปลี่ยนลำดับหรือถอดออกได้โดยไม่แตะ OrderServiceIOrderService service = new LoggingOrderService( new CachingOrderService( new OrderService(repository), cache), logger);
service.Place(order); // ผ่าน Logging -> Caching (ไม่มีผลต่อ Place) -> OrderService จริงใน project จริงที่ใช้ ASP.NET Core DI container มักจะประกอบ decorator ตอน register service (เช่นด้วย library Scrutor) แทนที่จะ new ต่อกันด้วยมือแบบนี้ แต่หลักการห่อซ้อนยังเหมือนเดิม
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ต้องการเพิ่มพฤติกรรมให้ object บางตัว แบบไดนามิกในเวลารัน โดยไม่กระทบ instance อื่นของ class เดียวกัน
- มี cross-cutting concern อย่าง logging, caching, validation, authorization, retry, compression ที่อยากแยกออกจาก logic หลักตาม SRP
- ต้องการผสมพฤติกรรมหลายแบบเข้าด้วยกันได้อย่างอิสระ โดยไม่อยากให้จำนวน class ระเบิดจากการทำ subclass ทุกชุดค่าผสม
- class ต้นฉบับถูก seal หรือ final แก้ไข/สืบทอดไม่ได้ แต่ยังต้องการเพิ่มพฤติกรรม
- ต้องการรักษา OCP — เพิ่มความสามารถโดยไม่แก้ code เดิมที่ผ่านการทดสอบแล้ว
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- พฤติกรรมที่จะเพิ่มนั้นคงที่และรู้ล่วงหน้าตั้งแต่ compile-time ไม่ได้เปลี่ยนตาม instance — การแก้ class เดิมหรือใช้ inheritance ธรรมดาอาจง่ายและอ่านง่ายกว่า
- ลำดับการห่อ decorator ส่งผลต่อความถูกต้องของระบบจนทำให้ debug ยาก และทีมไม่มีวินัยจัดการลำดับให้ชัดเจน
- ต้องการ “ถอด” decorator เฉพาะตัวออกจาก stack ที่ห่อไว้แล้ว — Decorator ไม่ได้ออกแบบมาให้ inspect หรือถอดตัวกลาง ๆ ออกได้ง่าย
- จำนวนชั้นการห่อเยอะเกินไปจนตาม stack trace หรือ debug ยาก ควรพิจารณา middleware pipeline หรือ Chain of Responsibility ที่ให้ภาพรวมของลำดับชัดเจนกว่า
- โจทย์จริงคือการควบคุมการเข้าถึง (access control, lazy loading, remote proxy) มากกว่าการเพิ่มพฤติกรรม — กรณีนั้น Proxy ตรงจุดประสงค์กว่า
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | เพิ่ม/ถอดพฤติกรรมได้ในเวลารันโดยไม่แก้ class เดิม |
| ข้อดี | หลีกเลี่ยง combinatorial explosion ของ subclass จากการผสมพฤติกรรม |
| ข้อดี | แต่ละ decorator ทำหน้าที่เดียว ทดสอบและอ่านแยกกันได้ตาม SRP |
| ข้อดี | ผู้เรียกไม่ต้องรู้ว่ากำลังคุยกับของจริงหรือของที่ถูกห่อ (transparent) |
| ข้อเสีย | มีหลาย object ขนาดเล็กเกิดขึ้น ทำให้ debug และตาม stack ยากขึ้น |
| ข้อเสีย | ลำดับการห่อมีผลต่อพฤติกรรม ต้องเข้าใจก่อนประกอบ stack |
| ข้อเสีย | code ตอนประกอบ (composition root) อาจดูรกถ้าห่อหลายชั้นด้วยมือ |
| ข้อเสีย | ถอด decorator ตัวกลางออกจาก stack ที่ห่อไว้แล้วทำได้ยาก |