Java 设计模式之桥接模式(Bridge Pattern):以 java-design-patterns 仓库的武器-附魔示例掌握抽象与实现解耦

原创2026-10-01 10:38:081,431 阅读
文章标签:示例工程后端

Java 设计模式之桥接模式(Bridge Pattern):以 java-design-patterns 仓库的武器-附魔示例掌握抽象与实现解耦

桥接模式(Bridge Pattern)是 Gang of Four 定义的结构型设计模式,核心思想是"将抽象与其实现解耦,使二者可以独立变化"。本文以 java-design-patterns 仓库中 bridge 模块 的"武器-附魔"完整示例为主线,结合 法语文档 的原始说明与仓库源码、测试,讲解该模式的意图、类层次结构、程序化实现、适用场景与收益权衡,读完即可在 Java 项目中独立落地这一模式。

模式概览:又称 Handle/Body(句柄/主体)

桥接模式在经典著作《设计模式:可复用面向对象软件的基础》中亦被称为 Handle/Body。法语文档对其意图的表述是:

Découpler une abstraction de son implémentation afin que les deux puissent varier indépendamment.

即:将抽象与其实现解耦,使得两者可以独立地变化。在 App.java 的类注释中,仓库作者给出了同样的解读:

Composition over inheritance. With Bridge, you can decouple an abstraction from its implementation so that the two can vary independently.

也就是说,桥接模式本质上是用对象组合(composition)取代继承(inheritance):实现细节从一条类层次中抽离出来,下沉到另一条拥有独立层次的类层次中,让客户端只依赖抽象接口。

现实类比:武器与附魔的自由组合

仓库文档使用了一个直观的例子来引入问题场景:

假设你有一件武器,它可以附加不同的附魔效果,并且你需要允许"任意武器 × 任意附魔"的自由搭配。你会怎么做?是为每种附魔组合复制多份武器类,还是单独创建附魔对象、按需装配到武器上?桥接模式正是帮你选择后者。

如果不使用桥接模式,采用"继承 + 穷举组合"的做法,就需要为 Sword、Hammer 各自派生 FlyingSword、SoulEatingSword、FlyingHammer、SoulEatingHammer 等类,随着武器种类(N)与附魔种类(M)的增多,类数量会膨胀为 N×M,形成 Rumbaugh 所称的"嵌套泛化(nested generalizations)"式爆炸性层次。桥接模式把两套维度拆成两条独立的类层次,类数量从 N×M 降为 N+M,这正是模式的价值所在。

两条独立类层次:Weapon 与 Enchantment

在仓库的 bridge/src/main/java/com/iluwatar/bridge 目录下,示例代码由两组接口与实现构成,分别对应"抽象"与"实现"两个维度。

抽象维度:Weapon 层次

抽象接口 Weapon.java 定义了一把武器对外暴露的三个操作,并返回其装配的附魔对象:

public interface Weapon {
  void wield();
  void swing();
  void unwield();
  Enchantment getEnchantment();
}

getEnchantment() 返回值类型是 Enchantment,这就在接口层面建立了武器(抽象)与附魔(实现)之间的"桥"。

抽象层次下有两个具体实现:Sword(剑)与 Hammer(锤),它们都通过构造器注入一个 Enchantment 实例,并在每个操作中委托给附魔对象。以 Sword.java 为例:

@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;
  }
}

Hammer.java 的结构与 Sword 完全一致,仅日志文案不同("The hammer is wielded/swung/unwielded")。注意这里的两个细节:

  • @AllArgsConstructor(Lombok 注解)生成了带 Enchantment 参数的构造器,这是桥接模式"组合优先于继承"的落点——武器不继承附魔的行为,而是持有附魔对象;
  • 每个武器操作都同步触发对应的附魔生命周期回调(onActivate/apply/onDeactivate),实现"武器操作 × 附魔效果"的动态串联。

实现维度:Enchantment 层次

另一条独立层次是附魔接口 Enchantment.java,它定义了附魔生命周期的三个方法:

public interface Enchantment {
  void onActivate();
  void apply();
  void onDeactivate();
}

实现类 FlyingEnchantment(飞行附魔)与 SoulEatingEnchantment(噬魂附魔)分别以独立日志输出其效果。以 FlyingEnchantment.java 为例:

@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.java 则分别输出 "The item spreads bloodlust."、"The item eats the soul of enemies."、"Bloodlust slowly disappears."。

从源码结构看,两条层次彼此完全不知晓对方的具体类:Weapon 只依赖 Enchantment 接口,Sword/Hammer 不关心注入的是哪种附魔;反过来 Enchantment 实现也不关心自己被装配到哪种武器上。这就是"两可独立变化"的保证——新增一种武器或一种附魔,都不需要改动对方层次中的任何代码。

运行时组合与程序输出

两条层次在运行时通过构造器完成自由组合。仓库的入口 App.java 演示了两种组合:骑士拿到"噬魂之剑",女武神拿到"飞行之锤":

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.

可以看到:武器的"挥动(swing)"始终触发附魔的 apply(),武器的"拔出(wield)/收起(unwield)"则分别触发 onActivate()/onDeactivate()。同样的武器骨架,由于注入了不同附魔,表现出完全不同的特效——这正是桥接模式"抽象与实现任意搭配"的直观证据。

