Cấu trúc Cơ bản 7 phút đọc

Facade Pattern trong C#: Một Service Gói Nhiều Subsystem

Facade pattern trong C# / .NET 10: gập sáu inject vào một application service fan ra, không để class bên trong rò ngược ra caller.

Mục lục
  1. Facade pattern giải quyết bài toán gì trong C#?
  2. Cấu trúc Facade trong .NET 10 trông thế nào?
  3. Facade nên có interface riêng hay chỉ là class?
  4. Khi nào Facade pattern bắn trật?
  5. So sánh Facade với Adapter và Mediator thế nào?
  6. Một ví dụ thật trong .NET 10 trông thế nào?
  7. Đọc tiếp gì trong series?

CheckoutController bắt đầu với một việc và một dependency. Sáu tháng sau, constructor nhận:

public CheckoutController(
    ICartRepository       carts,
    IInventoryService     inventory,
    ITaxCalculator        tax,
    IPaymentProcessor     payment,
    IShippingService      shipping,
    INotificationService  notifications,
    IOrderRepository      orders)
{ /* ... */ }

Bảy inject. Action method Checkout() duy nhất dài sáu mươi dòng. Nó load cart, validate inventory, tính tax, charge thẻ, book ship, persist order, và gửi email xác nhận. Không bước nào khó. Cùng nhau chúng là mê cung, và mê cung giờ là vấn đề của controller.

Facade pattern là câu trả lời. Kéo orchestration vào một application service — gọi là ICheckoutService — và để controller inject một thứ. Bên trong service, bảy collaborator làm việc của họ. Bên ngoài, bảy collaborator thôi tồn tại với caller. Đây là structural pattern lặng lẽ hữu ích nhất trong .NET: hầu hết codebase ngăn nắp dùng nó mà không gọi tên.

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

Pattern xứng chỗ khi một subsystem class hợp tác đã mọc vượt mức caller hợp lý điều phối được. Ba hình dạng cụ thể:

  1. Orchestration use case. Checkout, signup, refund — mỗi cái là chuỗi call vào subsystem khác. Facade đặt tên cho mỗi use case.
  2. Entry point library. Library expose mười class cho user nâng cao nhưng 90% caller muốn một method. Facade là cánh cửa thân thiện che các phòng phía sau.
  3. Trú ẩn legacy. Hệ thống legacy ba mươi module móc nối. Code mới bọc bốn cái thật sự dùng vào Facade để phần còn lại không bao giờ rỉ vào.

Cái không phải bài toán Facade: "Tôi muốn một type lạ vừa interface local" — đó là Adapter. "Tôi muốn thêm hành vi quanh call có sẵn" — đó là Decorator. Facade đặc biệt nói về giảm bề mặt caller phải biết.

Cấu trúc Facade trong .NET 10 trông thế nào?

Một class bình thường implement interface và nhận inner collaborator qua constructor. Mỗi method public là một use case và điều phối hai hoặc nhiều inner call:

public sealed record CheckoutRequest(string CartId, string PaymentMethod);
public sealed record CheckoutResult(string OrderId, decimal TotalCharged, string ConfirmationEmail);

public interface ICheckoutService
{
    Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct);
}

public sealed class CheckoutService : ICheckoutService
{
    private readonly ICartRepository      _carts;
    private readonly IInventoryService    _inventory;
    private readonly ITaxCalculator       _tax;
    private readonly IPaymentProcessor    _payment;
    private readonly IShippingService     _shipping;
    private readonly INotificationService _notifications;
    private readonly IOrderRepository     _orders;
    private readonly ILogger<CheckoutService> _log;

    public CheckoutService(/* tất cả phía trên */) { /* ... */ }

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        var cart   = await _carts.GetAsync(req.CartId, ct);
        await _inventory.ReserveAsync(cart.Items, ct);
        var totals = _tax.Compute(cart);
        var charge = await _payment.PayAsync(totals.GrandTotal, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error);

        var order = await _orders.CreateAsync(cart, totals, charge.TransactionId!, ct);
        await _shipping.BookAsync(order, ct);
        await _notifications.SendConfirmationAsync(order, ct);

        return new CheckoutResult(order.Id, totals.GrandTotal, order.Customer.Email);
    }
}

Controller giờ đọc:

[ApiController, Route("checkout")]
public sealed class CheckoutController : ControllerBase
{
    private readonly ICheckoutService _checkout;
    public CheckoutController(ICheckoutService checkout) => _checkout = checkout;

    [HttpPost]
    public async Task<ActionResult<CheckoutResult>> Post(CheckoutRequest req, CancellationToken ct)
        => Ok(await _checkout.CheckoutAsync(req, ct));
}

Bức tranh cấu trúc:

flowchart LR
    Caller[CheckoutController]
    Caller --> Facade[ICheckoutService]
    Facade --> Cart[ICartRepository]
    Facade --> Inv[IInventoryService]
    Facade --> Tax[ITaxCalculator]
    Facade --> Pay[IPaymentProcessor]
    Facade --> Ship[IShippingService]
    Facade --> Email[INotificationService]
    Facade --> Order[IOrderRepository]

Một mũi tên từ caller đến facade; sáu mũi tên từ facade tới inner collaborator. Đơn giản hoá sống ở biên caller thấy.

Facade nên có interface riêng hay chỉ là class?

Facade chỉ là class với collaborator; có interface riêng là quyết định khác:

Trong .NET 10 hiện đại, hầu hết application service có interface vì test dùng. Mặc định có; bỏ khi chứng minh nó chưa từng hữu ích.

Khi nào Facade pattern bắn trật?

Ba bẫy thực tế:

So sánh Facade với Adapter và Mediator thế nào?

