10 桥接(结构型)

一个东西有两个独立变化的维度,用继承会让类的数量乘起来爆炸。桥接把两个维度拆成两条独立的类层次,用组合搭一座桥,让 M×N 变回 M+N。附 database/sql 就是一座桥的实锤。

作者 David更新于

10 桥接 · Bridge(结构型)

桥接是结构型里公认最难理解的一个,倒不是因为它复杂,而是因为 GoF 给它起的名字(“抽象部分”“实现部分”)极其抽象,劝退了一批人。别怕,我们用大白话把它拆开。

一、这是什么:遥控器 × 设备,两个能各自进化的维度

你有遥控器,也有被控的设备。现在注意,这里有两个会独立变化的维度:

  • 遥控器这一维会变:有基础遥控器,有带静音的高级遥控器,以后可能有带语音的……
  • 设备这一维也会变:能控电视,能控收音机,以后可能控空调、控音箱……

关键洞察:这两个维度是正交的、各自独立进化的。 “高级遥控器”应该能控任何设备,“电视”应该能被任何遥控器控。它们不该被绑死。

桥接模式就是:把这两个维度拆成两套独立的类层次,让“遥控器”持有一个“设备”(这就是那座“桥”),两边各自随便扩展,谁也不拖累谁。

二、解决什么痛:继承会让类的数量“乘起来”

假设你头铁,用继承来做。你会得到:

BasicRemoteForTV、BasicRemoteForRadio、
AdvancedRemoteForTV、AdvancedRemoteForRadio......

2 种遥控器 × 2 种设备 = 4 个类。现在加一种设备(空调),变成 2×3=6;再加一种遥控器,3×3=9……类的数量是两个维度相乘的。这就是继承的“笛卡尔积爆炸”:每个维度增长,总数是乘法级膨胀。

桥接把它从乘法变回加法:

继承(维度耦合)桥接(维度解耦)
2 遥控 × 2 设备2×2 = 4 个类2 + 2 = 4 个类
3 遥控 × 4 设备3×4 = 12 个类3 + 4 = 7 个类
M 遥控 × N 设备M×NM + N

维度一多,M+N 对 M×N 是碾压性的优势。

桥接隔离的“会变的东西”,是“两个正交维度各自的变化”。 它认定这两维互不相干,于是干脆拆成两条平行的继承线,中间用一个“持有”关系(组合)搭桥。任一维度加东西,都是给那条线加一个类(加法),而不是给整个矩阵填一格(乘法)。

三、怎么实现:抽象持有实现,搭一座桥

先破除名词障碍。GoF 说的:

  • “抽象部分”(Abstraction) = 遥控器这一维(高层控制逻辑,面向用户)。
  • “实现部分”(Implementor) = 设备这一维(底层具体干活)。

注意这里的“实现”不是“接口的实现类”那个意思,而是“另一个维度里真正干活的那方”。这个词是所有误解的源头,记住它=设备维就行。

go/bridge/main.go(核心)
// 实现维:设备(TV/Radio 各自实现)
type Device interface {
	Name() string; IsEnabled() bool; Enable(); Disable()
	GetVolume() int; SetVolume(percent int)
}

// 抽象维:遥控器,持有一个 Device —— 这个"持有"就是那座"桥"
type RemoteControl struct {
	device Device        // ← 桥:遥控器这一维,通过它连到设备那一维
}
func (r *RemoteControl) TogglePower() string {
	if r.device.IsEnabled() { r.device.Disable(); return r.device.Name()+" 已关闭" }
	r.device.Enable(); return r.device.Name()+" 已开启"
}

// 扩展抽象维:高级遥控器,组合复用基础遥控器,再加新功能
type AdvancedRemoteControl struct {
	RemoteControl        // 内嵌复用
}
func (r *AdvancedRemoteControl) Mute() string {
	r.device.SetVolume(0); return r.device.Name()+" 已静音"
}

于是任意遥控器 × 任意设备自由组合,加维度只需加一个类:

basic := &RemoteControl{device: NewTV()}              // 基础遥控 控 电视
adv := NewAdvancedRemoteControl(NewRadio())           // 高级遥控 控 收音机
adv.Mute()                                            // 高级功能,对任何设备都好使

四、有哪些坑

  • 最大的坑是“看不出该用它”。 桥接的收益,建立在你**提前识别出“这里有两个独立维度”**之上。识别对了,它优雅无比;识别错了(其实只有一个维度,或者两维根本纠缠在一起),硬套桥接就是给自己找麻烦。经验:当你发现类名开始出现 AForB、XxxWithYyy 这种“两个词拼起来”的组合命名时,警报响起——大概率该上桥接了。
  • 和策略模式结构像,意图不同。 两者都是“持有一个接口对象”。但策略是“换算法”(同一件事的不同做法,运行时替换),桥接是“解耦两个维度”(两件正交的事,各自演化)。策略通常一个维度,桥接明确是两个。
  • 和适配器分清: 适配器是事后补救(两套接口没对上,加层翻译),桥接是事前规划(设计之初就预见双维度,主动拆开)。一个救火,一个防火。
  • 别过早桥接。 只有一个维度会变的时候,引入桥接纯属自缚手脚。等第二个独立维度真的冒头,再重构成桥接不迟。

五、开源里怎么用

  • Go database/sql —— 一座教科书级的桥。 sql.DB / sql.Rows 这套是“抽象维”(统一的数据库操作 API,你的业务代码用它),database/sql/driver 定义的接口由各家驱动实现,是“实现维”(MySQL 驱动、Postgres 驱动各自干活)。你的 SQL 代码(抽象维)和具体数据库(实现维)独立变化:换数据库只换驱动,业务代码不动;标准库升级 API,驱动也不用重写。这就是桥接。
  • JDBC:Java 世界的同款——java.sql 是抽象维,各厂商的 Driver 是实现维,DriverManager 负责搭桥。GoF 书里桥接的原型场景之一。
  • 日志门面 × 后端:SLF4J(抽象维)× logback/log4j(实现维);Go log/slog 的 Logger(抽象维)× Handler(实现维)。你写日志的代码和“日志往哪写、什么格式”两个维度彻底解耦。
  • 跨平台绘图/GUI:一个 Shape/Window 抽象维,配一套各平台的绘制“实现维”(Windows GDI、macOS Quartz、Linux X11)。图形逻辑和平台绘制各自演化——GoF 原书举的正是这个例子。

桥接管“两个维度”,下一篇组合管“树形结构”:当对象自己能包含同类对象、层层嵌套成一棵树时,怎么让“单个”和“一组”被一视同仁地对待。

本页目录