cv.cm

编码 · 设计模式

外观模式详解与示例

阅读约 5 分钟

简短回答

外观是一种结构型设计模式,它为复杂子系统中的一组接口提供一个简单的、更高层的接口。客户端代码不必按正确顺序去调用库存、支付、物流和邮件服务,只需调用 shop.placeOrder() 这样的一个方法,由外观来协调各个子系统类。子系统依然存在,需要时也可以直接使用;外观只是让常见情形变得简单。

意图

为子系统中的一组接口提供一个统一的接口。外观定义了一个更高层的接口,使子系统更易于使用。它是《设计模式》(四人组,GoF)一书中七种结构型模式之一。

它要解决的问题

设想一家网店。下一个订单需要多个服务:检查并预留库存、给银行卡扣款、支付失败时释放库存、创建发货单,以及发送确认邮件。每个服务都有自己的 API 和规则。

如果每个调用方,比如网页结账、移动端 API 和管理工具,都自己执行这些步骤,同样的流程就会被复制到许多地方。每一份副本都可能把顺序弄错或者忘记回滚,而且每个调用方都依赖五个服务。一旦某个服务变化,所有调用方都得修改。外观把这套流程放在一处,给调用方提供一个方法。

结构与参与者

参与者职责示例中的对应
外观(Facade)知道由哪些子系统类处理请求,并按正确顺序委托给它们。OrderFacade
子系统类完成真正的工作。它们并不知道外观的存在。Inventory, Payments, Shipping, Mailer
客户端(Client)用外观完成常见任务;遇到特殊情况仍可直接使用子系统类。结账代码

TypeScript 实现

// Subsystem classes, each with its own API.
class Inventory {
  private stock = new Map([["book", 2]]);
  reserve(sku: string): boolean {
    const n = this.stock.get(sku) ?? 0;
    if (n === 0) return false;
    this.stock.set(sku, n - 1);
    return true;
  }
  release(sku: string): void { this.stock.set(sku, (this.stock.get(sku) ?? 0) + 1); }
}
class Payments {
  charge(card: string, cents: number): boolean { return card.startsWith("4") && cents > 0; }
}
class Shipping {
  createShipment(sku: string, address: string): string { return `SHIP-${sku}-${address.length}`; }
}
class Mailer {
  send(to: string, text: string): void { console.log(`mail to ${to}: ${text}`); }
}

随后外观在一个方法背后,按正确的顺序执行整个工作流:

// Facade: one method that runs the whole workflow in the right order.
class OrderFacade {
  constructor(private inventory = new Inventory(), private payments = new Payments(),
              private shipping = new Shipping(), private mailer = new Mailer()) {}

  placeOrder(sku: string, card: string, cents: number, email: string, address: string): string {
    if (!this.inventory.reserve(sku)) return "out of stock";
    if (!this.payments.charge(card, cents)) {
      this.inventory.release(sku); // roll back the reservation
      return "payment declined";
    }
    const shipmentId = this.shipping.createShipment(sku, address);
    this.mailer.send(email, `Order confirmed, shipment ${shipmentId}`);
    return "ok";
  }
}

const shop = new OrderFacade();
console.log(shop.placeOrder("book", "4111", 1999, "[email protected]", "1 Main St"));
// mail to [email protected]: Order confirmed, shipment SHIP-book-9
// ok
console.log(shop.placeOrder("book", "5500", 1999, "[email protected]", "2 Side St")); // payment declined

现在调用方只依赖一个类的一个方法。回滚规则只存在一份。子系统类依然可用,所以只需要释放库存的管理工具,可以直接调用 Inventory。

何时使用,何时不用

以下情况适合使用外观:

  • 子系统很复杂,而大多数客户端只需要它能力中简单、常用的一个子集。
  • 你想减少客户端代码与众多子系统类之间的依赖。
  • 你想把系统分层,并用外观作为每一层的入口。
  • 你包装了一个庞大的第三方库,希望其余代码只使用其中少数几个需要的操作。

以下情况应避免使用或加以限制:

  • 子系统本身已经很简单。一对一转发每个调用的外观,只会增加一层而什么也没隐藏。
  • 外观开始收集系统各处互不相关的方法和业务规则。这就是“上帝对象”问题:一个类知道得太多、做得太多。应把它拆成几个职责单一的外观。

实际应用

  • Node.js:fs.readFile() 在一次调用中完成打开文件、读到末尾和关闭文件。更底层的 fs.open()、fs.read() 和 fs.close() 仍然可用。
  • jQuery 的流行很大程度上是因为它在 DOM 和当年各浏览器的差异之前,提供了一套简短而一致的 API。
  • GoF 一书中的例子是编译器:一个 Compiler 类把扫描器、语法分析器、代码生成器等子系统类,对只想编译代码的程序隐藏起来。
  • 后端应用中的服务层和 API 网关,扮演着众多内部服务的外观。

相关模式

模式与外观的区别
适配器让一个现有接口匹配另一个现有接口。外观则为许多类定义一个新的、更简单的接口。
中介者同样集中协调,但子系统对象认识中介者,并通过它通信。使用外观时,子系统类并不知道外观的存在。
抽象工厂可以与外观配合,以与平台无关的方式创建子系统对象。
单例外观通常是单个实例,因为子系统通常只需要一个入口。
代理与某一个对象拥有相同的接口,并控制对它的访问。外观则是在许多对象之上提供一个不同的、更简单的接口。

常见问题

外观与适配器有什么区别?

适配器把一个现有接口转换成客户端已经期望的另一个接口。外观则是创造一个新的、更简单的接口,让整个子系统更易于使用。

外观会完全隐藏子系统吗?

不会。外观让常见任务变得简单,但需要更多控制的客户端仍然可以直接使用子系统类。

一个系统可以有多个外观吗?

可以。大型系统通常每个子系统或每个用例都有一个外观,这样每个外观都保持小而专注。

服务层算是外观吗?

常常算。一个在一次调用背后协调仓储、外部 API 和通知的服务方法,对调用它的代码来说就扮演着外观的角色。

全部编码教程