类图与时序图:理解模式结构的关键

桥接模块在 bridge/etc 目录下提供了两张 PlantUML 生成的示意图,用于说明模式的结构与调用关系。

桥接模式类图:Weapon 与 Enchantment 两条独立类层次通过组合关联

桥接模式时序图:武器操作依次委托给附魔生命周期回调

类图清晰地展示了本文的核心结构:Weapon(抽象)与 Enchantment(实现)各成一条层次,Weapon 通过组合(带箭头的关联)持有 Enchantment,形成"桥"。时序图则进一步刻画了运行时消息流转:客户端调用 Sword/Hammer 的方法,武器内部再将调用转发给被注入的 Enchantment 实现。两张图对应源码 bridge/src/main/java/com/iluwatar/bridge 中的全部 6 个类/接口,可以在阅读源码时相互印证。

运行与验证方式

该模块是一个标准 Maven 工程,其 pom.xml 声明了模块名 bridge、父工程 java-design-patterns(版本 1.26.0-SNAPSHOT),并配置了日志依赖(slf4j-api、logback-classic)与测试依赖(junit-jupiter-engine、mockito-core)。其中 maven-assembly-plugin 将主类指定为 com.iluwatar.bridge.App,因此可以按如下方式运行:

# 在仓库根目录下执行(仓库提供 mvnw 包装器)
./mvnw -pl bridge exec:java -Dexec.mainClass=com.iluwatar.bridge.App
# 或先打包再用 assembly 插件生成可执行 jar 后运行
./mvnw -pl bridge package
java -jar bridge/target/bridge-*.jar

仓库为桥接模块提供了完整的单元测试,位于 bridge/src/test/java/com/iluwatar/bridge:

  • AppTest.java 通过 assertDoesNotThrow(() -> App.main(new String[] {})) 验证主程序可无异常执行;
  • WeaponTest、SwordTest、HammerTest 配合 Mockito 对武器与附魔的交互进行行为验证。

需要说明的是,./mvnw -pl bridge 命令要求先对仓库根 pom.xml 完成依赖解析(根工程聚合各子模块),且本仓库是只读的,运行验证仅在本地环境进行,不会修改仓库内容。

何时使用桥接模式

法语文档结合经典著作给出了五条明确的使用信号,可作为决策清单:

  1. 希望避免抽象与其实现之间的永久绑定——典型场景是实现需要在运行时被选择或切换(例如运行时动态装配不同的附魔);
  2. 抽象和实现都需要能通过子类独立扩展——桥接模式允许分别扩展两条层次,并把任意抽象与任意实现组合;
  3. 抽象实现的变更不应影响客户端——客户端只依赖抽象接口,实现层改动后客户端代码无需重新编译;
  4. 出现类层次的爆炸性增长(prolifération de classes)——若类的数量随维度组合呈乘积式膨胀,说明应当把对象拆成两部分,即 Rumbaugh 所说的"嵌套泛化"问题;
  5. 希望多个对象共享同一份实现——例如通过引用计数共享实现,同时对外隐藏这一事实(Coplien 的 String 类中多个对象共享同一字符串表示即为经典例子)。

收益与代价

从模式设计角度,桥接模式带来三方面收益:

  • 接口与实现解耦:将高层操作(接口)与低层操作(实现)分离,提升模块化程度,客户端只看到抽象接口而看不到实现细节;
  • 可扩展性提升:抽象层次与实现层次可以各自独立扩展,新增维度不再触碰既有代码;
  • 实现细节隐藏:客户端对实现的感知被隔离在桥的另一端。

同时也要清醒认识其代价:

  • 复杂度上升:多引入一条类层次与间接调用,对不熟悉该模式的开发者而言,系统结构更难一眼看透;
  • 运行时开销:多了一层抽象转发,会带来少量性能损耗,不过在实践中通常可以忽略不计。

与其他模式的关系

在 java-design-patterns 仓库的英文版 bridge/README.md 中,还整理了桥接模式与相邻模式的关系,有助于在真实项目中做模式选型:

  • 抽象工厂(Abstract Factory):可与桥接模式配合,用于创建与具体类无关的平台对象,见 abstract-factory;
  • 适配器(Adapter):Adapter 是为对象提供"不同的接口",而 Bridge 是"把对象的接口与实现分离",见 adapter;
  • 组合模式(Composite):Bridge 常与 Composite 搭配,用于建模组件的实现细节,见 composite;
  • 策略模式(Strategy):两者都基于组合,但意图不同——Strategy 用组合改变类的行为,Bridge 用组合分离抽象与实现,见 strategy。

小结

桥接模式是 Java 结构型设计模式中"组合优于继承"思想的最佳示范之一。通过本文中 java-design-patterns 仓库的武器-附魔示例,你可以完整掌握:两条独立类层次(Weapon 抽象层与 Enchantment 实现层)如何通过构造器注入建立桥梁;任意武器与任意附魔如何在运行时自由组合而不产生类爆炸;以及当抽象与实现需要独立演进、实现需在运行时切换、或类层次出现乘积式膨胀时,应当优先考虑桥接模式。进一步深入学习可阅读仓库英文版 bridge/README.md 及其类图源文件 bridge/etc/bridge.urm.puml,并结合源码与测试逐步验证。

登录后查看全文
java-design-patterns