Hành vi Cơ bản 8 phút đọc

Template Method trong C#: Hook Thừa Kế vs Strategy

Template Method pattern trong C# / .NET 10: khi base abstract với hook đúng, khi Strategy inject thắng, và cách tránh lock-in thừa kế.

Mục lục
  1. Template Method pattern giải quyết bài toán gì trong C#?
  2. Template Method giáo khoa trong .NET 10 trông thế nào?
  3. BackgroundService của ASP.NET Core dùng Template Method thế nào?
  4. Khi nào nên thay Template Method bằng Strategy?
  5. Khi nào Template Method pattern bắn trật?
  6. So sánh Template Method với Strategy và State thế nào?
  7. Một ví dụ thật trong .NET 10 trông thế nào?
  8. Đọc tiếp gì trong series?

Ba luồng checkout: web, mobile, kiosk in-store. Cả ba load cart, validate inventory, charge thẻ, persist order, và gửi email — theo thứ tự đó. Khác biệt nhỏ. Web hiện trang xác nhận; mobile push notification; kiosk in receipt. Cám dỗ là ba method gần giống, 80% share, copy-paste.

Template Method pattern là câu trả lời GoF cho việc này. Đặt khung share trong base class abstract, định nghĩa hook method cho phần biến đổi, và để mỗi subclass điền hook. Pattern là cơm bữa của OO 1990s. Trong C# hiện đại nó cạnh tranh với composition (Strategy) và lựa chọn không tầm thường — bài này nói về khi nào cái nào thắng.

Template Method pattern giải quyết bài toán gì trong C#?

Pattern xứng chỗ khi vài biến thể share khung cố định và khác ở một-hai bước được đặt tên rõ. Ba hình dạng cụ thể:

  1. Method lifecycle. BackgroundService của ASP.NET Core định nghĩa start/stop/execute; bạn chỉ override ExecuteAsync.
  2. Thuật toán có thể cấu hình với hook bắt buộc. Importer base đọc file, parse mỗi row, validate, persist — chỉ parser đổi theo format file.
  3. Template thừa kế với bước optional. ReportBuilder base định nghĩa header/body/footer; subclass override chỉ cái cần.

Cái không phải bài toán Template Method: "Tôi muốn swap thuật toán lúc runtime" — đó là Strategy. "Tôi muốn hành vi khác theo giai đoạn lifecycle" — đó là State. Template Method đặc biệt nói về khung cố định, hook biến đổi, qua thừa kế.

Template Method giáo khoa trong .NET 10 trông thế nào?

Run() non-virtual public (template), một hoặc nhiều hook protected abstract hoặc protected virtual:

public abstract class CheckoutFlow
{
    protected readonly ICartRepository Carts;
    protected readonly IPaymentProcessor Payment;
    protected readonly IOrderRepository Orders;

    protected CheckoutFlow(ICartRepository carts, IPaymentProcessor payment, IOrderRepository orders)
        => (Carts, Payment, Orders) = (carts, payment, orders);

    // Template - non-virtual, định nghĩa thuật toán.
    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await Carts.GetAsync(req.CartId, ct);
        var charge = await Payment.PayAsync(cart.Total, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error ?? "Unknown");

        var order = await Orders.CreateAsync(cart, charge.TransactionId!, ct);

        await NotifyCustomerAsync(order, ct);     // hook
        return BuildResult(order);                // hook
    }

    protected abstract Task NotifyCustomerAsync(Order order, CancellationToken ct);
    protected abstract CheckoutResult BuildResult(Order order);
}

public sealed class WebCheckoutFlow : CheckoutFlow
{
    private readonly IEmailService _emails;
    public WebCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IEmailService emails)
        : base(carts, payment, orders) => _emails = emails;

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => _emails.SendOrderConfirmationAsync(order.CustomerEmail, order.Id, ct);

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, "/checkout/done?id=" + order.Id);
}

public sealed class KioskCheckoutFlow : CheckoutFlow
{
    private readonly IReceiptPrinter _printer;
    public KioskCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IReceiptPrinter printer)
        : base(carts, payment, orders) => _printer = printer;

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => _printer.PrintReceiptAsync(order, ct);

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, ConfirmationUrl: null);
}

Bức tranh cấu trúc:

classDiagram
    class CheckoutFlow {
        <<abstract>>
        +CheckoutAsync(req)
        #NotifyCustomerAsync(order)*
        #BuildResult(order)*
    }
    class WebCheckoutFlow
    class KioskCheckoutFlow
    class MobileCheckoutFlow
    CheckoutFlow <|-- WebCheckoutFlow
    CheckoutFlow <|-- KioskCheckoutFlow
    CheckoutFlow <|-- MobileCheckoutFlow

