大家好,我是十三!欢迎来到十三Tech。

去年重构一个电商下单流程——客户端要依次调风控、库存、营销、支付、消息、积分六个子系统。每个子系统都有自己的初始化、超时、重试、错误码转换。客户端代码写到第三十行还没开始调业务方法,光系统装配就写得人心累。

后来抽了一个 OrderFacade,对外暴露 PlaceOrder(order) 一个方法,内部协调六个子系统。客户端代码从三十多行变成一行。这种"给复杂子系统一个干净入口"的设计就是门面模式

很多人把门面讲成"简化复杂系统",但简化有两种——隐藏合并。适配器是把不兼容接口"翻译"成兼容;门面是把多个调用"合并"成一个。这一篇就聊聊门面的标志特征是"多对一合并",以及它跟适配器、中介者的边界。

门面的现实类比:酒店前台一键预订多个服务

一、门面在解决什么——不是"加一层"

教科书定义:为子系统中的一组接口提供一个一致的界面。这话最模糊——任何 wrapper 都可以套上。真正的门面有一个标志特征——多对一合并

门面解决的核心问题是客户端被多个子系统折磨。注意这里的"多个"——如果一个子系统调用很复杂,那是适配器或代理的事;只有当多个子系统协作完成一个用例时,门面才合适。

判断口诀:子系统多 + 调用顺序复杂 → 门面;单个子系统接口对不上 → 适配器;子系统之间互相调用 → 中介者

门面和适配器还有一个关键差别——门面不隐藏子系统。门面只是提供了一个"更好用的入口",子系统对其他客户端依然可见;适配器则让原接口对调用方完全不可见。这个差别决定了:门面是"加法"(多一个入口),适配器是"替换"(换个入口)。

二、门面怎么写——协调而不实现

门面里没有复杂逻辑,它只做"协调"——按顺序调子系统、处理错误、汇总结果。业务逻辑在子系统,门面只编排

门面 UML 结构:Facade 组合多个子系统

type OrderFacade struct {
    risk *RiskService
    inv  *InventoryService
    prom *PromotionService
    pay  *PaymentService
    msg  *MessageService
}

func (f *OrderFacade) PlaceOrder(o *Order) error {
    if err := f.risk.Check(o.UserID); err != nil { return err }
    if err := f.inv.Lock(o.SKUs()); err != nil { return err }
    d, err := f.prom.Apply(o.ID, o.Rules); if err != nil { return err }
    if err := f.pay.Charge(o.Total() - d.Amount); err != nil { return err }
    return f.msg.SendOrderConfirmed(o.UserID)
}

PlaceOrder——五个子系统按业务流程编排:风控 → 库存 → 营销 → 支付 → 通知。每一步出错都直接返回,由调用方决定重试还是放弃。门面不做决策(除了流程顺序),决策在各子系统

写门面有个工程坑:错误处理要"原文返回"。门面不要把"风控拒绝"翻译成"下单失败"——调用方拿到的错误信息越具体越好定位。最多做一次错误包装(fmt.Errorf("place order: %w", err))保留原始错误链。

门面调用流程:客户端 → Facade → 多个子系统

三、什么时候别用门面

门面不是所有"系统复杂"场景都适用,至少有三个反模式。

反模式一:子系统本来就不复杂,硬上门面。两个子系统、三个调用,客户端直接写更清晰。门面的成本(多一个抽象层、多一份维护)只有子系统多了才摊得平。

反模式二:门面里塞业务逻辑。门面只协调,业务在各子系统。如果门面里写了"判断订单金额是否超限""计算积分",它已经退化成了"领域服务",名字应该改成 OrderService 而不是 OrderFacade——门面是"入口",不是"业务"。

反模式三:门面膨胀到几十个方法。一个门面对应一个用例集合。如果一个门面有 PlaceOrderCancelOrderRefundOrderQueryOrderModifyAddressApplyCouponRemoveCouponAddComment……这说明它想涵盖整个订单生命周期。门面应该按"用例类别"拆分——OrderPlacementFacadeOrderRefundFacadeOrderQueryFacade,每个门面三五个方法。

门面适用决策:子系统数量 + 客户端多样性

Go 标准库里门面最典型的地方是 net/http.Get——一行代码看起来简单,背后链路是:http.GetDefaultClient.Get,后者调 Client.Do,Do 又要做 cookie 处理、redirect 跟随、Transport 选择、连接池借还、TLS 握手、Header 序列化、Body 读取。http.Get 是这一连串子系统的门面——调用方一行搞定,背后是十几个内部模块协作。看懂 net/http 这套门面分层,比记住门面模式定义有用得多。

回头看那个电商下单——值得用门面吗?值得,六个子系统、固定流程、客户端多样化,正是门面的甜区。但如果只是两三个子系统的简单组合,直接调用更直接,门面反而是过度设计。

门面真正教会我的,不是"简化复杂系统",而是区分"用例入口"和"业务实现"——用例入口在门面,业务实现在子系统,两者分离让客户端只关心"做什么",不关心"怎么做"。这个区分比模式本身重要得多。

下一篇讲组合模式,它解决的是"树形结构如何统一处理"的问题,跟门面又是一个新维度。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI