Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

14_桥接模式 Bridge Pattern

1. 基本信息

中文名称: 桥接模式

英文名称: Bridge Pattern

模式类型: 结构型设计模式

Rust 中常见实现方式: trait、结构体组合、泛型、Box<dyn Trait>、关联类型

桥接模式的核心是:将抽象部分和实现部分分离,使它们可以独立变化。

在 Rust 中,桥接模式通常不是通过继承实现,而是通过:

trait 定义实现接口
struct 持有实现对象
泛型或 dyn Trait 连接抽象和实现

代码示例


2. 模式核心思想

桥接模式的核心思想是:

把两个容易一起变化的维度拆开,让它们通过组合关系连接,而不是通过继承关系绑定。

也就是说,桥接模式解决的是:

抽象层要变化
实现层也要变化
如果两者直接绑定,类型数量会迅速膨胀

例如:

图形类型:圆形、矩形、三角形
绘制平台:Windows、Linux、Web

如果不用桥接模式,可能会出现:

WindowsCircle
LinuxCircle
WebCircle
WindowsRectangle
LinuxRectangle
WebRectangle
WindowsTriangle
LinuxTriangle
WebTriangle

类型数量会随着两个维度相乘增长。

桥接模式的思路是:

图形是一套抽象
绘制平台是一套实现
图形内部持有绘制平台接口

这样图形和平台可以独立扩展。


3. 这个模式解决什么问题

桥接模式主要解决的是:抽象和实现存在多个变化维度时,如何避免它们强绑定。

在实际开发中,经常会遇到这种情况:

1. 一个业务抽象有多种类型。
2. 每种业务抽象又可以对应多种底层实现。
3. 抽象和实现都可能独立扩展。
4. 不希望为每种组合都创建一个新类型。

例如:

消息类型:普通消息、加急消息、广播消息
发送方式:邮件、短信、微信、MQTT

图形类型:圆形、矩形、线段
渲染后端:OpenGL、Vulkan、Canvas

日志类型:文本日志、JSON 日志
输出方式:控制台、文件、远程服务器

桥接模式的价值在于:

让抽象和实现分离,
避免继承层级或类型组合爆炸。

4. 不使用这个模式会怎样

如果不使用桥接模式,可能会出现以下问题:

1. 抽象类型和实现类型强耦合。
2. 每新增一种抽象或实现,都需要创建大量组合类型。
3. 代码结构变成多层继承或大量重复结构。
4. 修改实现层可能影响抽象层。
5. 扩展新功能时容易牵连多个模块。

例如消息系统中,如果直接把消息类型和发送方式绑定,可能会写出:

EmailNormalMessage
SmsNormalMessage
WechatNormalMessage
EmailUrgentMessage
SmsUrgentMessage
WechatUrgentMessage

如果后续新增一种消息类型或发送方式,组合数量继续增长。

桥接模式就是要把这种结构拆成:

消息抽象
发送实现

然后通过组合连接。


5. 传统面向对象中的实现思路

在传统 OOP 中,桥接模式通常包含两个层次:

1. Abstraction:抽象部分,定义高层业务接口。
2. RefinedAbstraction:扩展抽象。
3. Implementor:实现部分接口。
4. ConcreteImplementor:具体实现。

典型结构是:

Abstraction
 ├── implementor: Implementor
 └── operation()

Implementor
 └── operation_impl()

ConcreteImplementorA
 └── operation_impl()

ConcreteImplementorB
 └── operation_impl()

抽象对象内部持有实现接口,然后把具体实现委托给实现对象。

桥接模式的关键是:

抽象不直接继承具体实现,
而是通过组合持有实现接口。

这和“组合优于继承”的思想非常接近。


6. Rust 中的实现思路

6.1 Rust 中通常怎么实现

Rust 中通常通过 trait + 结构体组合 实现桥接模式。

下面以“消息发送”为例。

发送方式是一套实现维度:

#![allow(unused)]
fn main() {
trait MessageSender {
    fn send(&self, content: &str);
}

struct EmailSender;

impl MessageSender for EmailSender {
    fn send(&self, content: &str) {
        println!("通过邮件发送:{}", content);
    }
}

struct SmsSender;

impl MessageSender for SmsSender {
    fn send(&self, content: &str) {
        println!("通过短信发送:{}", content);
    }
}
}

消息类型是一套抽象维度:

#![allow(unused)]
fn main() {
struct NormalMessage<S: MessageSender> {
    sender: S,
}

impl<S: MessageSender> NormalMessage<S> {
    fn new(sender: S) -> Self {
        Self { sender }
    }

    fn send(&self, content: &str) {
        self.sender.send(content);
    }
}

struct UrgentMessage<S: MessageSender> {
    sender: S,
}

impl<S: MessageSender> UrgentMessage<S> {
    fn new(sender: S) -> Self {
        Self { sender }
    }

    fn send(&self, content: &str) {
        let content = format!("[加急] {}", content);
        self.sender.send(&content);
    }
}
}

使用:

fn main() {
    let normal_email = NormalMessage::new(EmailSender);
    normal_email.send("系统通知");

    let urgent_sms = UrgentMessage::new(SmsSender);
    urgent_sms.send("服务器异常");
}

