Skip to content
Go back

C# 外观模式实战:用一个订单处理系统讲清楚完整实现

大多数外观模式的教程都会给你一个家庭影院的例子:一个遥控器封装了电视、音响、蓝光播放器……理解概念没问题,但真的帮不到你当下的工作——你面对的是支付网关、库存系统、通知服务,还有把它们串起来的协调逻辑被复制粘贴在每一个 Controller 和后台任务里。

这篇文章从电商下单场景出发,完整实现一个 OrderFacade。内容覆盖:为什么需要外观、子系统设计、外观本身的实现、单元测试、依赖注入注册,以及几个常见问题的直接回答。代码可以直接编译运行。

外观模式封面图

没有外观时,Controller 长什么样

客户下单时,应用要做四件事:检查库存、扣款、预占库存、发送确认邮件。每件事属于不同的服务,有自己的接口和错误处理。

当 Controller 直接和每个子系统对话时:

public class OrderController
{
    private readonly PaymentService _payment;
    private readonly InventoryService _inventory;
    private readonly NotificationService _notifications;

    public async Task<string> PlaceOrder(
        string customerId,
        List<OrderItem> items,
        PaymentDetails paymentInfo)
    {
        // 逐项检查库存
        foreach (var item in items)
        {
            bool inStock = await _inventory
                .CheckStockAsync(item.Sku, item.Quantity);
            if (!inStock)
                throw new InvalidOperationException(
                    $"Item {item.Sku} is out of stock.");
        }

        // 计算总金额
        decimal total = items.Sum(
            i => i.UnitPrice * i.Quantity);

        // 扣款
        var charge = await _payment.ChargeAsync(
            paymentInfo.CardToken, total, "usd");

        if (!charge.Success)
            throw new InvalidOperationException("Payment failed.");

        // 预占库存
        foreach (var item in items)
            await _inventory.ReserveAsync(item.Sku, item.Quantity);

        // 发送确认
        await _notifications.SendOrderConfirmationAsync(
            customerId, charge.TransactionId, items);

        return charge.TransactionId;
    }
}

问题不在某一行代码,而是这段协调逻辑会出现在每一个需要下单的地方——Controller、后台 Job、API 端点。任何一处需要变更(加入欺诈检测、税费计算),就要改所有地方。测试时每个 Controller 方法都要 Mock 三个服务。

外观模式的答案是:把这段协调逻辑放进一个专用类,调用方只管一个方法。

三个子系统

下面把三个子系统单独定义好,它们各自只负责自己的职责。

支付服务

public sealed class PaymentResult
{
    public bool Success { get; init; }
    public string TransactionId { get; init; } = "";
    public string ErrorMessage { get; init; } = "";
}

public interface IPaymentService
{
    Task<PaymentResult> ChargeAsync(
        string cardToken, decimal amount, string currency);

    Task<PaymentResult> RefundAsync(
        string transactionId, decimal amount);
}

public sealed class PaymentService : IPaymentService
{
    public Task<PaymentResult> ChargeAsync(
        string cardToken, decimal amount, string currency)
    {
        // 模拟调用支付网关
        return Task.FromResult(new PaymentResult
        {
            Success = true,
            TransactionId = $"txn_{Guid.NewGuid():N}"
        });
    }

    public Task<PaymentResult> RefundAsync(
        string transactionId, decimal amount)
    {
        return Task.FromResult(new PaymentResult
        {
            Success = true,
            TransactionId = transactionId
        });
    }
}

库存服务

public interface IInventoryService
{
    Task<bool> CheckStockAsync(string sku, int quantity);
    Task ReserveAsync(string sku, int quantity);
    Task ReleaseAsync(string sku, int quantity);
}

public sealed class InventoryService : IInventoryService
{
    private readonly Dictionary<string, int> _stock = new()
    {
        ["SKU-001"] = 50,
        ["SKU-002"] = 120,
        ["SKU-003"] = 5
    };

    public Task<bool> CheckStockAsync(string sku, int quantity)
    {
        bool available = _stock.TryGetValue(sku, out int current)
            && current >= quantity;
        return Task.FromResult(available);
    }

    public Task ReserveAsync(string sku, int quantity)
    {
        if (_stock.ContainsKey(sku))
            _stock[sku] -= quantity;
        return Task.CompletedTask;
    }

    public Task ReleaseAsync(string sku, int quantity)
    {
        if (_stock.ContainsKey(sku))
            _stock[sku] += quantity;
        return Task.CompletedTask;
    }
}

通知服务