* đánh dấu member abstract; CheckoutAsync public là template. Thêm flow thứ tư là một subclass mới cộng hai implementation hook.

BackgroundService của ASP.NET Core dùng Template Method thế nào?

Microsoft.Extensions.Hosting.BackgroundService là Template Method kinh điển trong .NET hiện đại:

public sealed class OrderRetryService : BackgroundService
{
    private readonly IOrderRepository _orders;
    private readonly IPaymentProcessor _payment;
    public OrderRetryService(IOrderRepository orders, IPaymentProcessor payment)
        => (_orders, _payment) = (orders, payment);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var failed = await _orders.GetFailedRecentAsync(stoppingToken);
            foreach (var order in failed)
                await _payment.PayAsync(order.Total, order.CustomerId, order.Id, stoppingToken);
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Bạn override đúng một method — ExecuteAsync. Base class xử StartAsync, StopAsync, wire cancellation token, và đăng ký lifetime. Pattern vô hình vì quá quen; nhận ra nó cho biết MichelinController, DbContext.OnConfiguring, và hàng chục hook framework khác là gì.

Khi nào nên thay Template Method bằng Strategy?

Template Method là thừa kế; Strategy là composition. Thừa kế có vấn đề đã biết:

Thay bằng Strategy khi:

Tương đương composition của ví dụ checkout:

public sealed class CheckoutFlow
{
    private readonly ICartRepository _carts;
    private readonly IPaymentProcessor _payment;
    private readonly IOrderRepository _orders;
    private readonly INotificationStrategy _notify;
    private readonly IResultBuilder _builder;

    public CheckoutFlow(ICartRepository carts, IPaymentProcessor payment, IOrderRepository orders,
        INotificationStrategy notify, IResultBuilder builder)
        => (_carts, _payment, _orders, _notify, _builder) = (carts, payment, orders, notify, builder);

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await _carts.GetAsync(req.CartId, ct);
        var charge = await _payment.PayAsync(cart.Total, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded") throw new CheckoutFailedException(charge.Error ?? "Unknown");
        var order = await _orders.CreateAsync(cart, charge.TransactionId!, ct);
        await _notify.NotifyAsync(order, ct);
        return _builder.Build(order);
    }
}

Một class concrete, hai strategy, không thừa kế. Wiring là đăng ký DI: web flow lấy EmailNotification + WebResultBuilder, kiosk lấy PrintNotification + KioskResultBuilder.

Đánh đổi thật. Thừa kế của Template Method cho một class mỗi biến thể — đọc được, dễ navigate, đặt tên rõ. Composition cho linh hoạttestability với chi phí vài file và dòng DI nữa.

Khi nào Template Method pattern bắn trật?

Ba bẫy:

So sánh Template Method với Strategy và State thế nào?

Pattern Cơ chế biến đổi Coupling .NET hiện đại
Template Method Thừa kế (base + hook subclass) Chặt (subclass biết thuật toán base) BackgroundService, ControllerBase
Strategy Composition (inject implementation) Lỏng DI, Func<>
State Class tự thay theo giai đoạn lifecycle Nội bộ State machine

Tách rõ: Template Method là chúng ta share khung, bạn điền chỗ trống; Strategy là chúng ta share interface, bạn swap implementation; State là chúng ta đổi class theo cái đã xảy ra. Cả ba biến đổi hành vi; trục khác.

Một ví dụ thật trong .NET 10 trông thế nào?

Hình dạng thực dụng kết hợp Template Method (cho khung cố định) với strategy (cho phần cần cấu hình phong phú). Base class sở hữu thuật toán; hook ủy thác strategy khi biến thể có abstraction thật xứng đáng đặt tên:

public abstract class CheckoutFlow
{
    protected ICartRepository Carts { get; }
    protected IPaymentProcessor Payment { get; }
    protected IOrderRepository Orders { get; }

    protected CheckoutFlow(ICartRepository carts, IPaymentProcessor payment, IOrderRepository orders)
        => (Carts, Payment, Orders) = (carts, payment, orders);

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await Carts.GetAsync(req.CartId, ct)
            ?? throw new NotFoundException();
        var charge = await Payment.PayAsync(cart.Total, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error ?? "Unknown");

        var order = await Orders.CreateAsync(cart, charge.TransactionId!, ct);