这里有两个独立变化维度:

消息类型:NormalMessage / UrgentMessage
发送方式:EmailSender / SmsSender

它们没有互相继承,而是通过 sender: S 组合起来。


如果需要运行时动态选择实现,可以使用 Box<dyn MessageSender>

struct DynamicMessage {
    sender: Box<dyn MessageSender>,
}

impl DynamicMessage {
    fn new(sender: Box<dyn MessageSender>) -> Self {
        Self { sender }
    }

    fn send(&self, content: &str) {
        self.sender.send(content);
    }
}

fn main() {
    let sender: Box<dyn MessageSender> = Box::new(EmailSender);
    let message = DynamicMessage::new(sender);

    message.send("动态选择发送方式");
}

这种写法更灵活,但有动态分发开销。


6.2 和传统 OOP 写法相比有什么不同

和传统面向对象语言相比,Rust 的桥接模式有几个明显特点。

第一,Rust 不依赖继承体系。 传统 OOP 中常通过抽象类和实现接口来分离层次,Rust 中通常通过 trait + struct 组合 完成。

第二,Rust 可以用泛型实现静态桥接。 例如:

struct NormalMessage<S: MessageSender>

这种写法在编译期确定具体实现,性能较好。

第三,Rust 可以用 Box<dyn Trait> 实现动态桥接。 如果需要运行时切换实现,可以用 trait object。

第四,Rust 更强调组合关系。 桥接模式本质上就是组合,而 Rust 本身也更鼓励用组合替代继承。

第五,Rust 中需要考虑所有权。 抽象对象持有实现对象时,要明确:

是拥有实现对象?
是借用实现对象?
是共享实现对象?
是否需要多线程共享?

根据不同情况,可以选择:

S
&S
Box<dyn Trait>
Rc<dyn Trait>
Arc<dyn Trait>

6.3 Rust 中是否有更自然的替代写法

在 Rust 中,桥接模式不一定要写成传统的 Abstraction / Implementor 结构。

如果实现类型在编译期确定,推荐:

泛型 + trait bound

如果实现类型要运行时切换,推荐:

Box<dyn Trait>

如果实现对象需要共享,推荐:

Rc<dyn Trait>
Arc<dyn Trait>

如果两个维度都很简单,直接用 enum + match 可能更清楚。

例如发送方式只有两三种,而且不需要扩展时:

enum SenderKind {
    Email,
    Sms,
}

就可能已经足够。

所以在 Rust 中使用桥接模式时,关键不是套结构,而是先判断:

是否真的存在两个独立变化维度?
这两个维度是否都需要扩展?
是否需要运行时选择实现?

如果答案是肯定的,桥接模式才更有价值。


7. Rust 中涉及的语言特性

桥接模式在 Rust 中主要涉及以下语言特性:

1. trait
2. struct
3. impl
4. 泛型
5. trait bound
6. Box<dyn Trait>
7. Rc / Arc
8. 所有权与借用
9. 关联类型

这些特性分别解决的问题是:

trait:定义实现层接口
struct:定义抽象层对象
泛型:静态连接抽象和实现
Box<dyn Trait>:动态连接抽象和实现
Rc / Arc:共享实现对象
所有权:管理抽象对象和实现对象之间的关系
关联类型:在复杂桥接结构中约束实现类型

其中最核心的是:

trait + 结构体组合

也就是:

抽象对象内部持有一个实现接口。

8. 性能与工程代价

桥接模式的性能取决于使用静态分发还是动态分发。

如果使用泛型桥接:

1. 编译期确定具体实现。
2. 没有虚函数调用。
3. 编译器可以内联优化。
4. 性能通常接近直接调用。

如果使用 Box<dyn Trait>

1. 支持运行时选择实现。
2. 有动态分发开销。
3. 通常涉及堆分配。
4. 灵活性更强,但性能略低。

如果使用 Arc<dyn Trait>

1. 支持多线程共享实现对象。
2. 有原子引用计数开销。
3. 适合跨线程场景。

工程代价方面,桥接模式会增加一层抽象:

1. 需要设计抽象层和实现层。
2. 简单场景下可能显得复杂。
3. 过度桥接会让代码绕。
4. 泛型版本可能增加类型复杂度。
5. 动态版本可能增加运行时开销。

一句话总结:

桥接模式用结构复杂度换取两个维度的独立扩展能力。

9. 典型应用场景

桥接模式适合用于两个维度都可能变化的场景。

常见应用包括:

1. 图形类型 × 绘制平台
2. 消息类型 × 发送方式
3. 日志格式 × 输出目标
4. 数据库操作 × 数据库类型
5. UI 控件 × 平台实现
6. 文件格式 × 存储后端
7. 报表类型 × 导出格式
8. 支付业务 × 支付渠道
9. 设备控制命令 × 设备驱动实现

在 Rust 项目中,桥接模式可能出现在:

日志库设计
跨平台 GUI 抽象
数据库访问层
消息通知系统
图形渲染后端
嵌入式设备抽象层
插件化后端系统

例如日志系统可以拆成两个维度:

