模式补全
这一节按 八面剖析以理解一个概念 / ljg-learn 的思路整理为 Markdown 版本:先定锚,再用八个方向切开概念,最后压缩成公式、例子、类图和检验题。
定锚
桥接模式(结构型)的通行定义:把抽象部分和实现部分分离,让它们可以独立变化。
常见误解:桥接不是普通封装;它专门处理两个维度同时扩展导致的类爆炸。
核心词素:Bridge 的桥连接两岸:抽象层一岸,实现层一岸,中间靠组合通信。
八刀
历史
它来自图形、驱动、平台适配等多维变化场景。GoF 用它对抗继承层级膨胀。
辩证
反面是用继承枚举所有组合。更高理解是:把一个继承树拆成两个可组合维度。
现象
视频播放器需要同时面对两个变化维度:一边是操作系统平台(Windows、Linux、Unix),另一边是视频格式(MPEG、RMVB、AVI、WMV)。如果直接用继承硬拼,就会出现 WindowsMPEGPlayer、LinuxAVIPlayer、UnixRMVBPlayer 这一类组合爆炸。
语言
Bridge 的桥连接两岸:抽象层一岸,实现层一岸,中间靠组合通信。
形式
Abstraction has Implementor。若只有一个变化维度,桥接会显得过度设计。
存在
它让人从“分类继承”转向“维度组合”,系统因此更能呼吸。
美感
它美在横跨:两组变化各走各路,却在运行时轻轻合拢。
元反思
桥的隐喻容易让人只看连接。换成“坐标轴”隐喻,更容易看到它解决二维扩展。
内观
我把播放器的“平台”与“视频格式解码”拆开。平台负责“在哪里播”,格式负责“怎么解码”,二者彼此独立扩展,通过组合在运行时接起来。
八刀共同指向的深层结构:桥接模式不是为了“炫技”,而是在某个变化点上建立边界,让稳定部分继续稳定,让变化部分有自己的位置。
压缩
公式:桥接 = 抽象层组合实现层 + 两个维度独立扩展
一句话:桥接用组合拆掉多维继承爆炸。
结构图:
Abstraction -> Implementor
RefinedAbstraction + ConcreteImplementor动机/意图
当一个类同时沿着两个或多个维度变化时,用继承会产生组合爆炸。桥接模式的意图是把抽象维度和实现维度拆开,让抽象对象通过组合持有实现接口,从而使两个维度可以独立扩展。
结构/角色
Abstraction:抽象层,定义高层业务接口,持有实现层引用。RefinedAbstraction:扩展抽象层,增加或改写高层行为。Implementor:实现层接口,定义底层能力。ConcreteImplementor:具体实现层,提供平台、渠道或设备相关实现。Client:客户端组合抽象层和实现层。
典型 UML
classDiagram class Abstraction { -implementor: Implementor +operation(): void } class RefinedAbstraction class Implementor { <<interface>> +operationImpl(): void } class ConcreteImplementorA class ConcreteImplementorB Abstraction <|-- RefinedAbstraction Abstraction --> Implementor Implementor <|.. ConcreteImplementorA Implementor <|.. ConcreteImplementorB
使用场景
- 抽象和实现都需要独立扩展,例如形状与颜色、消息类型与发送渠道。
- 多维继承导致类数量快速膨胀。
- 不希望抽象层绑定某个具体平台或技术实现。
- 需要在运行时切换实现,而不是在继承层级中固定死。
正例:TypeScript
这个场景里,“播放器平台”是抽象层,“视频文件格式”是实现层。平台和格式都可能独立扩展,所以让平台持有格式对象,而不是为每个组合都造一个子类。
interface VideoFile {
decode(fileName: string): void
}
class MPEGFile implements VideoFile {
decode(fileName: string) {
console.log(`decode MPEG file: ${fileName}`)
}
}
class AVIFile implements VideoFile {
decode(fileName: string) {
console.log(`decode AVI file: ${fileName}`)
}
}
class RMVBFile implements VideoFile {
decode(fileName: string) {
console.log(`decode RMVB file: ${fileName}`)
}
}
abstract class OperationSystem {
constructor(protected videoFile: VideoFile) {}
abstract play(fileName: string): void
}
class WindowsPlayer extends OperationSystem {
play(fileName: string) {
console.log("run on Windows")
this.videoFile.decode(fileName)
}
}
class LinuxPlayer extends OperationSystem {
play(fileName: string) {
console.log("run on Linux")
this.videoFile.decode(fileName)
}
}
class UnixPlayer extends OperationSystem {
play(fileName: string) {
console.log("run on Unix")
this.videoFile.decode(fileName)
}
}
const player = new WindowsPlayer(new AVIFile())
player.play("movie.avi")正例:UML 类图
classDiagram class OperationSystem { -videoFile: VideoFile +play(fileName: string): void } class WindowsPlayer { +play(fileName: string): void } class LinuxPlayer { +play(fileName: string): void } class UnixPlayer { +play(fileName: string): void } class VideoFile { <<interface>> +decode(fileName: string): void } class MPEGFile { +decode(fileName: string): void } class AVIFile { +decode(fileName: string): void } class RMVBFile { +decode(fileName: string): void } OperationSystem <|-- WindowsPlayer OperationSystem <|-- LinuxPlayer OperationSystem <|-- UnixPlayer OperationSystem o-- VideoFile VideoFile <|.. MPEGFile VideoFile <|.. AVIFile VideoFile <|.. RMVBFile
反例:TypeScript
反例里把“平台”和“格式”直接揉进同一层继承或同一级具体类中。每多一个平台或格式,都要新增一批组合类,扩展成本会成倍增长。
class WindowsMPEGPlayer {
play(fileName: string) {
console.log(`Windows play MPEG: ${fileName}`)
}
}
class WindowsAVIPlayer {
play(fileName: string) {
console.log(`Windows play AVI: ${fileName}`)
}
}
class LinuxMPEGPlayer {
play(fileName: string) {
console.log(`Linux play MPEG: ${fileName}`)
}
}
class LinuxAVIPlayer {
play(fileName: string) {
console.log(`Linux play AVI: ${fileName}`)
}
}
class UnixRMVBPlayer {
play(fileName: string) {
console.log(`Unix play RMVB: ${fileName}`)
}
}反例:UML 类图
classDiagram class WindowsMPEGPlayer { +play(fileName: string): void } class WindowsAVIPlayer { +play(fileName: string): void } class WindowsRMVBPlayer { +play(fileName: string): void } class LinuxMPEGPlayer { +play(fileName: string): void } class LinuxAVIPlayer { +play(fileName: string): void } class UnixWMVPlayer { +play(fileName: string): void }
案例
现需要提供大中小3种型号的画笔,能够绘制5种不同颜色,如果使用蜡笔,我们需要准备3*5=15支蜡笔,也就是说必须准备15个具体的蜡笔类。而如果使用毛笔的话,只需要3种型号的毛笔,外加5个颜料盒,用3+5=8个类就可以实现15支蜡笔的功能。本实例使用桥接模式来模拟毛笔的使用过程
如果需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Linux、Unix等)上播放多种格式的视频文件,常见的视频格式包括MPEG、RMVB、AVI、WMV等。现使用桥接模式设计该播放器。
掌握检验
- 桥接模式解决的“两个变化维度”是什么意思?
- 桥接和适配器都用了组合,它们面对的问题有何不同?
- 如何判断当前是需要桥接,而不是继续继承?