        await OnPaidAsync(order, ct);                  // hook optional
        await NotifyCustomerAsync(order, ct);          // hook bắt buộc
        return BuildResult(order);                     // hook bắt buộc
    }

    protected virtual Task OnPaidAsync(Order order, CancellationToken ct) => Task.CompletedTask;
    protected abstract Task NotifyCustomerAsync(Order order, CancellationToken ct);
    protected abstract CheckoutResult BuildResult(Order order);
}

public sealed class WebCheckoutFlow : CheckoutFlow
{
    private readonly IEmailService _emails;
    public WebCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IEmailService emails)
        : base(carts, payment, orders) => _emails = emails;

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => _emails.SendOrderConfirmationAsync(order.CustomerEmail, order.Id, ct);

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, $"/checkout/done?id={order.Id}");
}

public sealed class KioskCheckoutFlow : CheckoutFlow
{
    private readonly IReceiptPrinter _printer;
    public KioskCheckoutFlow(ICartRepository carts, IPaymentProcessor payment,
        IOrderRepository orders, IReceiptPrinter printer)
        : base(carts, payment, orders) => _printer = printer;

    protected override async Task OnPaidAsync(Order order, CancellationToken ct)
        => await _printer.PrintReceiptAsync(order, ct);    // in *trước* khi thông báo

    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        => Task.CompletedTask;                             // đã in rồi

    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, ConfirmationUrl: null);
}

// Composition root
builder.Services.AddScoped<WebCheckoutFlow>();
builder.Services.AddScoped<KioskCheckoutFlow>();
// per-context: inject cái đúng dựa theo caller

// Test template qua subclass tối thiểu
public sealed class TestFlow : CheckoutFlow
{
    public TestFlow(ICartRepository c, IPaymentProcessor p, IOrderRepository o) : base(c, p, o) {}
    public string? Notified { get; private set; }
    protected override Task NotifyCustomerAsync(Order order, CancellationToken ct)
        { Notified = order.Id; return Task.CompletedTask; }
    protected override CheckoutResult BuildResult(Order order)
        => new(order.Id, order.Total, null);
}

[Fact]
public async Task Template_runs_hooks_in_order()
{
    var sut = new TestFlow(/* mock */);
    var result = await sut.CheckoutAsync(new CheckoutRequest("c1", "stripe"), default);
    Assert.NotNull(sut.Notified);
}

Cái trên trang: khung cố định. Hai subclass thật. Một subclass test để chạy template. Thêm flow thứ tư là một subclass và hai method. Không có trên trang: switch qua loại flow, khung copy-paste, hay zoo Strategy năm trục.

Đọc tiếp gì trong series?

Một quy tắc thực dụng: default về composition; với Template Method khi khung thật sự là abstraction xứng đáng đặt tên. ASP.NET Core dùng Template Method cho BackgroundService vì "thứ chạy ở background" là concept thật. Code app của bạn đa phần không — và Strategy là hình dạng nhẹ hơn, testable hơn.

Câu hỏi thường gặp

Khi nào ưu tiên Template Method hơn Strategy?
Ưu tiên Template Method khi khung là abstraction xứng đáng đặt tên và biến thể là bước nhỏ bên trong; subclass là biến thể thật của một concept. Ưu tiên Strategy khi biến thể abstraction — khung là phụ và thuật toán là phần thú vị. Strategy compose; Template Method thừa kế.
BackgroundService của ASP.NET Core có phải Template Method không?
Đúng — là ví dụ giáo khoa. BackgroundService định nghĩa lifecycle StartAsync/StopAsync/ExecuteAsync, và bạn chỉ override ExecuteAsync. Base class kiểm soát thuật toán; bạn điền một hook. Cùng hình với ControllerBase, IdentityUserStore, DbContext.OnModelCreating — framework dùng Template Method nhiều.
Tránh lock-in thừa kế với Template Method thế nào?
Làm bề mặt public là một Run() concrete và làm hook protected abstract. Tài liệu hoá hook nào bắt buộc và optional với protected virtual. Ngày subclass cần biến đổi cái base không lường trước, refactor inject Strategy cho bước đó thay vì khắc method protected virtual mới như suy nghĩ sau.
Hook method nên throw nếu không override?
Nếu hook logic là bắt buộc, đánh dấu abstract — compiler ép. Nếu hook optional, cho default virtual no-op. Throw NotImplementedException từ virtual là dấu hợp đồng không rõ; chuyển sang abstract hoặc cung cấp default thật.