public interface INotificationService
{
    Task SendOrderConfirmationAsync(
        string customerId, string orderId, decimal totalAmount);

    Task SendPaymentFailureAsync(
        string customerId, string reason);

    Task SendRefundConfirmationAsync(
        string customerId, string orderId, decimal refundAmount);
}

public sealed class NotificationService : INotificationService
{
    public Task SendOrderConfirmationAsync(
        string customerId, string orderId, decimal totalAmount)
    {
        Console.WriteLine(
            $"Order {orderId} confirmed for {customerId}. " +
            $"Total: {totalAmount:C}");
        return Task.CompletedTask;
    }

    public Task SendPaymentFailureAsync(
        string customerId, string reason)
    {
        Console.WriteLine($"Payment failed for {customerId}: {reason}");
        return Task.CompletedTask;
    }

    public Task SendRefundConfirmationAsync(
        string customerId, string orderId, decimal refundAmount)
    {
        Console.WriteLine(
            $"Refund of {refundAmount:C} issued for order {orderId}");
        return Task.CompletedTask;
    }
}

三个服务各自独立、可单独测试。问题从来不是它们本身,而是把它们串起来的那段逻辑。

外观的领域类型

在实现外观之前,先定义调用方真正关心的类型。这些类型属于外观层,与任何子系统的内部类型解耦:

public sealed record OrderItem(
    string Sku, int Quantity, decimal UnitPrice);

public sealed record PaymentDetails(string CardToken);

public sealed record OrderRequest(
    string CustomerId,
    List<OrderItem> Items,
    PaymentDetails Payment);

public sealed record OrderResult
{
    public bool Success { get; init; }
    public string OrderId { get; init; } = "";
    public decimal TotalCharged { get; init; }
    public string ErrorMessage { get; init; } = "";
}

调用方只需要知道这四个类型,不需要了解卡 token 怎么传给支付网关、库存怎么按 SKU 查询,也不需要关心通知渠道的选择。

实现 OrderFacade

外观类协调三个子系统,对外只暴露两个方法:下单和退款。

public interface IOrderFacade
{
    Task<OrderResult> PlaceOrderAsync(OrderRequest request);

    Task<OrderResult> RefundOrderAsync(
        string customerId, string orderId, List<OrderItem> items);
}

public sealed class OrderFacade : IOrderFacade
{
    private readonly IPaymentService _payment;
    private readonly IInventoryService _inventory;
    private readonly INotificationService _notifications;

    public OrderFacade(
        IPaymentService payment,
        IInventoryService inventory,
        INotificationService notifications)
    {
        _payment = payment;
        _inventory = inventory;
        _notifications = notifications;
    }

    public async Task<OrderResult> PlaceOrderAsync(OrderRequest request)
    {
        // 第一步:校验所有商品库存
        foreach (var item in request.Items)
        {
            bool inStock = await _inventory
                .CheckStockAsync(item.Sku, item.Quantity);

            if (!inStock)
            {
                return new OrderResult
                {
                    Success = false,
                    ErrorMessage = $"Item {item.Sku} is out of stock."
                };
            }
        }

        // 第二步:计算总金额
        decimal total = request.Items.Sum(
            i => i.UnitPrice * i.Quantity);

        // 第三步:扣款
        var paymentResult = await _payment.ChargeAsync(
            request.Payment.CardToken, total, "usd");

        if (!paymentResult.Success)
        {
            await _notifications.SendPaymentFailureAsync(
                request.CustomerId, paymentResult.ErrorMessage);

            return new OrderResult
            {
                Success = false,
                ErrorMessage = "Payment processing failed."
            };
        }

        // 第四步:预占库存
        foreach (var item in request.Items)
            await _inventory.ReserveAsync(item.Sku, item.Quantity);

        // 第五步:发送确认通知
        await _notifications.SendOrderConfirmationAsync(
            request.CustomerId, paymentResult.TransactionId, total);

        return new OrderResult
        {
            Success = true,
            OrderId = paymentResult.TransactionId,
            TotalCharged = total
        };
    }

    public async Task<OrderResult> RefundOrderAsync(
        string customerId, string orderId, List<OrderItem> items)
    {
        decimal refundAmount = items.Sum(i => i.UnitPrice * i.Quantity);

        // 第一步:处理退款
        var refundResult = await _payment.RefundAsync(
            orderId, refundAmount);

        if (!refundResult.Success)
        {
            return new OrderResult
            {
                Success = false,
                ErrorMessage = "Refund processing failed."
            };
        }

        // 第二步:释放库存
        foreach (var item in items)
            await _inventory.ReleaseAsync(item.Sku, item.Quantity);

        // 第三步:通知客户
        await _notifications.SendRefundConfirmationAsync(
            customerId, orderId, refundAmount);

        return new OrderResult
        {
            Success = true,
            OrderId = orderId,
            TotalCharged = -refundAmount
        };
    }
}

