Facade:少暴露一些,是种能力

11 阅读7分钟

你有没有接过这种需求:接入一个"功能很强大"的图表引擎,文档 200 页,核心类 30 多个,初始化要按顺序调 5 步,还得手动管理销毁。你只是想画一条折线图。

这不是引擎的问题——它确实强大。问题在于,它把所有复杂度都裸露给了你。

Facade 模式要做的事情很简单:在你和一坨复杂子系统之间,加一个"只说人话"的入口。

一、经典困境:子系统的裸露成本

原文给了一个很有画面感的场景:你的代码需要和一个复杂库里几十个对象协作——初始化它们、追踪依赖顺序、按正确流程调方法。

结果就是,你的业务逻辑被第三方的初始化细节、依赖顺序、内部状态管理细节污染得面目全非。

电话接线员就是 Facade:顾客只说需求,背后的订单系统、支付网关、配送服务由接线员调度

电话接线员就是 Facade:顾客只说需求,背后的订单系统、支付网关、配送服务由接线员调度

原文的类比很精准:电话订购时的接线员就是 Facade。  你告诉接线员"我要一本书",它帮你对接订单系统、支付网关、物流配送。你不需要知道后面有几个部门在转。

调用方的认知负担不该随子系统的复杂度线性增长。

二、Facade 的核心思路

Facade 的做法是:创建一个新类,把子系统中最常用的几个高频操作封装成简单方法。客户端只跟这个类打交道。

Facade 模式结构图:客户端通过外观类访问子系统,而不是直接操作内部模块

Facade 模式结构图:客户端通过外观类访问子系统,而不是直接操作内部模块

角色职责
Facade封装子系统的高频操作,提供简洁入口
Additional Facade避免单一外观膨胀为上帝对象,可按职责拆分
Complex Subsystem底层真正干活的模块集合,它们不知道外观的存在
Client只通过 Facade 访问子系统

注意一个关键区分:Facade 不是"适配",它是"收束"。  它没有改变子系统的接口形状,也没有控制谁能不能访问——它只是把散乱的调用链收拢成一个更简单、更稳定的入口。

Facade 提供的不是全部能力,而是"刚好够用"的能力。

三、前端的四个典型 Facade 场景

1. 图表引擎入口

ECharts 有几十个组件、上百个配置项。但你 90% 的业务场景,就是折线图、柱状图、饼图,数据格式还差不多。

// ✅ 图表 Facade:只暴露业务需要的高频能力
class ChartFacade {
  private chart: EChartsInstance

  constructor(container: HTMLElement) {
    this.chart = echarts.init(container)
  }

  line(data: { x: string[]; y: number[] }) {
    this.chart.setOption({
      xAxis: { type: 'category', data: data.x },
      yAxis: { type: 'value' },
      series: [{ type: 'line', data: data.y }]
    })
  }

  bar(data: { x: string[]; y: number[] }) {
    this.chart.setOption({
      xAxis: { type: 'category', data: data.x },
      yAxis: { type: 'value' },
      series: [{ type: 'bar', data: data.y }]
    })
  }

  destroy() {
    this.chart.dispose()
  }
}

业务代码只需要 chartFacade.line({ x, y }) 一行。初始化、option 构建、销毁管理,全部藏在 Facade 背后。

业务不需要知道 ECharts 有多少配置项,它只需要"画一条线"。

2. BFF SDK 封装

后端有十几个微服务,前端如果直接对接每个服务的接口,模型不一样、鉴权方式不一样、错误格式不一样。BFF(Backend for Frontend)本身就是一个 Facade——但前端调 BFF 也可以再封一层。

// ✅ BFF SDK Facade
class OrderSDK {
  // 对外只暴露 3 个高频方法
  async createOrder(items: CartItem[]) {
    const user = await this.getUser()
    const address = await this.getDefaultAddress(user.id)
    return api.post('/orders', { items, address, userId: user.id })
  }

  async getOrderDetail(id: string) {
    const [order, logistics] = await Promise.all([
      api.get(`/orders/${id}`),
      api.get(`/logistics/${id}`)
    ])
    return { ...order, logistics }
  }

  async cancelOrder(id: string) {
    return api.post(`/orders/${id}/cancel`)
  }
}

三个方法覆盖了 80% 的订单场景。背后的用户查询、地址拉取、物流聚合、错误处理,调用方完全不用管。

调用方要的是"下单",不是"编排 5 个微服务的调用链"。

3. AI Agent 编排调用

当你的应用需要调 LLM,流程是:构建 prompt → 调模型 → 解析输出 → 校验格式 → 可能重试。这些步骤散在业务代码里就是灾难。

