Strategy Pattern trong C#: Func, DI, hay Interface?
Strategy pattern trong C# / .NET 10: khi Func delegate là đủ, khi interface inject xứng đáng, và Strategy khác State và Command thế nào.
Mục lục
- Strategy pattern giải quyết bài toán gì trong C#?
- Strategy giáo khoa trong .NET 10 trông thế nào?
- Khi nào Func delegate là đủ?
- Khi nào interface inject xứng đáng?
- Strategy kết hợp Factory Method để pick lúc runtime thế nào?
- Khi nào Strategy pattern bắn trật?
- So sánh Strategy với State và Command thế nào?
- Một ví dụ thật trong .NET 10 trông thế nào?
- Đọc tiếp gì trong series?
Shop có ba thuật toán discount hôm nay: PercentOff,
BuyOneGetOne, FreeShipping. Mai sẽ có cái thứ tư. Phiên bản
đầu là switch qua một string trong OrderTotalsCalculator.
Sáu tháng sau calculator phụ thuộc helper ngày, segment
customer, và bốn service khác — tất cả import chỉ vì một biến
thể discount nào đó dùng.
Strategy pattern là câu trả lời. Mỗi thuật toán thành object
riêng sau interface nhỏ. Caller chọn cái nào dùng; calculator
giữ sạch. C# hiện đại cho hai hình dạng dễ chịu: Func<> một
dòng cho strategy tầm thường, và interface inject cho cái phong
phú. Bài này nói về chọn giữa hai cái — và nhận ra bạn đã dùng
Strategy khoảnh khắc inject IComparer<T> vào sort.
Đây là bài chúng ta tham chiếu chéo từ Singleton, Factory Method, Bridge, Command, State, và một số khác.
Strategy pattern giải quyết bài toán gì trong C#?
Pattern xứng chỗ khi logic của caller biến đổi đúng ở một bước được định nghĩa rõ, và bước đó có nhiều implementation thay thế được. Ba hình dạng cụ thể:
- Rule discount / pricing. Percent-off, BOGO, free shipping, tiered. Calculator cart chạy bất cứ rule nào áp.
- Sort / so sánh.
IComparer<T>,IEqualityComparer<T>. Caller chọn so sánh; sort không bao giờ biết chi tiết. - Format serialise. Cùng data, JSON hay Protobuf hay CSV. Serialiser là Strategy.
Cái không phải bài toán Strategy: "hành vi đổi khi state transition" — đó là State. "Tôi muốn ghi action cho replay" — đó là Command. Strategy đặc biệt nói về một thuật toán, swappable bởi caller.
Strategy giáo khoa trong .NET 10 trông thế nào?
Một interface nhỏ và một class mỗi thuật toán:
public sealed record CartContext(string[] Skus, decimal Subtotal, string Region);
public interface IDiscountStrategy
{
decimal Apply(CartContext ctx);
}
public sealed class PercentOff(decimal Percent) : IDiscountStrategy
{
public decimal Apply(CartContext ctx) => ctx.Subtotal * (1m - Percent);
}
public sealed class FreeShipping : IDiscountStrategy
{
public decimal Apply(CartContext ctx) => ctx.Subtotal; // shipping được thêm sau
}
public sealed class BuyOneGetOne(string Sku, decimal UnitPrice) : IDiscountStrategy
{
public decimal Apply(CartContext ctx)
{
var count = ctx.Skus.Count(s => s == Sku);
return ctx.Subtotal - (count / 2) * UnitPrice;
}
}
Caller nhận strategy như dependency và chạy nó:
public sealed class CartCalculator
{
private readonly IDiscountStrategy _discount;
public CartCalculator(IDiscountStrategy discount) => _discount = discount;
public decimal GrandTotal(CartContext ctx) => _discount.Apply(ctx);
}
Bức tranh cấu trúc:
classDiagram
class IDiscountStrategy {
<<interface>>
+Apply(ctx) decimal
}
class PercentOff
class FreeShipping
class BuyOneGetOne
IDiscountStrategy <|.. PercentOff
IDiscountStrategy <|.. FreeShipping
IDiscountStrategy <|.. BuyOneGetOne
class CartCalculator
CartCalculator o-- IDiscountStrategy : dùng
Thêm strategy mới là một class mới cộng một dòng DI.
Khi nào Func delegate là đủ?
Cho strategy một-method tầm thường, interface là overhead.
Func<TInput, TOutput> là cùng pattern với một file ít hơn:
public sealed class CartCalculator
{
private readonly Func<CartContext, decimal> _discount;
public CartCalculator(Func<CartContext, decimal> discount) => _discount = discount;
public decimal GrandTotal(CartContext ctx) => _discount(ctx);
}
// Composition root
builder.Services.AddSingleton<Func<CartContext, decimal>>(ctx => ctx.Subtotal * 0.9m);
Đánh đổi thật: Func<> không có tên, không phát hiện được qua
reflection, không inject service vào tự nó được, và không unit
test được mà không khởi tạo closure. Nâng lên interface khoảnh
khắc bạn muốn bất kỳ cái nào trong số đó.
Khi nào interface inject xứng đáng?
Với interface khi ít nhất một trong các điều này đúng:
- Strategy cần dependency.
TaxStrategygọiIGeoServicekhông sống sạch trongFunc<>;Func<>close over dependency lúng túng. - Strategy có nhiều method. "Tính discount, cộng quyết định
có stack được, cộng tạo label" là ba method trên một
interface.
Func<>là một. - Strategy phải phát hiện được. Tooling (DI scanner,
serialiser, admin UI) cần tìm mọi implementation của
IDiscountStrategy. Lambda ẩn danh ẩn. - Strategy giữ state.
RateLimiterstrategy nhớ call gần đây là class, không phải delegate.
Strategy kết hợp Factory Method để pick lúc runtime thế nào?
Các strategy phía trên đều sống dạng đăng ký DI; caller chọn một theo key:
public enum DiscountKind { PercentOff, BuyOneGetOne, FreeShipping }
builder.Services.AddKeyedSingleton<IDiscountStrategy, PercentOff>(DiscountKind.PercentOff,
(_, _) => new PercentOff(0.10m));
builder.Services.AddKeyedSingleton<IDiscountStrategy, FreeShipping>(DiscountKind.FreeShipping,
(_, _) => new FreeShipping());
builder.Services.AddKeyedSingleton<IDiscountStrategy, BuyOneGetOne>(DiscountKind.BuyOneGetOne,
(_, _) => new BuyOneGetOne("BOOK", 9.99m));
// Caller
public sealed class Checkout
{
private readonly IKeyedServiceProvider _sp;
public Checkout(IKeyedServiceProvider sp) => _sp = sp;
public decimal ComputeTotal(DiscountKind kind, CartContext ctx)
{
var strategy = _sp.GetRequiredKeyedService<IDiscountStrategy>(kind);
return strategy.Apply(ctx);
}
}
Đây đúng là Factory Method
pattern — keyed service chọn
Strategy đúng lúc runtime. Hai pattern compose tự nhiên; nhận
ra cả hai cho phép bạn đọc call AddKeyedSingleton<> đúng.
Khi nào Strategy pattern bắn trật?
Ba bẫy:
- Strategy một-strategy. Một class concrete sau interface tầm thường là wrapper không thêm giá trị. Bỏ đến khi có biến thể thứ hai.
- Strategy mutate state share. Strategy được kỳ vọng stateless hoặc tự chứa. Mutate state share phá lời hứa thay thế.
- Strategy thực ra là state. Nếu Strategy tự chọn dựa trên giai đoạn lifecycle object, bạn có State thay vào đó. Bỏ chọn tay và dùng state machine.
So sánh Strategy với State và Command thế nào?
| Pattern | Đại diện cho | Ai chọn |
|---|---|---|
| Strategy | Một thuật toán giữa các peer | Caller / DI |
| State | Cả hành vi ở giai đoạn lifecycle | State machine của object |
| Command | Một hành động với input | Caller, gửi tới handler |
Tách rõ: Strategy là thế nào (thuật toán); State là là gì (lifecycle); Command là làm gì một lần (hành động với input). Strategy nhẹ nhất ba cái; nâng lên cái khác khi hình dạng thôi vừa.
Một ví dụ thật trong .NET 10 trông thế nào?
Hình dạng đầy đủ — strategy keyed cho discount, dùng bởi
calculator checkout, cộng Func<> cho discount shipping tầm
thường không cần class riêng:
public sealed record CartContext(string[] Skus, decimal Subtotal, string Region);
public interface IDiscountStrategy
{
decimal Apply(CartContext ctx);
}
public sealed class PercentOff : IDiscountStrategy
{
private readonly decimal _percent;
public PercentOff(decimal percent) => _percent = percent;
public decimal Apply(CartContext ctx) => ctx.Subtotal * (1m - _percent);
}
public sealed class TieredDiscount : IDiscountStrategy
{
private readonly IClock _clock; // dependency: delegate không inject sạch được
public TieredDiscount(IClock clock) => _clock = clock;
public decimal Apply(CartContext ctx)
{
var dayBoost = _clock.UtcNow.DayOfWeek == DayOfWeek.Monday ? 0.05m : 0m;
var baseRate = ctx.Subtotal switch
{
< 50m => 0.00m,
< 200m => 0.05m,
_ => 0.10m,
};
return ctx.Subtotal * (1m - baseRate - dayBoost);
}
}
// Composition root
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddKeyedSingleton<IDiscountStrategy, PercentOff>("flat10",
(_, _) => new PercentOff(0.10m));
builder.Services.AddKeyedSingleton<IDiscountStrategy, TieredDiscount>("tiered");
// Calculator shipping tầm thường - Func là đủ
builder.Services.AddSingleton<Func<decimal, decimal>>(subtotal =>
subtotal > 100m ? 0m : 7.99m);
// Caller
public sealed class Checkout
{
private readonly IKeyedServiceProvider _sp;
private readonly Func<decimal, decimal> _shipping;
public Checkout(IKeyedServiceProvider sp, Func<decimal, decimal> shipping)
=> (_sp, _shipping) = (sp, shipping);
public decimal Total(string discountKey, CartContext ctx)
{
var discounted = _sp.GetRequiredKeyedService<IDiscountStrategy>(discountKey).Apply(ctx);
return discounted + _shipping(discounted);
}
}
// Test
[Fact]
public void Tiered_discount_applies_5pct_in_50_to_200_range()
{
var clock = new Mock<IClock>();
clock.SetupGet(c => c.UtcNow).Returns(new DateTime(2026, 5, 5, 0, 0, 0, DateTimeKind.Utc)); // thứ Ba
var sut = new TieredDiscount(clock.Object);
var result = sut.Apply(new CartContext(Array.Empty<string>(), 150m, "US"));
Assert.Equal(142.5m, result); // 150 * (1 - 0.05)
}
Cái bạn đọc: mỗi strategy nhỏ, có dependency riêng hoặc không,
và test được riêng. Caller không bao giờ import PercentOff.
Thêm strategy mới là một class mới cộng một dòng DI.
Đọc tiếp gì trong series?
- Bài trước: State — cùng hình nhưng swap dựa trên lifecycle.
- Bài kế: Template Method — khi khung được share và chỉ một-hai bước biến đổi.
- Tham chiếu chéo: Factory Method — cách tự nhiên để chọn Strategy lúc runtime.
- Tham chiếu chéo: Command — khi bạn muốn hành động là data, không chỉ thuật toán.
- Cây quyết định: Cách chọn design pattern phù hợp.
Một quan sát thực dụng: Strategy có lẽ là pattern behavioral
dùng nhiều nhất trong .NET hiện đại, và gần như không ai gọi nó
bằng tên. Mọi IComparer<T> bạn truyền vào OrderBy, mọi
Func<> bạn inject, mọi keyed service đều là pattern. Một khi
thấy nó khắp nơi, câu hỏi thành "cái này nên là Func<> hay
interface?" — và đó là quyết định thiết kế duy nhất còn lại.
Câu hỏi thường gặp
Khi nào Func delegate là đủ so với interface IStrategy đầy đủ?
Func<> đủ khi strategy là một method, không có dependency, và không xứng unit test riêng. Interface xứng đáng khi strategy có nhiều method, giữ state, phụ thuộc service inject, hoặc cần phát hiện được khắp codebase. Với Func<> trước; nâng lên interface khi muốn inject ILogger vào delegate.Strategy khác State thế nào?
Nên cài Strategy hay dùng switch expression?
switch expression khi thuật toán nhỏ, đóng (bạn kiểm soát mọi biến thể), và không cần dependency riêng. Nâng lên Strategy khi bất kỳ cái nào vỡ: thuật toán bên thứ ba tham gia, biến thể cần IDateTimeProvider inject, hoặc bạn muốn mỗi biến thể test riêng. switch ổn; Strategy mua testability.Strategy kết hợp Factory Method thế nào?
IDiscountStrategy keyed theo region, rồi chạy .Apply(cart) trên instance trả về. Trong .NET 8+ đây đúng là IKeyedServiceProvider.GetKeyedService<IDiscountStrategy>(region).