注意 PlaceOrderAsync 里的顺序:库存校验在扣款之前。如果某个 SKU 没货,直接返回失败,不会产生任何扣款。如果扣款失败,外观负责发送付款失败通知,调用方完全不用关心这个分支。

这就是外观模式的核心价值——调用方只看到成功或失败的结果,看不到多步骤之间的协调细节。

使用外观后的 Controller

有了外观之后,Controller 变成什么样子:

public class OrderController
{
    private readonly IOrderFacade _orderFacade;

    public OrderController(IOrderFacade orderFacade)
    {
        _orderFacade = orderFacade;
    }

    public async Task<OrderResult> PlaceOrder(
        string customerId,
        List<OrderItem> items,
        string cardToken)
    {
        var request = new OrderRequest(
            CustomerId: customerId,
            Items: items,
            Payment: new PaymentDetails(cardToken));

        return await _orderFacade.PlaceOrderAsync(request);
    }
}

没有库存循环,没有支付错误处理,没有通知调用。Controller 只做一件事:构建请求,委托给外观。

对比文章开头的版本,差距一目了然。

单元测试

外观的单元测试验证的是编排逻辑——每个子系统是否在正确的时机以正确的参数被调用。

public sealed class OrderFacadeTests
{
    private readonly Mock<IPaymentService> _mockPayment;
    private readonly Mock<IInventoryService> _mockInventory;
    private readonly Mock<INotificationService> _mockNotifications;
    private readonly OrderFacade _facade;

    public OrderFacadeTests()
    {
        _mockPayment = new Mock<IPaymentService>();
        _mockInventory = new Mock<IInventoryService>();
        _mockNotifications = new Mock<INotificationService>();

        _facade = new OrderFacade(
            _mockPayment.Object,
            _mockInventory.Object,
            _mockNotifications.Object);
    }

    [Fact]
    public async Task PlaceOrderAsync_AllItemsInStock_ReturnsSuccess()
    {
        var items = new List<OrderItem>
        {
            new("SKU-001", 2, 25.00m),
            new("SKU-002", 1, 15.00m)
        };

        _mockInventory
            .Setup(i => i.CheckStockAsync(
                It.IsAny<string>(), It.IsAny<int>()))
            .ReturnsAsync(true);

        _mockPayment
            .Setup(p => p.ChargeAsync(
                It.IsAny<string>(), It.IsAny<decimal>(), It.IsAny<string>()))
            .ReturnsAsync(new PaymentResult
            {
                Success = true,
                TransactionId = "txn_abc123"
            });

        var request = new OrderRequest(
            "cust_001", items, new PaymentDetails("tok_visa"));

        var result = await _facade.PlaceOrderAsync(request);

        Assert.True(result.Success);
        Assert.Equal("txn_abc123", result.OrderId);
        Assert.Equal(65.00m, result.TotalCharged);

        _mockNotifications.Verify(
            n => n.SendOrderConfirmationAsync(
                "cust_001", "txn_abc123", 65.00m),
            Times.Once);
    }

    [Fact]
    public async Task PlaceOrderAsync_ItemOutOfStock_ReturnsFailure()
    {
        var items = new List<OrderItem>
        {
            new("SKU-999", 1, 50.00m)
        };

        _mockInventory
            .Setup(i => i.CheckStockAsync("SKU-999", 1))
            .ReturnsAsync(false);

        var request = new OrderRequest(
            "cust_002", items, new PaymentDetails("tok_visa"));

        var result = await _facade.PlaceOrderAsync(request);

        Assert.False(result.Success);
        Assert.Contains("out of stock", result.ErrorMessage);

        // 库存不足时,不应该尝试扣款
        _mockPayment.Verify(
            p => p.ChargeAsync(
                It.IsAny<string>(), It.IsAny<decimal>(), It.IsAny<string>()),
            Times.Never);
    }

