Chuyển tới nội dung chính

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);
mẹo

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

StrategyState
Mục đíchHoán đổi thuật toánThay đổi hành vi dựa trên internal state
Ai thay đổiClient (bên ngoài)Object tự thay đổi (bên trong)
Kiến thứcClient biết chọn strategy nàoClient không biết về state transition
Ví dụChọn phương thức thanh toánMá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 sort
  • ILogger provider — các strategy logging khác nhau (console, file, cloud)
  • IConfiguration provider — 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.

  • 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ó