Decorator Pattern (Mẫu Decorator)
Định nghĩa (Definition)
Decorator gắn thêm trách nhiệm bổ sung (Additional Responsibilities) cho object động. Nó cung cấp thay thế linh hoạt cho subclassing khi cần mở rộng chức năng. Decorator bao object gốc và thêm hành vi trước/sau khi ủy quyền (Delegate) cho nó.
Ví dụ Coffee Shop
Khách hàng gọi cà phê cơ bản (Espresso, Latte) và thêm topping (Extra Shot, Whipped Cream, Oat Milk, Caramel Syrup). Mỗi topping thêm giá và thay đổi mô tả. Thay vì tạo EspressoWithWhippedCreamAndOatMilk, chúng ta bao object động.
Cấu trúc (Structure)
Component Interface
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
Concrete Component — Cà phê cơ bản (Base Coffees)
public class Espresso : ICoffee
{
public string GetDescription() => "Espresso";
public decimal GetCost() => 2.50m;
}
public class Latte : ICoffee
{
public string GetDescription() => "Latte";
public decimal GetCost() => 4.00m;
}
public class ColdBrew : ICoffee
{
public string GetDescription() => "Cold Brew";
public decimal GetCost() => 3.75m;
}
Base Decorator
public abstract class CoffeeDecorator : ICoffee
{
protected readonly ICoffee _coffee;
protected CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}
Concrete Decorator — Topping
public class ExtraShot : CoffeeDecorator
{
public ExtraShot(ICoffee coffee) : base(coffee) { }
public override string GetDescription() =>
$"{_coffee.GetDescription()} + Extra Shot";
public override decimal GetCost() =>
_coffee.GetCost() + 0.75m;
}
public class WhippedCream : CoffeeDecorator
{
public WhippedCream(ICoffee coffee) : base(coffee) { }
public override string GetDescription() =>
$"{_coffee.GetDescription()} + Whipped Cream";
public override decimal GetCost() =>
_coffee.GetCost() + 0.50m;
}
public class OatMilk : CoffeeDecorator
{
public OatMilk(ICoffee coffee) : base(coffee) { }
public override string GetDescription() =>
$"{_coffee.GetDescription()} + Oat Milk";
public override decimal GetCost() =>
_coffee.GetCost() + 0.60m;
}
public class CaramelSyrup : CoffeeDecorator
{
public CaramelSyrup(ICoffee coffee) : base(coffee) { }
public override string GetDescription() =>
$"{_coffee.GetDescription()} + Caramel Syrup";
public override decimal GetCost() =>
_coffee.GetCost() + 0.40m;
}
Client — Xây dựng đơn hàng (Building the Order)
// Đơn giản: chỉ Latte
ICoffee order1 = new Latte();
Console.WriteLine($"{order1.GetDescription()} — ${order1.GetCost():F2}");
// Latte — $4.00
// Fancy: Latte + Oat Milk + Extra Shot + Whipped Cream
ICoffee order2 = new WhippedCream(
new ExtraShot(
new OatMilk(
new Latte())));
Console.WriteLine($"{order2.GetDescription()} — ${order2.GetCost():F2}");
// Latte + Oat Milk + Extra Shot + Whipped Cream — $5.85
// Cold Brew với Caramel
ICoffee order3 = new CaramelSyrup(new ColdBrew());
Console.WriteLine($"{order3.GetDescription()} — ${order3.GetCost():F2}");
// Cold Brew + Caramel Syrup — $4.15
Cú pháp constructor lồng nhau (new W(new X(new Y(new Z())))) rất khó đọc khi có nhiều lớp. Cân nhắc dùng Builder hoặc extension method cho fluent construction:
// Fluent approach với extension method
ICoffee order = new Latte()
.WithOatMilk()
.WithExtraShot()
.WithWhippedCream();
public static class CoffeeExtensions
{
public static ICoffee WithExtraShot(this ICoffee c) => new ExtraShot(c);
public static ICoffee WithWhippedCream(this ICoffee c) => new WhippedCream(c);
public static ICoffee WithOatMilk(this ICoffee c) => new OatMilk(c);
public static ICoffee WithCaramelSyrup(this ICoffee c) => new CaramelSyrup(c);
}
Decorator vs Inheritance Explosion
| Cách tiếp cận | Class cần cho 3 cà phê + 4 topping |
|---|---|
| Inheritance (mọi tổ hợp) | 3 × 2⁴ = 48 class |
| Decorator | 3 cà phê + 4 decorator = 7 class |
Sử dụng thực tế trong .NET (.NET Real-World Usage)
Streamdecorator —BufferedStream,GZipStream,CryptoStreambao stream khácHttpClienthandler —DelegatingHandlerchain trong ASP.NET Core- ASP.NET Core Middleware — mỗi middleware bao middleware tiếp theo
IAsyncEnumerablevớiConfigureAwaitvà cancellation — chuỗi decorator-like
Khi nào sử dụng (When to Use)
- Cần thêm hành vi cho object động lúc runtime
- Muốn tránh subclass cố định cho mọi tổ hợp feature
- Thêm và xóa trách nhiệm có thể thực hiện mà không thay đổi code hiện có
Khi nào KHÔNG sử dụng (When NOT to Use)
- Số lượng tổ hợp cố định và ít — class đơn giản hơn
- Chuỗi decorator quá sâu — debug trở nên khó khăn
- Hành vi thêm không composable (decorator xung đột với nhau)
Điểm chính (Key Takeaways)
- Decorator bao object và ủy quyền (Delegate) cho nó, thêm hành vi trước/sau
- Cả component và decorator implement cùng interface
- Decorator có thể xếp chồng (Stacked) theo bất kỳ thứ tự và số lượng
- Đây là OCP trong thực tế — mở rộng hành vi mà không sửa class hiện có
Câu hỏi phỏng vấn (Interview Questions)
Q: Decorator khác Adapter như thế nào? Decorator mở rộng hành vi mà không thay đổi interface. Adapter thay đổi interface để làm các thứ không tương thích hoạt động cùng nhau. Cả hai đều bao object, nhưng mục đích khác nhau.
Q: Decorator khác Proxy như thế nào? Về cấu trúc chúng giống hệt nhau — cả hai đều bao object. Nhưng Decorator thêm hành vi, trong khi Proxy kiểm soát truy cập (lazy loading, access control, caching). Proxy thường quản lý vòng đời (Lifecycle) của object bị bao; Decorator thì không.
Q: Mối liên hệ giữa Decorator và middleware trong ASP.NET Core là gì?
ASP.NET Core middleware chính là Decorator pattern. Mỗi middleware bao RequestDelegate tiếp theo, thêm hành vi trước và sau khi request đi qua pipeline.
Chủ đề liên quan (Related Topics)
- Adapter — thay đổi interface, không thêm hành vi
- Proxy — kiểm soát truy cập, cấu trúc bao tương tự
- Composite — cấu trúc cây tương tự, nhưng tổng hợp thay vì thêm hành vi
- Builder — có thể giúp xây dựng chuỗi decorator phức tạp fluent
- SOLID — OCP — Decorator là pattern OCP kinh điển