桥接模式如何解决多维度扩展导致的类爆炸?
简化版
桥接模式把多个变化维度从一条继承链中拆出来,通过组合关系连接。这样新增一个维度值只需要新增对应类,而不是为它和其他维度的每一种组合都创建子类。
详细版
类爆炸通常来自乘法组合。比如消息系统中有 3 种消息类型和 4 种发送渠道,如果用继承表达组合,可能需要 12 个类;以后增加 1 种消息类型,又要补 4 个类。
桥接模式会把它拆成:
- 消息类型维度:
NormalMessage、UrgentMessage、VerifyCodeMessage。 - 发送渠道维度:
SmsSender、EmailSender、WeComSender、PushSender。
消息类型通过 Sender 接口委托发送。新增消息类型只影响消息侧,新增发送渠道只影响渠道侧。
完整版教学
一、类爆炸的根源是维度相乘
单一维度扩展时,继承不一定有问题。例如只有图形类型变化,新增圆形、矩形、三角形,类数量大致是线性增长。
问题出现在两个维度同时变化:
| 维度 A | 维度 B | 继承组合数量 |
|---|---|---|
| 2 种 | 3 种 | 6 个类 |
| 4 种 | 5 种 | 20 个类 |
| 8 种 | 6 种 | 48 个类 |
随着维度增多,类数量会快速膨胀。更麻烦的是,很多类只是简单组合,没有真正新的业务价值。
二、继承方案为什么维护困难
假设用继承设计图形绘制:
Shape
├── SvgCircle
├── CanvasCircle
├── SvgRectangle
└── CanvasRectangle
如果新增 Triangle,要加 SvgTriangle 和 CanvasTriangle。如果再新增 OpenGL 绘制方式,要为所有形状补一批 OpenGLXxx。
这种设计有几个问题:
- 类数量多,命名和目录都膨胀。
- 相同逻辑在多个组合类中重复。
- 新增一个维度值时要扫描另一个维度的所有已有值。
- 修改公共规则时容易漏改某些组合类。
三、桥接模式如何拆维度
桥接方案会变成:
Shape 抽象侧
├── Circle
├── Rectangle
└── Triangle
Renderer 实现侧
├── SvgRenderer
├── CanvasRenderer
└── OpenGLRenderer
Shape 持有 Renderer:
abstract class Shape {
protected final Renderer renderer;
protected Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
这样类数量从乘法关系变成加法关系。3 种形状和 3 种渲染方式不再需要 9 个组合类,而是 3 个形状类 + 3 个渲染类。
四、扩展成本如何下降
桥接模式下新增一个形状:
- 新增一个
Shape子类。 - 复用所有已有
Renderer。
新增一种渲染方式:
- 新增一个
Renderer实现类。 - 复用所有已有
Shape。
这就是开闭原则的体现:对扩展开放,对已有类修改关闭。
五、桥接不是减少所有类
桥接模式并不是让类数量永远最少。它有时会比简单写法多出接口和抽象类。
它真正减少的是组合类和重复代码。对于变化维度明确、组合关系复杂的系统,这个收益很明显;对于只有两三个固定组合的小功能,桥接可能显得过度设计。
六、判断是否存在多维度扩展
可以观察需求描述中的连接词:
- “不同类型的 A 支持不同方式的 B”
- “同一个业务能力要支持多个平台”
- “同一种操作要输出多种格式”
- “不同设备对应不同遥控器”
这些表达往往暗示两个维度:业务形态和实现方式。如果未来都可能扩展,就值得考虑桥接。
七、常见误区与追问
二维扩展的验收要同时观察类型数与修改面。初始2种通知×2种渠道,运行期有4种组合,但代码只维护4个具体类型;增加第3种通知后类型总数变5,不是6个组合子类。若增加一种通知仍要进入所有渠道类修改,说明维度边界泄漏。
| 检查维度 | 判定依据 |
|---|---|
| 新增抽象成员 | 只扩展抽象侧类型 |
| 新增实现成员 | 只扩展 Implementor 侧类型 |
运行组合 3×2=6;核心具体类型 3+2=5
易错点:桥接减少的是组合类数量和修改传播,不是减少可用组合。
- 误区:两个维度必须数量相同。 两侧数量彼此独立,任何抽象都可组合兼容实现。
- 追问:如何限制不合法组合? 在工厂或能力声明中校验兼容矩阵,而不是把分支散到两侧。
- 误区:新增维度成员完全零修改。 注册配置或工厂映射可能需要更新,但核心另一侧业务类型不应改。
- 追问:能否支持运行期切换渠道? 若状态和线程安全允许,可替换 Implementor 或创建新 Abstraction。
- 追问:第三个变化维度怎么办? 先评估是否真正正交,再组合新接口,避免对象装配失控。
八、加强记忆
桥接模式解决类爆炸的方式是把“乘法组合”改成“加法扩展”:继承会让 A × B 变成大量组合类,桥接让 A 和 B 各自成体系,再用组合把它们连起来。