09|桥接:拆开业务类型与实现方式两条变化轴
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.bridge。
系列导航:Java 25 设计模式学习指南
通知有普通提示与警报两种业务类型,也有纯文本与 Markdown 两种呈现方式。如果用继承枚举组合,会出现 TextInfoNotice、MarkdownInfoNotice、TextAlertNotice、MarkdownAlertNotice;新增一种呈现方式后,还要再增加一整排组合类。
问题不在类名太长,而在两条独立变化的轴被绑进同一棵继承树。桥接让业务类型保留自己的层级,让呈现实现通过组合接入。
从问题提炼设计意图
桥接把抽象部分与实现部分分离,使两者可以独立扩展。这里的抽象部分是通知业务,实现部分是呈现策略接口及其实现。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
Notice |
业务抽象,拥有 display 流程并持有 Renderer |
InfoNotice、AlertNotice |
扩充抽象,定义不同业务标题 |
Renderer |
实现接口,约定如何呈现标题和正文 |
TextRenderer、MarkdownRenderer |
具体呈现实现 |
classDiagram
Notice <|-- InfoNotice
Notice <|-- AlertNotice
Notice --> Renderer : bridge
Renderer <|.. TextRenderer
Renderer <|.. MarkdownRenderer
代码思路:变化应该落在哪个对象上
Notice 不继承任何 Renderer,而是保存一个 private final Renderer 字段。构造器决定把哪种实现与本次业务对象配对,display 只调用接口,不检查 renderer 的具体类型。
业务标题由 title 抽象方法提供,所以 AlertNotice 只关心 ALERT 这层业务意义。MarkdownRenderer 只关心把给定标题和正文排成 Markdown,它无需知道输入来自哪种通知。
这样,新增通知类型只扩展 Notice 一侧,新增呈现器只扩展 Renderer 一侧。两种业务乘两种实现仍有四种运行组合,但不需要四个组合子类。测试覆盖这四种搭配,证明两条轴确实可以自由组合。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.bridge,源码目录为 src/main/java/com/hanserwei/patterns/bridge/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
Notice.java:
package com.hanserwei.patterns.bridge;
import java.util.Objects;
/** 通知业务抽象,通过组合连接可独立变化的呈现实现. */
public abstract class Notice {
/** 本通知使用的呈现器. */
private final Renderer renderer;
/** 装配呈现实现. */
protected Notice(Renderer renderer) {
this.renderer = Objects.requireNonNull(renderer, "renderer");
}
/** 组合业务标题与非空正文,交由呈现器输出. */
public final String display(String body) {
return renderer.render(title(), Objects.requireNonNull(body, "body"));
}
/** 由具体通知定义业务标题. */
protected abstract String title();
}
AlertNotice.java:
package com.hanserwei.patterns.bridge;
/** 通知业务维度中的具体类型. */
public final class AlertNotice extends Notice {
/** 接收独立选择的呈现器. */
public AlertNotice(Renderer renderer) {
super(renderer);
}
/** 返回本通知类型的业务标题. */
@Override
protected String title() {
return "ALERT";
}
}
MarkdownRenderer.java:
package com.hanserwei.patterns.bridge;
/** 桥接结构中的具体呈现实现. */
public final class MarkdownRenderer implements Renderer {
/** 呈现已经由通知对象准备好的标题和正文. */
@Override
public String render(String title, String body) {
return "## " + title + "\n" + body;
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.bridge;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
Notice notice = new AlertNotice(new MarkdownRenderer());
System.out.println(notice.display("Review required"));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runBridge
./gradlew test --tests 'com.hanserwei.patterns.bridge.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
## ALERT
Review required
用测试确认模式的行为
四种组合分别得到正确标题与格式;缺失呈现器时拒绝构造。组合覆盖可以发现某个业务子类偷偷绑定具体呈现器的问题。
对应测试位于 src/test/java/com/hanserwei/patterns/bridge/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 消息种类与发送或呈现实现需要分别扩展。
- 业务报表与输出设备、文件格式存在独立组合。
- 跨平台图形组件的高层行为和底层绘制实现需要解耦。
适用边界与容易踩的坑
不要因为存在一个接口字段就立刻称为桥接。需要先识别两条确实有独立变化需求的轴;如果只替换一次算法,策略模式的描述可能更准确。
桥接接口应稳定且足够小。若每增加一种通知都必须给 Renderer 添加专用方法,业务细节仍然穿透到了实现层,两个维度就没有真正分开。反过来,也不能为了统一接口而丢失关键能力。
本例保留装配时固定的 Renderer,没有提供任意 setRenderer。运行期间能否更换实现是业务选择,不是模式硬性要求;固定依赖更容易推理生命周期。Markdown 呈现仅供教学,不包含完整语法转义。
与相近模式比较
桥接强调抽象层级和实现层级独立变化;策略强调给上下文替换某个算法。类图可能接近,判断时应该看变化需求和职责,而不是只看箭头。
动手练习
新增 SuccessNotice 与 JsonRenderer,要求两个改动互不修改对方已有类型。为新增组合写测试,然后检查 Renderer 是否被迫识别通知子类;如果出现 instanceof,重新检查职责边界。
系列导航:Java 25 设计模式学习指南