1. Decorator Pattern là gì?
Decorator Pattern (Mẫu trang trí) thuộc nhóm Structural Pattern (Nhóm Cấu trúc) trong 23 mẫu thiết kế của GoF.
Mẫu thiết kế này cho phép bạn linh hoạt bổ sung các hành vi, tính năng mới cho một đối tượng (object) tại thời điểm chạy (runtime) mà không làm thay đổi cấu trúc của lớp (class) đó hay ảnh hưởng đến các đối tượng khác cùng lớp.
Nói một cách dễ hiểu, Decorator hoạt động như một lớp vỏ bọc (Wrapper). Bạn lấy một đối tượng gốc, đặt nó vào trong một đối tượng “trang trí” (Decorator) chứa các hành vi mới.
2. Tại sao lại cần Decorator Pattern? (Bài toán thực tế)
Hãy tưởng tượng bạn đang viết phần mềm tính tiền cho một quán cà phê. Bạn có một lớp gốc là Coffee.
Khách hàng không chỉ uống cà phê đen, họ muốn thêm: Sữa, Đường, Trân châu, Kem… Nếu sử dụng phương pháp Kế thừa (Inheritance) thông thường, bạn sẽ phải tạo ra vô số lớp con để đáp ứng mọi tổ hợp:
CoffeeWithMilkCoffeeWithSugarCoffeeWithMilkAndSugarCoffeeWithMilkSugarAndIceCream…
Hậu quả: Tình trạng “Bùng nổ tổ hợp” (Combinatorial Explosion) khiến hệ thống phình to, code bị lặp lại và cực kỳ khó bảo trì. Decorator Pattern ra đời để thay thế cho sự cứng nhắc của tính kế thừa bằng sự linh hoạt của Composition (Gộp nhóm).
3. Cấu trúc của Decorator Pattern
Một hệ thống áp dụng Decorator Pattern thường có 4 thành phần chính:
Component (Giao diện chung): Định nghĩa một interface hoặc abstract class chung cho cả đối tượng gốc và các lớp trang trí (Decorator).
Concrete Component (Đối tượng gốc): Lớp triển khai Component. Đây là đối tượng cơ bản mà bạn sẽ thêm tính năng vào (Ví dụ: Cà phê đen).
Decorator (Lớp trang trí gốc): Một abstract class triển khai Component và chứa một tham chiếu (reference) đến một đối tượng Component.
Concrete Decorator (Lớp trang trí cụ thể): Các lớp kế thừa từ Decorator. Chúng ghi đè các phương thức của Component, gọi phương thức của đối tượng đang được bọc, và thực hiện thêm các hành vi phụ (Ví dụ: Lớp thêm Sữa, Lớp thêm Đường).
4. Ví dụ thực tế (Kèm Code Java)
Dưới đây là cách mô phỏng bài toán quán cà phê bằng Decorator Pattern sử dụng ngôn ngữ Java.
Bước 1: Tạo Component chung
public interface Beverage {
String getDescription();
double cost();
}
Bước 2: Tạo Concrete Component (Đối tượng gốc)
public class Espresso implements Beverage {
@Override
public String getDescription() {
return "Cà phê Espresso";
}
@Override
public double cost() {
return 30000; // 30,000 VNĐ
}
}
Bước 3: Tạo lớp Base Decorator
public abstract class CondimentDecorator implements Beverage {
protected Beverage beverage; // Chứa tham chiếu đến đối tượng cần bọc
public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}
}
Bước 4: Tạo các Concrete Decorator (Tính năng được thêm vào)
// Lớp thêm Sữa
public class Milk extends CondimentDecorator {
public Milk(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + ", Sữa";
}
@Override
public double cost() {
return beverage.cost() + 5000; // Cộng thêm 5,000 VNĐ tiền sữa
}
}
// Lớp thêm Đường
public class Sugar extends CondimentDecorator {
public Sugar(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + ", Đường";
}
@Override
public double cost() {
return beverage.cost() + 2000; // Cộng thêm 2,000 VNĐ tiền đường
}
}
Bước 5: Chạy thử chương trình (Client)
public class CoffeeShop {
public static void main(String[] args) {
// 1. Khách gọi 1 ly Espresso gốc
Beverage myCoffee = new Espresso();
// 2. Khách muốn thêm Sữa
myCoffee = new Milk(myCoffee);
// 3. Khách muốn thêm Đường
myCoffee = new Sugar(myCoffee);
System.out.println("Món của bạn: " + myCoffee.getDescription());
System.out.println("Tổng tiền: " + myCoffee.cost() + " VNĐ");
}
}
/* KẾT QUẢ ĐẦU RA:
Món của bạn: Cà phê Espresso, Sữa, Đường
Tổng tiền: 37000.0 VNĐ
*/
5. Đánh giá Ưu – Nhược điểm
| Ưu điểm | Nhược điểm |
| Tính linh hoạt cao: Thêm/bớt tính năng cho đối tượng lúc runtime (khi chạy) mà không cần can thiệp vào code cũ. | Tạo ra nhiều lớp nhỏ: Hệ thống có thể bị phân mảnh vì chứa quá nhiều lớp Decorator nhỏ lẻ. |
| Tránh bùng nổ lớp con (Subclass explosion): Thay thế được cho kế thừa nhiều tầng phức tạp. | Khó debug: Khi một đối tượng bị bọc bởi quá nhiều lớp (ví dụ: bọc 5-7 lớp Decorator), việc tìm ra lỗi sẽ tốn nhiều thời gian hơn. |
| Đảm bảo nguyên tắc SRP (Single Responsibility Principle): Mỗi lớp chỉ đảm nhiệm một hành vi cụ thể. | Khởi tạo đối tượng phức tạp hơn so với cách gọi thông thường. |
6. Khi nào nên dùng Decorator Pattern?
Bạn nên cân nhắc sử dụng Decorator Pattern trong các trường hợp sau:
Cần thêm tính năng cho các đối tượng một cách linh hoạt, không ảnh hưởng đến các đối tượng khác cùng lớp.
Cần thêm hoặc xóa bớt tính năng của đối tượng trong quá trình runtime.
Khi việc kế thừa (inheritance) trở nên bất khả thi hoặc không hợp lý (ví dụ: lớp bị đánh dấu là
finaltrong Java hoặcsealedtrong C#).
Tổng kết
Decorator Pattern là một “vũ khí” tuyệt vời giúp code của bạn trở nên dễ mở rộng (scalable) và tuân thủ chặt chẽ các nguyên tắc SOLID. Bằng cách hiểu khái niệm “vỏ bọc” (wrapper), bạn có thể dễ dàng thiết kế các hệ thống phức tạp như giao diện người dùng (UI), luồng dữ liệu (I/O Streams) hoặc các module phân quyền.





