Java 桥接模式(Bridge Pattern)实战指南:在 java-design-patterns 中解耦抽象与实现
Java 桥接模式(Bridge Pattern)实战指南:在 java-design-patterns 中解耦抽象与实现
桥接模式(Bridge)是 GoF 二十三种设计模式中的经典结构型模式,核心思想是"将抽象部分与它的实现部分分离,使它们都可以独立地变化"。本指南以 java-design-patterns 仓库中的 bridge 模块为实例,从模式意图、类层次结构、可运行的 Java 代码、测试验证到适用场景与权衡,完整拆解如何用组合(composition)取代继承,实现抽象维度与实现维度的自由搭配。读完本文,你将掌握 Weapon + Enchantment 双层次组合的落地写法,并能判断在 GUI、数据库驱动、设备驱动等场景中何时该引入桥接模式。
一、模式概述:Bridge 是什么
Bridge 模式是一种结构型设计模式,由 Erich Gamma 等四人组(Gang of Four)在《Design Patterns: Elements of Reusable Object-Oriented Software》中正式提出。它的核心意图是:
解耦抽象(Abstraction)与实现(Implementation),使两者可以独立变化。
在 Java 中实现 Bridge 模式时,抽象层通过持有实现层接口的引用而非具体类,从而将一个本可能不断膨胀的类继承树拆成两个相互独立的继承层次。这也解释了它的另一个别名——Handle/Body:抽象部分(Handle)只负责对外提供统一操作入口,具体行为(Body)由被组合进来的实现对象完成。
一句话理解
Bridge 模式的核心是优先使用组合而非继承:把实现细节从(抽象)继承体系中"推"到另一个独立的实现继承体系中。
Wikipedia 对它的定义与上述意图一致:桥接模式旨在"将抽象与实现解耦,使二者可以独立变化"。
现实世界类比:万能遥控器
一个非常经典的类比是万能遥控器:
- 遥控器(抽象)对外提供统一的按键操作——开机/关机、换台、调音量;
- 不同品牌、不同型号的电视机(实现)各自用完全不同的内部电路完成这些操作;
- 遥控器并不关心电视机内部如何工作,只需面向一个统一的接口发送指令。
这种设计带来的直接收益是:新增一个电视品牌时,无需修改遥控器的任何代码;同时也可以为同一批电视机开发不同形态的遥控器(语音版、触屏版等)。抽象与实现沿两个维度独立扩展,正是 Bridge 模式在现实世界中的投射。
二、仓库源码解读:Weapon 与 Enchantment 双层次结构
java-design-patterns 仓库的 bridge 模块位于 bridge/ 目录,包含 7 个主源码文件(bridge/src/main/java/com/iluwatar/bridge/)和 4 个测试文件(bridge/src/test/java/com/iluwatar/bridge/)。该示例构造了两个相互独立的类层次:武器层次(Weapon)与附魔层次(Enchantment)。
借助类图可以一目了然地看到两个层次的关系:
类图由 bridge/etc/bridge.urm.puml 生成,从中可以清晰地看到:
Weapon接口(抽象层次)下派生Sword、Hammer两个具体类;Enchantment接口(实现层次)下派生FlyingEnchantment、SoulEatingEnchantment两个具体类;Sword、Hammer通过-enchantment字段与Enchantment建立组合(composition)关联。
如果不用 Bridge 而全部用继承,要覆盖 2 种武器 × 2 种附魔的全部组合,就需要 4 个类(会飞行的剑、噬魂的剑、会飞行的锤、噬魂的锤);武器与附魔各新增一种,组合数就会暴涨为 N×M。而采用 Bridge 后,类数量只需要 N+M,这就是它抑制类爆炸的根本原因。
2.1 抽象层次:Weapon 接口及其实现
Weapon.java 定义了抽象层次的操作契约:
public interface Weapon {
void wield(); // 挥舞前握持武器
void swing(); // 挥砍
void unwield(); // 收手放下武器
Enchantment getEnchantment(); // 获取当前绑定的附魔
}
Sword.java 与 Hammer.java 是 Weapon 的两个具体实现。它们的关键点在于:构造器接收一个 Enchantment 实例并保存在私有字段中,所有对外操作都委托给这个字段完成。以 Sword 为例:
@Slf4j
@AllArgsConstructor
public class Sword implements Weapon {
private final Enchantment enchantment;
@Override
public void wield() {
LOGGER.info("The sword is wielded.");
enchantment.onActivate();
}
@Override
public void swing() {
LOGGER.info("The sword is swung.");
enchantment.apply();
}
@Override
public void unwield() {
LOGGER.info("The sword is unwielded.");
enchantment.onDeactivate();
}
@Override
public Enchantment getEnchantment() {
return enchantment;
}
}
注意源码中使用了 Lombok 注解 @AllArgsConstructor 自动生成全参构造器、@Slf4j 注入日志对象,实际手写代码时需自行实现。Hammer 的结构与 Sword 完全同构,仅日志文案不同("The hammer is wielded." 等),这里不再赘述。
2.2 实现层次:Enchantment 接口及其实现
Enchantment.java 是独立的实现层次接口:
public interface Enchantment {
void onActivate(); // 附魔被激活
void apply(); // 附魔效果生效
void onDeactivate(); // 附魔失效
}
FlyingEnchantment.java(飞行附魔)与 SoulEatingEnchantment.java(噬魂附魔)各自给出具体行为。以 FlyingEnchantment 为例:
@Slf4j
public class FlyingEnchantment implements Enchantment {
@Override
public void onActivate() {
LOGGER.info("The item begins to glow faintly.");
}
@Override
public void apply() {
LOGGER.info("The item flies and strikes the enemies finally returning to owner's hand.");
}
@Override
public void onDeactivate() {
LOGGER.info("The item's glow fades.");
}
}
SoulEatingEnchantment 则对应输出:"The item spreads bloodlust."(激活)、"The item eats the soul of enemies."(生效)、"Bloodlust slowly disappears."(失效)。
两个层次各自独立:武器不知道附魔的具体实现细节,附魔也不依赖任何特定武器。二者仅通过 Weapon 持有的 Enchantment 引用发生组合关系。
三、程序化示例:运行 App 观察 Bridge 效果
仓库的入口类 App.java 演示了如何自由组合两个层次:
@Slf4j
public class App {
public static void main(String[] args) {
LOGGER.info("The knight receives an enchanted sword.");
var enchantedSword = new Sword(new SoulEatingEnchantment());
enchantedSword.wield();
enchantedSword.swing();
enchantedSword.unwield();
LOGGER.info("The valkyrie receives an enchanted hammer.");
var hammer = new Hammer(new FlyingEnchantment());
hammer.wield();
hammer.swing();
hammer.unwield();
}
}
运行后控制台输出如下:
The knight receives an enchanted sword.
The sword is wielded.
The item spreads bloodlust.
The sword is swung.
The item eats the soul of enemies.
The sword is unwielded.
Bloodlust slowly disappears.
The valkyrie receives an enchanted hammer.
The hammer is wielded.
The item begins to glow faintly.
The hammer is swung.
The item flies and strikes the enemies finally returning to owner's hand.
The hammer is unwielded.
The item's glow fades.
从输出可以验证桥接的两层调用链:
Sword.wield()→ 打印"剑被握持" → 委托SoulEatingEnchantment.onActivate();Sword.swing()→ 打印"剑被挥砍" → 委托SoulEatingEnchantment.apply();Hammer.wield()等操作则委托给FlyingEnchantment对应方法。
关键点:同样的 wield/swing/unwield 调用,在不同武器 + 不同附魔组合下产生完全不同的行为,而 Sword/Hammer 的代码一行未改。 这正是"抽象与实现可独立变化"的直接体现。
如何运行
bridge 是 Maven 多模块项目 java-design-patterns(父工程版本 1.26.0-SNAPSHOT)下的一个独立模块(见 bridge/pom.xml)。在仓库根目录执行:
./mvnw -pl bridge compile # 仅编译 bridge 模块
./mvnw -pl bridge test # 运行模块测试
./mvnw -pl bridge exec:java -Dexec.mainClass=com.iluwatar.bridge.App # 运行示例(需相应插件配置)
该模块的运行时依赖为 slf4j-api + logback-classic,测试依赖为 JUnit 5(junit-jupiter-engine)与 Mockito(mockito-core),并在 bridge/pom.xml 中通过 maven-assembly-plugin 将主类 com.iluwatar.bridge.App 声明为可执行清单入口。
四、测试验证:组合桥接的可靠性
仓库用测试证实了"武器的每个动作都会正确委托给所组合的附魔实现"这一核心行为。
WeaponTest.java 是所有武器测试的抽象基类,它用 Mockito 对附魔实现做 mock 校验:
abstract class WeaponTest {
final void testBasicWeaponActions(final Weapon weapon) {
assertNotNull(weapon);
var enchantment = weapon.getEnchantment();
assertNotNull(enchantment);
assertNotNull(weapon.getEnchantment());
weapon.swing();
verify(enchantment).apply();
verifyNoMoreInteractions(enchantment);
weapon.wield();
verify(enchantment).onActivate();
verifyNoMoreInteractions(enchantment);
weapon.unwield();
verify(enchantment).onDeactivate();
verifyNoMoreInteractions(enchantment);
}
}
SwordTest.java 与 HammerTest.java 分别用 spy(new Sword(mock(FlyingEnchantment.class))) 和 spy(new Hammer(mock(FlyingEnchantment.class))) 构造被测对象,并断言:swing() 必须触发 enchantment.apply(),wield() 必须触发 onActivate(),unwield() 必须触发 onDeactivate(),且没有多余的交互。这从测试层面锁定了桥接委托关系的正确性。
AppTest.java 则验证主程序可无异常执行:
@Test
void shouldExecuteApplicationWithoutException() {
assertDoesNotThrow(() -> App.main(new String[] {}));
}
五、何时使用 Bridge 模式
根据 GoF 及仓库文档的归纳,当出现以下情况时应考虑引入 Bridge 模式:
| 触发条件 | 说明 |
|---|---|
| 需要避免抽象与实现的永久绑定 | 实现需要在运行时被选择或切换(例如根据平台、配置动态决定) |
| 抽象与实现都需要可扩展 | 二者都应能通过子类化独立扩展,互不牵制 |
| 实现变化不应影响客户端 | 更换实现时,客户端代码无需重新编译 |
| 层次中出现大量类 | 类数量急剧膨胀,说明需要把一个对象拆成两部分——Rumbaugh 称之为 "nested generalizations"(嵌套泛化) |
| 希望在多个对象间共享实现 | 用引用计数等方式共享同一实现,且对客户端隐藏细节——例如 Coplien 的 String 类中多个对象共享同一字符串表示 |
典型应用场景
仓库文档列举了 Bridge 在工业界的经典应用:
- GUI 框架:抽象是"窗口",实现是底层操作系统的窗口系统(Windows、macOS、Linux 各自实现),框架一次编写,跨平台运行;
- 数据库驱动:抽象是通用的数据库访问接口(JDBC),实现是各家数据库厂商的专属驱动;
- 设备驱动:抽象是与设备无关的代码,实现是与设备相关的代码,操作系统得以适配千差万别的硬件。
与继承方案的对比
以本仓库示例来说:
- 继承方案:
Sword、Hammer各派生"飞行版/噬魂版"子类,组合数随维度增长爆炸,代码大量重复; - Bridge 方案:武器与附魔各自独立演进,新增一种附魔只需新增一个
Enchantment实现类,所有武器自动支持,反之亦然。
六、优势与权衡
优势
- 接口与实现解耦:高层操作(抽象)与底层操作(实现)分离,系统模块化程度更高;
- 可扩展性提升:抽象层次与实现层次可以各自独立扩展,互不干扰;
- 隐藏实现细节:客户端只看到抽象层的接口,完全感知不到底层实现的具体形态。
权衡与代价
- 增加复杂度:系统结构与代码多出一个层次,对不熟悉该模式的开发者而言理解成本上升;
- 运行时开销:多了一层抽象间接调用,理论上引入轻微性能损耗,但在绝大多数实际场景中可以忽略。
七、与其他设计模式的关系
| 模式 | 与 Bridge 的关系 |
|---|---|
| Abstract Factory | 可与 Bridge 搭配使用:由抽象工厂创建与具体类无关的平台对象,配合 Bridge 实现平台无关的系统 |
| Adapter | Adapter 是给已有对象换一个接口;Bridge 是把对象的接口与实现分离,意图不同 |
| Composite | Bridge 常与 Composite 配合,用于建模组件的实现细节 |
| Strategy | 二者都基于组合,但意图不同:Strategy 用组合改变类的行为,Bridge 用组合分离抽象的接口与实现 |
区别速记:Adapter 解决"接口不匹配",Strategy 解决"行为可替换",Bridge 解决"抽象与实现双维度独立演进"。 三者都以组合为核心,但服务的目标截然不同。
八、小结
本仓库的 bridge 模块用"武器 × 附魔"这一直观示例,完整演示了 Bridge 模式的标准落地姿势:
- 抽象层定义统一操作契约(
Weapon); - 实现层定义独立的另一套契约(
Enchantment); - 抽象层通过构造器注入持有实现层引用,所有操作委托给该引用;
- 两个层次各自增删具体类,均不影响对方与客户端。
在 java-design-patterns 仓库中,本模块还配有类图(bridge/etc/bridge.urm.puml → bridge/etc/bridge.urm.png)、时序图(bridge/etc/bridge-sequence-diagram.png)与完整的单元测试(bridge/src/test/java/com/iluwatar/bridge/),是学习与验证 Bridge 模式的最佳参考实现。当你发现类层次正随组合维度爆炸式增长时,请记住:组合优于继承,Bridge 就是这句话最经典的结构型实践。

