12.5.4 例题4:桥接模式
本节例题是绘图软件:系统要用不同绘图程序绘制不同图形,例如直线和圆形。图形类型会扩展,绘图程序也会扩展。课程借这个例题讲桥接模式,并特别提醒:题干中的表格、方法名和参数顺序都可能直接决定填空答案。
桥接模式要解决“两个维度都变化”
如果只有一个变化维度,继承还能处理。例如只有图形变化,可以有 Line、Circle;只有绘图程序变化,可以有 DP1、DP2。但本题同时有两个维度:
| 维度 | 例题含义 | 可能变化 |
|---|---|---|
| 抽象维度 | 图形:直线、圆形、矩形等 | 新增图形 |
| 实现维度 | 绘图程序:DP1、DP2 等 | 新增绘图平台/程序 |
如果用继承硬拼,会出现 DP1Line、DP2Line、DP1Circle、DP2Circle……每新增一个图形或一个绘图程序,类数量交叉增长,这就是类爆炸。
flowchart TD
Bad["继承硬拼"] --> B1["DP1Line"]
Bad --> B2["DP2Line"]
Bad --> B3["DP1Circle"]
Bad --> B4["DP2Circle"]
Good["桥接拆分"] --> Shape["Shape 图形层"]
Good --> Drawing["Drawing 绘图程序层"]
Shape --> Bridge["Shape 持有 Drawing"]
Drawing --> Bridge桥接结构
桥接模式把两个维度拆成两个继承体系,再用组合把它们连起来。
| 角色 | 在例题中的含义 | 填空线索 |
|---|---|---|
| Implementor | 绘图程序接口,如 Drawing | 由 implements Drawing 反推它是接口 |
| ConcreteImplementor | V1Drawing、V2Drawing | 实现画线、画圆等底层方法 |
| Abstraction | Shape 抽象图形 | 持有 Drawing 成员 |
| RefinedAbstraction | Line、Circle | 调用 Drawing 完成绘制 |
接口空:从实现类反推
课程中第一组空出现在类似 Drawing 的接口上。判断依据是:下方类写了 implements Drawing,说明 Drawing 是接口;实现类中出现了画线、画圆两个方法,说明接口中缺的就是这两个方法声明。
interface Drawing {
void drawLine(double x1, double y1, double x2, double y2);
void drawCircle(double x, double y, double r);
}接口方法声明有几个关键点:
| 要素 | 是否能省 | 原因 |
|---|---|---|
| 返回值类型 | 不能 | 方法签名必需 |
| 方法名 | 不能 | 要与实现类一致 |
| 参数列表 | 不能 | 参数个数和顺序会影响调用 |
方法体 {} | 不能写 | 接口方法不写具体实现 |
abstract | 通常不用写 | 接口方法天然抽象 |
表格不是装饰:它决定方法名
课程提醒,题干中如果在常规描述之外给出表格,后面通常会用到。本题表格列出不同绘图程序的实际方法名,例如:
| 绘图程序 | 画线方法 | 画圆方法 |
|---|---|---|
| DP1 | drawLine(...) | draw_a_circle(x, y, r) |
| DP2 | drawline(...) 或其他命名 | drawCircle(x, y, r) |
不同程序的方法名可能相近但不完全相同。填空时要先确认当前类调用的是 DP1 还是 DP2,再去表格中找对应方法,不能把 DP2 的方法名填到 DP1 的实现里。
实现类方法体空:先定位当前程序
在 V1Drawing 中,如果它封装的是 DP1,那么画圆就应该调用 DP1 的画圆方法:
class V1Drawing implements Drawing {
private DP1 dp1 = new DP1();
public void drawCircle(double x, double y, double r) {
dp1.draw_a_circle(x, y, r);
}
}在 V2Drawing 中,如果它封装的是 DP2,就要调用 DP2 对应方法:
class V2Drawing implements Drawing {
private DP2 dp2 = new DP2();
public void drawCircle(double x, double y, double r) {
dp2.drawCircle(x, y, r);
}
}这类空的推导顺序是:
- 当前类是
V1Drawing还是V2Drawing。 - 当前类内部持有的是 DP1 还是 DP2。
- 表格中该程序画线/画圆的方法名是什么。
- 参数名和参数顺序是否与表格一致。
抽象类空:看到 abstract class 要警惕
课程后半段讲 Shape 抽象类。它持有绘图程序对象,并有构造函数、具体的 drawLine 或辅助方法。若它被声明为 abstract class,但代码中看不到抽象方法,就要怀疑空缺处是抽象方法。
abstract class Shape {
protected Drawing drawing;
public Shape(Drawing drawing) {
this.drawing = drawing;
}
public abstract void draw();
}
class Circle extends Shape {
public Circle(Drawing drawing) {
super(drawing);
}
public void draw() {
drawing.drawCircle(x, y, r);
}
}在抽象类中声明抽象方法时,abstract 关键字不能漏,方法体也不能写。
参数顺序是隐形扣分点
课程特别提到,类似题中曾考过参数顺序:一个方法可能是 x1, y1, x2, y2,另一个方法可能是 x1, x2, y1, y2。这不是小问题,顺序错了就等于调用语义错。
| 看到的线索 | 正确做法 |
|---|---|
| 表格给了方法签名 | 严格按表格抄参数顺序 |
| 代码变量名相似 | 不凭习惯重排 |
| 方法名来自不同程序 | 重新核对对应程序的签名 |
桥接模式的优劣
| 维度 | 优点 | 代价 |
|---|---|---|
| 扩展性 | 图形和绘图程序可独立扩展 | 初始结构比直接继承复杂 |
| 类数量 | 避免交叉继承导致类爆炸 | 需要多理解一层委托关系 |
| 复用性 | Shape 复用 Drawing 接口,具体程序可替换 | 调试时调用链更长 |
桥接模式之所以替代“多层继承硬拼”,不是因为继承过时,而是因为当变化维度超过一个时,继承会把维度耦合在同一个类名里,扩展成本呈乘法增长。桥接把乘法拆成加法。
与适配器的辨析
| 对比项 | 适配器 | 桥接 |
|---|---|---|
| 主要目的 | 接口转换 | 两个维度独立变化 |
| 典型题干 | 已有类不能满足新接口 | 新图形、新平台/程序都可能扩展 |
| 代码重点 | Adapter 调用 Adaptee | Abstraction 持有 Implementor |
| 设计时机 | 常是后期补救 | 常是前期设计 |
例题
自查清单
- 能否说明为什么“图形 × 绘图程序”会导致类爆炸?
- 能否从
implements反推出接口及其方法声明? - 能否根据表格找到 DP1/DP2 正确方法名?
- 能否严格保留参数列表和参数顺序?
- 能否区分桥接模式与适配器模式的动机?