Strategy Pattern (Mẫu Strategy)
Định nghĩa (Definition)
Strategy định nghĩa một họ thuật toán, đóng gói từng cái, và làm chúng có thể hoán đổi (Interchangeable). Strategy cho phép thuật toán thay đổi độc lập với client sử dụng nó.
Ví dụ Coffee Shop
Quán cà phê hỗ trợ nhiều chiến lược giá (Pricing Strategy) — giá thường, giảm giá giờ vàng (Happy Hour), giá thành viên thân thiết (Loyalty), và khuyến mãi theo mùa. Thay vì khối if/else khổng lồ, mỗi strategy là một class riêng. Cashier ủy quyền giá cho strategy đang hoạt động.
Cấu trúc (Structure)
Strategy Interface
public interface IPricingStrategy
{
decimal CalculatePrice(decimal basePrice, CupSize size);
string GetName();
}
Concrete Strategy
public class RegularPricing : IPricingStrategy
{
private static readonly Dictionary<CupSize, decimal> _markup = new()
{
[CupSize.Small] = 0.0m,
[CupSize.Medium] = 0.50m,
[CupSize.Large] = 1.00m,
};
public decimal CalculatePrice(decimal basePrice, CupSize size) =>
basePrice + _markup[size];
public string GetName() => "Regular Pricing";
}
public class HappyHourPricing : IPricingStrategy
{
private readonly decimal _discount;
public HappyHourPricing(decimal discount = 0.30m)
{
_discount = discount;
}
public decimal CalculatePrice(decimal basePrice, CupSize size) =>
basePrice * (1 - _discount); // Giảm giá flat, không quan tâm size
public string GetName() => $"Happy Hour (giảm {_discount:P0})";
}
public class LoyaltyPricing : IPricingStrategy
{
private static readonly Dictionary<CupSize, decimal> _markup = new()
{
[CupSize.Small] = 0.0m,
[CupSize.Medium] = 0.50m,
[CupSize.Large] = 1.00m,
};
public decimal CalculatePrice(decimal basePrice, CupSize size)
{
var price = basePrice + _markup[size];
return price * 0.9m; // Giảm 10% cho thành viên thân thiết
}
public string GetName() => "Loyalty Member (giảm 10%)";
}
public class SeasonalPricing : IPricingStrategy
{
private readonly string _season;
private readonly decimal _surcharge;
public SeasonalPricing(string season, decimal surcharge)
{
_season = season;
_surcharge = surcharge;
}
public decimal CalculatePrice(decimal basePrice, CupSize size) =>
basePrice + _surcharge;
public string GetName() => $"Mùa {_season} (+${_surcharge:F2})";
}
Context — Thu ngân (The Cashier)
public class Cashier
{
private IPricingStrategy _strategy;
public Cashier(IPricingStrategy strategy)
{
_strategy = strategy;
}
public void SetStrategy(IPricingStrategy strategy) => _strategy = strategy;
public void Charge(string coffee, decimal basePrice, CupSize size)
{
var finalPrice = _strategy.CalculatePrice(basePrice, size);
Console.WriteLine($"[{_strategy.GetName()}] {size} {coffee}: ${basePrice:F2} → ${finalPrice:F2}");
}
}
Client — Hoán đổi Strategy lúc Runtime
var cashier = new Cashier(new RegularPricing());
// Buổi sáng — giá thường
cashier.Charge("Latte", 4.00m, CupSize.Medium);
cashier.Charge("Espresso", 2.50m, CupSize.Small);
// 2-4 chiều — giờ vàng (Happy Hour)!
cashier.SetStrategy(new HappyHourPricing(0.30m));
cashier.Charge("Latte", 4.00m, CupSize.Medium);
cashier.Charge("Espresso", 2.50m, CupSize.Small);
// Thành viên thân thiết bước vào
cashier.SetStrategy(new LoyaltyPricing());
cashier.Charge("Latte", 4.00m, CupSize.Large);
// Giá mùa đông
cashier.SetStrategy(new SeasonalPricing("Đông", 0.50m));
cashier.Charge("Latte", 4.00m, CupSize.Medium);
Cashier không biết giá được tính như thế nào. Nó ủy quyền cho strategy được inject. Thêm quy tắc giá mới = tạo class mới — không cần sửa Cashier (OCP).
Strategy vs State
| Strategy | State | |
|---|---|---|
| Mục đích | Hoán đổi thuật toán | Thay đổi hành vi dựa trên internal state |
| Ai thay đổi | Client (bên ngoài) | Object tự thay đổi (bên trong) |
| Kiến thức | Client biết chọn strategy nào | Client không biết về state transition |
| Ví dụ | Chọn phương thức thanh toán | Máy bán hàng tự động chuyển chế độ |
Sử dụng thực tế trong .NET (.NET Real-World Usage)
IComparer<T>— hoán đổi strategy sắp xếp mà không đổi thuật toán sortILoggerprovider — các strategy logging khác nhau (console, file, cloud)IConfigurationprovider — các nguồn configuration khác nhau (JSON, env, Azure Key Vault)- ASP.NET Core authentication scheme — các strategy auth khác nhau per request
List<T>.Sort(IComparer<T>)— Strategy kinh điển trong BCL
Khi nào sử dụng (When to Use)
- Có nhiều biến thể thuật toán và cần hoán đổi lúc runtime
- Muốn tránh logic điều kiện phức tạp (
if/switch) để chọn thuật toán - Thuật toán cần tái sử dụng trong nhiều context khác nhau
Khi nào KHÔNG sử dụng (When NOT to Use)
- Chỉ có một thuật toán — không cần abstraction
- Thuật toán hiếm khi thay đổi — method đơn giản là đủ
- Strategy chỉ khác nhau ở parameter — dùng một class với configuration
Điểm chính (Key Takeaways)
- Strategy đóng gói mỗi thuật toán phía sau interface chung
- Thuật toán được hoán đổi lúc runtime — không cần sửa code
- Loại bỏ khối điều kiện lớn — mỗi nhánh trở thành một strategy class
- Context ủy quyền cho strategy — không biết chi tiết implementation
Câu hỏi phỏng vấn (Interview Questions)
Q: Strategy khác Template Method như thế nào? Strategy dùng composition — inject các object thuật toán khác nhau. Template Method dùng inheritance — subclass override từng bước cụ thể. Strategy linh hoạt hơn; Template Method đơn giản hơn khi cấu trúc thuật toán cố định.
Q: Strategy liên quan đến Dependency Injection như thế nào? Trong ASP.NET Core, đăng ký strategy trong DI:
services.AddScoped<IPricingStrategy, HappyHourPricing>();
Để đổi strategy động, inject IEnumerable<IPricingStrategy> và chọn lúc runtime, hoặc dùng factory.
Q: Nhược điểm của Strategy là gì? Client phải biết các strategy có sẵn để chọn đúng. Nếu có nhiều strategy, logic quyết định có thể phức tạp. Factory hoặc Strategy Registry có thể giúp.
Chủ đề liên quan (Related Topics)
- Template Method — khung thuật toán qua inheritance
- State — cấu trúc tương tự, hành vi được điều khiển bởi internal state
- Factory Method — thường dùng để tạo strategy object
- SOLID — OCP — strategy mới không làm hỏng code hiện có