cv.cm

编码 · 设计模式

桥接模式详解与示例

阅读约 5 分钟

简短回答

桥接是一种结构型设计模式,它把一个庞大的类层级拆成抽象和实现两个独立的层级,并通过组合把它们连接起来。抽象(例如各种报表)持有一个实现对象(例如各种输出渲染器)的引用,并把底层工作委托给它。这样两边都可以独立扩展,于是 3 种报表和 3 种渲染器只需 6 个类,而不是 9 个子类组合。

意图

将抽象与它的实现解耦,使两者可以独立变化。桥接是《设计模式》(四人组,GoF)一书中七种结构型模式之一,书中也称其为 Handle/Body(句柄/本体)。

它要解决的问题

设想一个应用,要通过两种渠道(邮件和短信)发送两类消息(警报和提醒)。如果用继承建模,就会得到 EmailAlert、SmsAlert、EmailReminder 和 SmsReminder。再加一个推送渠道和一种摘要消息,就需要九个类。任何一边每多一种类型,总数都会成倍增加。

真正的问题在于,这个类层级把两个独立的问题混在了一起:消息是什么,以及它怎么送达。桥接把它们分成两个层级,再用一个引用连接起来,于是总数是做加法增长,而不是乘法。

设计方式2 种消息 × 2 种渠道3 种消息 × 3 种渠道4 种消息 × 4 种渠道
每种组合一个子类4 个类9 个类16 个类
桥接4 个类6 个类8 个类

结构与参与者

参与者职责示例中的对应
抽象(Abstraction)客户端使用的高层接口;持有一个实现者的引用。Message
扩展抽象(RefinedAbstraction)在抽象的基础上扩展出不同的行为。Alert, Reminder
实现者(Implementor)底层操作的接口。Sender
具体实现者(ConcreteImplementor)为某个平台或技术实现底层操作。EmailSender, SmsSender

TypeScript 实现

// Implementor: how a message is delivered.
interface Sender {
  deliver(to: string, subject: string, body: string): string;
}

class EmailSender implements Sender {
  deliver(to: string, subject: string, body: string) { return `email ${to} | ${subject} | ${body}`; }
}

class SmsSender implements Sender {
  deliver(to: string, subject: string, body: string) { return `sms ${to} | ${subject}: ${body}`.slice(0, 40); }
}

// Abstraction: what the message is. It holds the bridge to a Sender.
abstract class Message {
  constructor(protected sender: Sender) {}
  abstract send(to: string): string;
}

class Alert extends Message {
  constructor(sender: Sender, private problem: string) { super(sender); }
  send(to: string): string {
    return this.sender.deliver(to, "ALERT", this.problem.toUpperCase());
  }
}

class Reminder extends Message {
  constructor(sender: Sender, private task: string, private due: string) { super(sender); }
  send(to: string): string {
    return this.sender.deliver(to, "Reminder", `${this.task} by ${this.due}`);
  }
}

console.log(new Alert(new SmsSender(), "disk full").send("+100")); // sms +100 | ALERT: DISK FULL
console.log(new Reminder(new EmailSender(), "Pay invoice", "Friday").send("[email protected]"));
// email [email protected] | Reminder | Pay invoice by Friday

新增一个 PushSender 只需要一个类,它立刻就能配合警报和提醒使用。新增一个 Digest 消息也只需要一个类,它立刻就能配合每一种发送器使用。因为发送器只是一个构造函数参数,所以也可以在运行时选择,例如根据用户偏好。

何时使用,何时不用

以下情况适合使用桥接:

  • 一个类在两个或更多独立的维度上变化,而用继承会为每种组合都生成一个类。
  • 你想在运行时切换实现,或者让多个抽象共享同一个实现对象。
  • 你编写的代码必须在多个平台(操作系统、渲染后端、数据库引擎)上运行,并希望把平台细节放在单独的一个层级里。

以下情况应避免使用:

  • 只有一个变化维度。一个带有多个实现的简单接口就足够了。
  • 你只有一个实现,却“为将来着想”而引入它。多出来的一层让代码更难读,眼下又没有任何好处。

实际应用

  • Java 的 JDBC 是一个著名的例子:应用代码基于 JDBC API 编写,而每个数据库厂商提供实现底层操作的驱动。两边各自独立演进。
  • 图形和 UI 工具包在平台相关的绘制后端之上,保留一套与平台无关的控件或图形 API。GoF 一书自己的例子是一个 Window 抽象,每种窗口系统对应一个单独的 WindowImp 实现。
  • 日志库通常把代码调用的 logger API,与写入文件、控制台或网络服务的处理器(handler 或 appender)分离开。

相关模式

模式与桥接的关系
适配器结构相似,但它是在类设计完成之后,让互不相关的现有类能够协同工作。桥接则是在编写类之前就规划好的。
策略同样委托给一个可互换的对象。策略关注的是替换算法;桥接关注的是把整个抽象层级与实现层级分开。
抽象工厂可以为桥接创建并配置合适的实现对象,例如正确的平台后端。
组合桥接同样基于“组合优于继承”,但组合模式讲的是对象构成的树,而不是两个独立的层级。

常见问题

桥接模式中的“实现”是什么意思?

它不是指类内部的代码,而是指一个独立的对象层级,负责完成底层的、与平台相关的工作,抽象会把任务委托给它。

桥接与策略有什么区别?

它们的结构几乎相同。策略侧重于替换对象内部的某一个算法,而桥接是把两个完整的层级分开,使每一边都能独立扩展。

桥接与适配器有什么区别?

桥接是预先设计的,让两边都可以变化。适配器是事后添加的,让一个现有的、不兼容的类适配某个接口。

桥接和依赖注入是一回事吗?

不是。桥接是组织两个层级的方式。依赖注入是提供实现对象的方式,也是给抽象配上其实现者的自然做法。

全部编码教程