    [Fact]
    public async Task PlaceOrderAsync_PaymentFails_NotifiesCustomer()
    {
        var items = new List<OrderItem>
        {
            new("SKU-001", 1, 30.00m)
        };

        _mockInventory
            .Setup(i => i.CheckStockAsync(
                It.IsAny<string>(), It.IsAny<int>()))
            .ReturnsAsync(true);

        _mockPayment
            .Setup(p => p.ChargeAsync(
                It.IsAny<string>(), It.IsAny<decimal>(), It.IsAny<string>()))
            .ReturnsAsync(new PaymentResult
            {
                Success = false,
                ErrorMessage = "Card declined"
            });

        var request = new OrderRequest(
            "cust_003", items, new PaymentDetails("tok_declined"));

        var result = await _facade.PlaceOrderAsync(request);

        Assert.False(result.Success);

        // 扣款失败要通知客户
        _mockNotifications.Verify(
            n => n.SendPaymentFailureAsync("cust_003", "Card declined"),
            Times.Once);

        // 扣款失败后不应该预占库存
        _mockInventory.Verify(
            i => i.ReserveAsync(It.IsAny<string>(), It.IsAny<int>()),
            Times.Never);
    }

    [Fact]
    public async Task RefundOrderAsync_ValidOrder_ReleasesInventory()
    {
        var items = new List<OrderItem>
        {
            new("SKU-001", 2, 25.00m)
        };

        _mockPayment
            .Setup(p => p.RefundAsync("txn_abc123", 50.00m))
            .ReturnsAsync(new PaymentResult
            {
                Success = true,
                TransactionId = "txn_abc123"
            });

        var result = await _facade.RefundOrderAsync(
            "cust_001", "txn_abc123", items);

        Assert.True(result.Success);

        _mockInventory.Verify(
            i => i.ReleaseAsync("SKU-001", 2),
            Times.Once);

        _mockNotifications.Verify(
            n => n.SendRefundConfirmationAsync(
                "cust_001", "txn_abc123", 50.00m),
            Times.Once);
    }
}

注意几个关键断言:库存不足时验证付款从未被调用;付款失败时验证库存预占从未发生。这些断言验证的是外观的顺序逻辑——如果协调代码散落在各个 Controller 里,这类 bug 很难被测试覆盖到。

注册到依赖注入容器

因为外观依赖的是接口,替换实现只需要改一行注册:

using Microsoft.Extensions.DependencyInjection;

public static class OrderServiceRegistration
{
    public static IServiceCollection AddOrderProcessing(
        this IServiceCollection services)
    {
        services.AddSingleton<IPaymentService, PaymentService>();
        services.AddSingleton<IInventoryService, InventoryService>();
        services.AddSingleton<INotificationService, NotificationService>();
        services.AddTransient<IOrderFacade, OrderFacade>();

        return services;
    }
}

Program.cs 里一行搞定:

builder.Services.AddOrderProcessing();

以后把 PaymentService 替换成 Stripe 集成,只改注册那行。外观不变,Controller 不变。

适合用外观的场景

电商下单是教科书级别的场景,但相同的结构适用于:

判断是否需要外观,看一个信号:同样的多服务协调逻辑是否在多个地方重复出现。一旦有复制粘贴,就该提取成一个专用类。

外观不违反单一职责原则——外观的职责就是编排,不实现支付、不管库存、不发通知,只协调。这是一个职责、一个变化轴。

外观 vs 适配器 vs 策略

容易混淆的三个模式:

实践中这些模式经常组合使用。外观内部可以用适配器来规范化第三方 API,也可以用策略模式在多个支付提供商之间选择。

常见问题

外观应该暴露子系统的所有方法吗?
不。只暴露调用方真正需要的高层操作。OrderFacade 只暴露 PlaceOrderAsyncRefundOrderAsync,不暴露检查库存、单独扣款或发邮件的方法。如果调用方需要直接操作子系统,让它依赖子系统接口即可。

外观可以调用另一个外观吗?
可以,而且挺常见。一个大型应用可能有 OrderFacade 在下单时委托给 ShippingFacade。注意不要产生循环依赖,依赖关系要保持单向流动。

多个子系统的错误怎么处理?
优先设计成让错误不需要补偿。本文例子里库存校验在扣款之前,所以不存在「扣款成功但库存不够再退款」的场景。对于更复杂的流程,可以考虑命令模式(Command Pattern)来实现补偿事务。

对性能有影响吗?
外观只增加一次方法调用,开销可以忽略不计。没有多余的网络调用或 I/O,只是把协调逻辑搬进了一个专用类。

参考


Tags


Previous

.NET 10 中结合 API 版本控制与 OpenAPI 文档的实践指南

Next

什么是不变量,为什么领域模型是执行它们的最佳场所