// ✅ AI 调用 Facade
class AIFacade {
  async ask(question: string): Promise<string> {
    const prompt = this.buildPrompt(question)
    const raw = await this.callLLM(prompt)
    const parsed = this.parseResponse(raw)
    if (!this.validate(parsed)) {
      return this.retry(question) // 格式不对就重试
    }
    return parsed
  }

  private buildPrompt(q: string) { /* ... */ }
  private callLLM(prompt: string) { /* ... */ }
  private parseResponse(raw: string) { /* ... */ }
  private validate(result: string) { /* ... */ }
  private retry(q: string) { /* ... */ }
}

业务代码只管 aiFacade.ask("总结这段文本")。Prompt 工程、重试策略、格式校验,都是 Facade 内部的事。

对外一个方法,对内五步流水线。这就是 Facade 的节制。

4. 可观测性客户端

前端监控通常涉及:错误上报、性能打点、用户行为追踪、日志采集。如果每个模块独立初始化、独立调用,业务代码里到处散落监控逻辑。

// ✅ 可观测性 Facade
class ObservabilityFacade {
  private errorReporter = new ErrorReporter()
  private perf = new PerfMonitor()
  private tracker = new UserTracker()

  init(config: AppConfig) {
    this.errorReporter.init(config.dsn)
    this.perf.init(config.appId)
    this.tracker.init(config.trackId)
  }

  reportError(err: Error, context?: Record<string, any>) {
    this.errorReporter.capture(err, context)
  }

  markTiming(label: string) {
    this.perf.mark(label)
  }

  trackEvent(event: string, data?: Record<string, any>) {
    this.tracker.send(event, data)
  }
}

一个 init() 解决三个 SDK 的初始化顺序和配置传递。业务代码只面对一个统一入口。

四、Facade vs Adapter vs Proxy vs Mediator

这个系列已经聊了 Adapter 和 Proxy,加上 Facade 和 Mediator,四个模式经常让人混淆。一张表拉清楚:

维度FacadeAdapterProxyMediator
核心意图收束复杂度转换接口控制访问集中通信
接口关系提供新的简化接口把旧接口转成新接口与目标接口相同提供协调接口
子系统感知子系统不知道 Facade 存在被适配方不知道 Adapter 存在真实对象不知道 Proxy 存在组件只知道 Mediator
隐喻前台总机转接头门卫调度塔
典型场景SDK 封装、模块入口第三方库接入缓存/权限/懒加载组件间通信

一句话总结:Adapter 换语言,Proxy 守大门,Facade 开总机,Mediator 当塔台。

五、深层价值:关于"节制"的设计哲学

从系统设计的视角看,Facade 的核心价值是信息隐藏(Information Hiding) ——David Parnas 1972 年提出的经典原则。他认为,模块应该通过一个"窄接口"对外暴露能力,而把变化细节藏在内部。

这和认知心理学中的工作记忆限制完美对应:人的工作记忆只能同时处理 4±1 个信息块(Cowan, 2001)。Facade 把几十个 API 收束为 3-5 个高频方法,正好落在人脑舒适区内。

好的 API 设计不是把所有能力都暴露出去,而是让调用方在 4 个选项以内就能完成 80% 的工作。

六、什么时候不该用 Facade

Facade 不是银弹。你需要警惕:

• 上帝对象风险:如果你的 Facade 变成了一个 3000 行的巨类,囊括了子系统所有功能——恭喜,你创造了一个新的复杂性源头

• 过度封装:底层子系统本身就很简单(比如只有 2-3 个类),再加一层 Facade 反而增加了间接性

• 能力截断:Facade 天然是"功能受限"的,如果调用方频繁需要绕过 Facade 直接操作底层,说明你的 Facade 抽象粒度不对

判断清单:

✅ 子系统有 5+ 个类且初始化有顺序依赖 → 适合 Facade

✅ 80% 的调用方只用 20% 的功能 → 适合 Facade

❌ 调用方经常需要底层的精细控制 → Facade 粒度可能太粗

❌ 子系统本身已经很简洁 → 不需要多一层

七、一句话带走

如果你只想记一个核心观点:

Facade 的价值不在于它提供了什么能力,而在于它替你藏了多少复杂度。少暴露一点,不是缺陷,是设计者对调用方注意力的尊重。

它和 Adapter 的区别:Adapter 是两套系统"语言不通",需要翻译;Facade 是子系统太复杂,需要一个只说大白话的前台。它和 Proxy 的区别:Proxy 守门,接口不变;Facade 开总机,接口更简单。

克制地暴露能力,才是高级的 API 设计。

参考原文:

• Refactoring.Guru — Facade