Pattern Số inner object Đổi interface? Thêm hành vi?
Facade Nhiều Có (nhiều → một) Không (chỉ orchestration)
Adapter Một Có (mismatch → match) Không
Mediator Nhiều (peer, không phải subordinate) Có (peer → bus) Không

Tách rõ nhất: Adapter một-một có đổi hình; Facade nhiều-một có orchestration; Mediator nhiều-nhiều qua hub. Facade sở hữu các quan hệ và quyết định thứ tự call; Mediator chỉ broker message giữa các bên ngang hàng.

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

Hình dạng đầy đủ: cùng checkout, với điều phối transactional và biên DTO. Facade là type public duy nhất; các class bên trong đăng ký riêng và giữ internal trong assembly của chúng.

// Hợp đồng public (cái caller thấy) -------------------------------
public sealed record CheckoutRequest(string CartId, string PaymentMethod);
public sealed record CheckoutResult(string OrderId, decimal TotalCharged, string ConfirmationEmail);

public interface ICheckoutService
{
    Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct);
}

// Implementation (internal) ---------------------------------------
internal sealed class CheckoutService : ICheckoutService
{
    private readonly ICartRepository      _carts;
    private readonly IInventoryService    _inventory;
    private readonly ITaxCalculator       _tax;
    private readonly IPaymentProcessor    _payment;
    private readonly IShippingService     _shipping;
    private readonly INotificationService _notifications;
    private readonly IOrderRepository     _orders;
    private readonly IUnitOfWork          _uow;
    private readonly ILogger<CheckoutService> _log;

    public CheckoutService(
        ICartRepository carts, IInventoryService inventory, ITaxCalculator tax,
        IPaymentProcessor payment, IShippingService shipping,
        INotificationService notifications, IOrderRepository orders,
        IUnitOfWork uow, ILogger<CheckoutService> log)
    {
        _carts = carts; _inventory = inventory; _tax = tax;
        _payment = payment; _shipping = shipping; _notifications = notifications;
        _orders = orders; _uow = uow; _log = log;
    }

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        await using var tx = await _uow.BeginAsync(ct);

        var cart = await _carts.GetAsync(req.CartId, ct)
            ?? throw new NotFoundException($"Cart {req.CartId} not found");

        await _inventory.ReserveAsync(cart.Items, ct);

        var totals = _tax.Compute(cart);

        var charge = await _payment.PayAsync(totals.GrandTotal, cart.CustomerId, req.CartId, ct);
        if (charge.Status != "succeeded")
            throw new CheckoutFailedException(charge.Error ?? "Unknown");

        var order = await _orders.CreateAsync(cart, totals, charge.TransactionId!, ct);
        await _shipping.BookAsync(order, ct);

        await tx.CommitAsync(ct);

        // Notification sau commit - email gửi fail không được rollback order đã thành công.
        await _notifications.SendConfirmationAsync(order, ct);

        _log.LogInformation("Order {OrderId} created for cart {CartId}", order.Id, req.CartId);
        return new CheckoutResult(order.Id, totals.GrandTotal, order.Customer.Email);
    }
}

// Composition root ------------------------------------------------
builder.Services.AddScoped<ICheckoutService, CheckoutService>();
// inner collaborator đăng ký riêng dạng scoped/singleton service

// Caller (controller phía trên) -----------------------------------
[ApiController, Route("checkout")]
public sealed class CheckoutController : ControllerBase
{
    private readonly ICheckoutService _checkout;
    public CheckoutController(ICheckoutService checkout) => _checkout = checkout;

    [HttpPost]
    public async Task<ActionResult<CheckoutResult>> Post(CheckoutRequest req, CancellationToken ct)
        => Ok(await _checkout.CheckoutAsync(req, ct));
}

Quyết định đáng nhấn mạnh: notification xảy ra sau commit. Facade là chỗ duy nhất biết quyết định đó tồn tại, vì Facade là chỗ duy nhất điều phối cả hai. Giấu cái biết đó trong Facade là toàn bộ điểm.

Đọc tiếp gì trong series?

Một ghi chú thực dụng: Facade là pattern ít được gọi tên nhất trong codebase thật. Hầu hết application service Facade; đa số team chỉ gọi nó "service X". Ổn — pattern xứng đáng dù bạn gọi nó bằng gì. Lợi ích biết tên là nhận ra failure mode (god class, return rò, passthrough một-một) sớm hơn.

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

Facade khác Adapter ở điểm gì?
Adapter một-một — làm class lạ vừa interface local. Facade nhiều-một — gom nhiều class có sẵn sau interface mới đơn giản hơn mà caller muốn từ đầu. Adapter cho hình dạng không khớp; Facade cho quá nhiều hình dạng caller không muốn điều phối.
Application service có cùng là Facade không?
Thực dụng là có. Application service trong DDD đứng giữa controller và domain, điều phối vài aggregate, expose một method cho mỗi use case. Đó đúng là Facade pattern. Khung tư duy khác — application service nhấn mạnh transaction boundary và use case — nhưng hình dạng cấu trúc giống hệt.
Facade có thể rò bên trong qua return type không?
Có, và đó là cách phổ biến nhất pattern fail. Nếu CheckoutFacade.Checkout() trả cùng Order aggregate mà repository bên trong trao, mọi caller giờ phụ thuộc type bên trong. Convert sang DTO nhỏ ở biên Facade để caller chỉ phụ thuộc data thực sự cần.
Khi nào Facade làm code khó theo dõi hơn?
Khi Facade là wrapper một-một không thêm gì — cùng tham số vào, cùng kết quả ra, chỉ thêm file. Hoặc khi Facade có nhiều method không liên quan vì thành service locator khoác áo service. Facade xứng đáng khi mỗi method điều phối hai hoặc nhiều inner call; dưới ngưỡng đó, inject class bên trong trực tiếp.