日志格式:PlainText / Json / Compact
输出目标:Console / File / Remote

桥接后就不需要写:

ConsoleJsonLogger
FileJsonLogger
RemoteJsonLogger
ConsoleTextLogger
FileTextLogger
RemoteTextLogger

而是让格式和输出目标独立组合。


10. 和相似模式的区别

桥接模式容易和适配器模式、策略模式、装饰器模式、代理模式混淆。

10.1 桥接模式和适配器模式

二者都可能使用包装和 trait,但目的不同。

桥接模式:提前设计抽象和实现的分离,使两个维度可以独立变化。
适配器模式:事后解决接口不兼容问题,让已有接口适配新接口。

桥接模式更像是:

系统设计时就把两个变化维度拆开。

适配器模式更像是:

已经有两个接口不兼容,现在加一层转换。

例如:

消息类型和发送方式独立扩展:桥接模式
把旧打印机接口适配成新打印接口:适配器模式

10.2 桥接模式和策略模式

二者都可能用 trait 表示可替换实现,但关注点不同。

策略模式:关注算法或行为的替换。
桥接模式:关注抽象层和实现层两个维度的分离。

策略模式通常只有一个变化点:

排序算法可以换。

桥接模式通常有两个变化维度:

消息类型可以变,发送方式也可以变。

例如:

选择 zip 或 gzip 压缩:策略模式
普通消息/加急消息 × 邮件/短信发送:桥接模式

10.3 桥接模式和装饰器模式

二者都使用组合,但目的不同。

桥接模式:分离抽象和实现,使二者独立变化。
装饰器模式:在不修改原对象的情况下增强功能。

桥接模式解决的是:

两个维度如何独立扩展。

装饰器模式解决的是:

如何给对象叠加额外功能。

例如:

图形对象选择不同渲染后端:桥接模式
给文件读取器增加缓存、压缩、加密:装饰器模式

10.4 桥接模式和代理模式

二者都可能持有另一个对象,但目的不同。

桥接模式:连接抽象和实现两个层次。
代理模式:控制对原对象的访问。

代理模式更强调:

权限检查
延迟加载
远程调用
缓存访问

桥接模式更强调:

抽象和实现解耦
两个维度独立扩展

11. 使用该模式的优点

桥接模式的优点主要有:

1. 分离抽象和实现,降低耦合。
2. 避免多维度组合导致类型数量爆炸。
3. 抽象层和实现层可以独立扩展。
4. 更符合组合优于继承的思想。
5. 在 Rust 中可以通过 trait 和泛型获得较好性能。
6. 可以根据需要选择静态分发或动态分发。

从工程角度看,桥接模式的价值在于:

让系统面对多个变化维度时,结构仍然保持清晰。

12. 使用该模式的代价

桥接模式的代价主要包括:

1. 增加抽象层,代码结构更复杂。
2. 需要提前识别系统中的变化维度。
3. 对简单场景来说可能过度设计。
4. 使用 Box<dyn Trait> 会有动态分发和堆分配开销。
5. 泛型桥接可能让类型定义变长。
6. 抽象层设计不好时,反而会增加理解成本。

特别是在 Rust 中,不要为了“显得设计高级”而强行桥接。

如果一个系统只有一个变化维度,策略模式可能就够了。

如果只是接口不兼容,适配器模式更合适。


13. 什么时候不应该使用

以下情况不适合使用桥接模式:

1. 只有一个变化维度。
2. 抽象和实现不会独立变化。
3. 类型数量很少,直接实现更清楚。
4. 系统不需要运行时切换实现。
5. enum + match 已经足够表达。
6. 为了套模式而人为拆分结构。

在 Rust 中可以遵循一个简单原则:

如果只是替换算法,考虑策略模式。
如果是接口不兼容,考虑适配器模式。
如果是两个维度都要独立扩展,再考虑桥接模式。

14. 一个简单例子思路

场景: 消息系统

需求:

消息类型有普通消息和加急消息。
发送方式有邮件发送和短信发送。
消息类型和发送方式都可能继续扩展。

处理流程:

1. 定义 MessageSender trait 作为实现层接口。
2. EmailSender 和 SmsSender 实现 MessageSender。
3. NormalMessage 和 UrgentMessage 作为抽象层对象。
4. 消息对象内部持有 sender。
5. 调用 send() 时,消息对象处理自己的抽象逻辑,再委托 sender 发送。

适合练习的 Rust 特性:

trait
泛型
trait bound
Box<dyn Trait>
struct 组合
所有权
动态分发和静态分发对比

这个例子可以体现桥接模式的核心:

消息类型和发送方式是两个独立变化维度。
桥接模式让它们可以自由组合。

15. 总结一句话

桥接模式的本质是:

把抽象部分和实现部分拆开,通过组合连接,使二者可以独立变化。

在 Rust 中,桥接模式通常通过 trait + 结构体组合 实现;如果实现类型在编译期确定,可以使用泛型和 trait bound;如果需要运行时切换实现,可以使用 Box<dyn Trait>。桥接模式适合两个维度都可能扩展的场景,不适合简单对象或只有单一变化点的场景。