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
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ể:
- 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.
- 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.
- 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:
- Interface đáng khi caller mock hoặc swap trong test
(controller gọi
ICheckoutServicelà case kinh điển), khi nhiều hơn một implementation hợp lý (test mode bỏ qua charge thật), hoặc khi Facade thuộc public API. - Class không interface ổn cho orchestration thuần internal mà inner collaborator đã được test và Facade chỉ là keo. Hai file thêm cho interface không có implementation thứ hai là overhead, không phải kiến trú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ế:
- Facade rò bên trong.
CheckoutAsynctrảOrderaggregate thô. Mọi consumer Facade giờ phụ thuộc type repository bên trong. Convert sang DTO (CheckoutResultphía trên) ở biên; không bao giờ để inner aggregate vượt qua. - Facade thành god class. Mỗi use case mới được thêm thành method
khác trên
CheckoutServiceđến khi có ba mươi method trên cùng interface. Tách: một Facade cho mỗi gia đình use-case (ICheckoutService,IRefundService,ICartService). - Facade là passthrough một-một.
CheckoutService.PayAsyncchỉ gọi_payment.PayAsyncvới cùng tham số. Class không kiếm được đơn giản hoá nào — xoá nó và injectIPaymentProcessorthẳng. Facade kiếm tồn tại bằng gom, không phải forward.
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?
- Bài trước: Decorator — cùng hình dạng nhưng bọc một-một thêm hành vi.
- Bài kế: Flyweight — share data immutable qua nhiều instance.
- Tham chiếu chéo: Mediator — khi inner object là peer nói chuyện qua bus, không phải subordinate Facade điều phối.
- Tham chiếu chéo: Singleton — Facade
thường
Scoped(per request) nhưng đôi khiSingletonkhi stateless và gọi nhiều. - Cây quyết định: Cách chọn design pattern phù hợp.
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 là 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ì?
Application service có cùng là Facade không?
Facade có thể rò bên trong qua return type không?
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.