GoF设计模式——桥接模式
本文是【GoF设计模式】系列第10篇
前言
为什么需要桥接模式?
假设要做一个图形编辑器,有圆形、矩形、三角形三种图形,每种图形又要支持红色、蓝色、黄色三种颜色。用继承来实现,就要写RedCircle、BlueCircle、RedRectangle、BlueRectangle… 类的数量 = 图形数 × 颜色数,3 × 3 = 9 个类。再加一种图形或一种颜色,类就会爆炸式增长。
// 继承方案:类爆炸classRedCircleextendsCircle{...}classBlueCircleextendsCircle{...}classRedRectangleextendsRectangle{...}// ... 9 个类,再加一个图形或颜色就要翻倍两种选择:要么忍受类爆炸,要么把颜色作为字段写在图形类里——后者看似简单,但每种颜色逻辑都要在图形类内部判断,代码会变得臃肿,加一种颜色就要改所有图形类。
这种"两个维度各自变化,组合后类爆炸"的矛盾,就是桥接模式要解决的问题。
概念
桥接模式(Bridge Pattern)是一种结构型设计模式,核心思想是将抽象与实现分离到两个独立的维度,使它们可以各自独立扩展。
桥接模式包含四个角色:
- Abstraction(抽象):定义抽象部分的接口,持有对实现层的引用(这就是"桥")。通常用抽象类,因为需要持有引用并提供默认逻辑。
- RefinedAbstraction(修正抽象):对抽象接口进行扩展,是抽象的具体变体。
- Implementor(实现):定义实现部分的接口,与抽象层的接口可以完全不同。
- ConcreteImplementor(具体实现):实现实现化接口的具体类。
Abstraction 内部持有 Implementor 的引用,这是"桥"的关键——通过组合而非继承连接两个维度。RefinedAbstraction 扩展抽象层,ConcreteImplementorA/B 实现实现层,两边各自独立发展。
可以把桥接模式理解为遥控器和电视的关系:遥控器是抽象层,电视是实现层。遥控器内部持有一个电视的引用,按下"开机"键时调用电视的开机方法。不同的遥控器(基础遥控器、万能遥控器)是修正抽象,不同品牌的电视(小米、索尼)是具体实现。这样遥控器和电视可以各自独立发展——新增一个电视品牌不需要改遥控器的代码,新增一种遥控器也不需要改电视的代码。
实现
桥接模式只有一种实现方式:抽象层持有实现层的引用,通过组合而非继承来关联两个维度。
基础实现
实现步骤:定义实现层接口,创建具体实现类;定义抽象层基类(抽象类),内部持有实现层引用;创建修正抽象类扩展抽象层。
// ===== 实现层接口 =====interfaceImplementor{publicvoidoperationImpl();}// ===== 具体实现A =====classConcreteImplementorAimplementsImplementor{publicvoidoperationImpl(){System.out.println("实现方式A");}}// ===== 具体实现B =====classConcreteImplementorBimplementsImplementor{publicvoidoperationImpl(){System.out.println("实现方式B");}}// ===== 抽象层基类(用抽象类,因为需要持有实现层引用) =====abstractclassAbstraction{protectedImplementorimpl;// 桥:持有实现层的引用publicAbstraction(Implementorimpl){this.impl=impl;}publicvoidoperation(){impl.operationImpl();// 委托给实现层}}// ===== 修正抽象 =====classRefinedAbstractionextendsAbstraction{publicRefinedAbstraction(Implementorimpl){super(impl);}publicvoidoperation(){// 可以在委托前后加自己的逻辑System.out.println("RefinedAbstraction 额外逻辑");super.operation();}}// ===== 客户端 =====publicclassClient{publicstaticvoidmain(String[]args){ImplementorimplA=newConcreteImplementorA();Abstractionabs=newRefinedAbstraction(implA);abs.operation();}}引入一个例子:「一台万能遥控器,手里有小米电视和索尼电视两台设备。遥控器内部有个"电视插槽",插哪台电视就能控制哪台——遥控器本身不用改代码,电视也不用改接口」。
遥控器对应Abstraction(抽象层),电视对应Implementor(实现层),"插槽"对应构造方法注入的组合关系。换一台电视只需把新实例传给遥控器,两边各自独立。
// 实现层:电视品牌interfaceTV{publicvoidpowerOn();publicvoidpowerOff();}classXiaomiTVimplementsTV{publicvoidpowerOn(){System.out.println("小米电视开机");}publicvoidpowerOff(){System.out.println("小米电视关机");}}classSonyTVimplementsTV{publicvoidpowerOn(){System.out.println("索尼电视开机");}publicvoidpowerOff(){System.out.println("索尼电视关机");}}// 抽象层:遥控器abstractclassRemoteControl{protectedTVtv;// 持有电视的引用(桥)publicRemoteControl(TVtv){this.tv=tv;}publicvoidturnOn(){tv.powerOn();}publicvoidturnOff(){tv.powerOff();}}// 修正抽象:万能遥控器(额外功能)classUniversalRemoteextendsRemoteControl{publicUniversalRemote(TVtv){super(tv);}publicvoidmute(){System.out.println("静音");}}// 使用:遥控器和电视自由组合RemoteControlbasic=newRemoteControl(newXiaomiTV());basic.turnOn();// 控制小米电视UniversalRemoteuniversal=newUniversalRemote(newSonyTV());universal.turnOn();universal.mute();// 万能遥控器额外功能为什么 Abstraction 用抽象类而不是接口?桥接模式的抽象层需要:
- 持有实现层的引用:接口不能有实例字段,只有抽象类才能保存
Implementor实例 - 提供默认的委托逻辑:
operation()方法通常直接委托给impl,写在抽象类中避免子类重复代码
简单说:接口定义"能做什么",抽象类定义"怎么做框架"。桥接模式的抽象层需要既有状态(持有引用)又有行为(默认委托),所以用抽象类更合适。
桥接模式与简单组合的区别
桥接模式本质上就是组合,但它和普通的"一个类持有另一个类"有本质区别:
| 维度 | 简单组合 | 桥接模式 |
|---|---|---|
| 目的 | 复用功能、委托调用 | 分离两个独立变化的维度 |
| 结构 | 无约束,随意组合 | 抽象层 + 实现层 + 桥(持有引用) |
| 扩展性 | 通常只有一方可扩展 | 两边各自独立扩展 |
举个例子:
// 简单组合:订单持有支付方式的引用classOrder{privatePaymentMethodpayment;// 组合:只是委托publicvoidcheckout(){payment.pay(amount);}}订单和支付方式之间没有"两个维度各自变化"的关系——订单只有一个维度,支付方式也只有一个维度。目的只是"委托支付",不是解决类爆炸。
而桥接模式的图形 + 渲染:
// 桥接:图形持有渲染器的引用abstractclassShape{protectedRendererrenderer;// 桥:维度分离publicvoiddraw(){renderer.render(this);}}图形是一个维度(圆形、矩形…),渲染是另一个维度(矢量、像素…),两边各自独立扩展——新增一种图形不影响渲染层,新增一种渲染不影响图形层。
判断标准:新增一个实现类,抽象层是否需要新增对应的子类?
- 简单组合:新增
ApplePay,订单类不动——一方扩展,另一方不动 - 桥接模式:新增渲染方式,图形类不动;新增图形,渲染类不动——两边都可以独立扩展
记忆口诀:组合是手段,桥接是结构。组合解决"复用",桥接解决"维度分离"。
总结
桥接模式本质上是用组合替代继承来解决"两个维度各自变化"的问题——把乘法关系的类数量变成加法关系。
什么时候用:
- 一个类有两个独立变化的维度(如图形和颜色、支付方式和优惠策略)
- 每个维度都可能独立扩展,不希望类数量爆炸
- 需要在运行时动态切换实现层(如换一个电视品牌)
什么时候不用:
- 只有一个变化维度,用继承或简单组合就够了
- 两个维度耦合紧密,分开反而增加理解成本
- 系统简单,过早使用桥接模式会增加复杂度
简单记忆:
桥接拆维度,组合替继承。两个方向各自变,类数从乘变加法。
| 模式 | 接口关系 | 核心意图 | 典型场景 |
|---|---|---|---|
| 桥接 | 抽象持有实现引用 | 分离两个独立变化的维度 | 图形+渲染、消息+渠道 |
| 适配器 | 目标接口 ≠ 被包装对象接口 | 转换接口,让不兼容的类协同 | 第三方库集成 |
| 策略 | 上下文持有策略引用 | 替换同一维度的不同算法 | 排序算法、折扣计算 |
| 抽象工厂 | 工厂接口定义创建方法 | 创建一系列相关对象 | 跨数据库 DAO |
口诀对比:桥接拆维度,适配改接口,策略换算法,工厂管创建。
桥接 vs 适配器
| 维度 | 桥接模式 | 适配器模式 |
|---|---|---|
| 核心意图 | 事前设计,分离两个独立变化的维度 | 事后补救,让不兼容的接口协同工作 |
| 结构差异 | 抽象层持有实现层引用,双方接口可不同 | 适配器实现目标接口,持有被适配对象 |
| 关注点 | 预防类爆炸,支持独立扩展 | 解决接口不兼容问题 |
| 典型场景 | JDBC 驱动、跨平台 UI | 第三方库集成、遗留系统对接 |
逐步区分法:
- 如果两个维度都可能独立扩展 → 选桥接
- 如果只是要让现有类协同工作 → 选适配器
- 如果需要事后补救接口不兼容 → 选适配器
桥接 vs 策略
| 维度 | 桥接模式 | 策略模式 |
|---|---|---|
| 核心意图 | 分离两个独立变化的维度 | 替换同一维度的不同算法 |
| 结构差异 | 抽象层持有实现层引用,两者是平行的维度 | 上下文持有策略引用,策略是算法的变体 |
| 关注点 | 维度分离,独立扩展 | 算法替换,行为可变 |
| 典型场景 | 图形+渲染、消息+渠道 | 排序算法、折扣计算 |
逐步区分法:
- 如果有两个独立变化的维度 → 选桥接
- 如果只是同一个行为的不同实现方式 → 选策略
- 如果关注点是"用什么算法" → 选策略
- 如果关注点是"用什么实现体系" → 选桥接
桥接 vs 抽象工厂
| 维度 | 桥接模式 | 抽象工厂模式 |
|---|---|---|
| 核心意图 | 分离两个独立变化的维度 | 创建一系列相关对象 |
| 结构差异 | 抽象层持有实现层引用 | 工厂接口定义创建方法 |
| 关注点 | 结构分离,运行时切换 | 对象创建,产品族约束 |
| 典型场景 | JDBC 驱动、跨平台 UI | 跨数据库 DAO、跨 UI 主题 |
逐步区分法:
- 如果需要在运行时切换实现 → 选桥接
- 如果只需要创建一系列相关对象 → 选抽象工厂
- 如果关注点是"对象怎么创建" → 选抽象工厂
- 如果关注点是"实现怎么切换" → 选桥接
练习题目
图形渲染系统
题目描述:一个图形系统需要支持多种图形和多种渲染方式。图形包括圆形(Circle)和矩形(Rectangle),渲染方式包括矢量渲染(Vector)和像素渲染(Pixel)。请使用桥接模式实现,使得图形类型和渲染方式可以独立扩展。
输入描述:第一行是一个整数 N(1 ≤ N ≤ 100),表示后面有 N 行输入。接下来的 N 行,每行包含两个字符串,第一个表示图形类型(Circle / Rectangle),第二个表示渲染方式(Vector / Pixel)。
输出描述:对于每行输入,输出该图形使用该渲染方式的绘制结果。
输入示例:
6 Circle Vector Circle Pixel Rectangle Vector Rectangle Pixel Circle Vector Rectangle Pixel输出示例:
Drawing Circle with Vector Renderer Drawing Circle with Pixel Renderer Drawing Rectangle with Vector Renderer Drawing Rectangle with Pixel Renderer Drawing Circle with Vector Renderer Drawing Rectangle with Pixel Renderer解题思路:如果不使用桥接模式,就要为每种组合创建类(VectorCircle、PixelCircle、VectorRectangle、PixelRectangle),类数量 = 图形数 × 渲染数。使用桥接模式,渲染方式是实现层(Renderer接口),图形是抽象层(Shape抽象类持有Renderer引用),通过构造方法注入。这样图形和渲染各自独立扩展,类数量从乘法变成加法。
importjava.util.*;publicclassMain{publicstaticvoidmain(String[]args){Scannersc=newScanner(System.in);intn=sc.nextInt();while(n-->0){StringshapeType=sc.next();StringrendererType=sc.next();// 实现层:渲染方式Rendererr=null;if("Vector".equals(rendererType)){r=newVector();}else{r=newPixel();}// 抽象层:图形(持有渲染器引用)Shapes=null;if("Circle".equals(shapeType)){s=newCircle(r);}else{s=newRectangle(r);}s.draw();}}}// 实现层接口:渲染器interfaceRenderer{publicStringgetName();}classVectorimplementsRenderer{publicStringgetName(){return"Vector Renderer";}}classPixelimplementsRenderer{publicStringgetName(){return"Pixel Renderer";}}// 抽象层:图形(持有渲染器引用)abstractclassShape{protectedRendererrenderer;protectedStringname;publicShape(Stringname,Rendererrenderer){this.name=name;this.renderer=renderer;}publicvoiddraw(){System.out.println("Drawing "+name+" with "+renderer.getName());}}classCircleextendsShape{publicCircle(Rendererr){super("Circle",r);}}classRectangleextendsShape{publicRectangle(Rendererr){super("Rectangle",r);}}扩展:实际项目中的桥接模式
JDBC 驱动架构
JDBC 是桥接模式最经典的应用。Connection、Statement等接口是抽象层,各数据库厂商的驱动(MySQL Connector、PostgreSQL Driver)是实现层。应用代码只依赖 JDBC 接口,不关心底层是哪个数据库——MySQL 升级驱动版本不影响业务代码,切换数据库只需换驱动和连接字符串。
消息通知系统
通知系统需要支持多种消息类型(普通、紧急、营销)和多种发送渠道(短信、邮件、微信)。用桥接模式,消息类型是抽象层,发送渠道是实现层。新增一种消息类型只需加一个Message子类,新增一种发送渠道只需加一个MessageSender实现,两者互不影响。
日志框架的 Appender
SLF4J + Logback 中,Logger 是抽象层,Appender(输出目标)是实现层。一个 Logger 可以配置多个 Appender,日志同时输出到控制台、文件、远程服务。新增异步 Logger 不影响 Appender,新增 Kafka Appender 不影响 Logger。
跨平台 UI 组件
UI 框架中,按钮是一个维度,操作系统是另一个维度。同一种按钮在不同操作系统上的渲染方式不同。用桥接模式,UI 组件是抽象层,操作系统渲染是实现层。新增下拉框不影响操作系统层,新增 Linux 支持不影响 UI 组件层。
支付方式与优惠策略
电商系统中,支付方式(支付宝、微信)是一个维度,优惠策略(无优惠、满减、折扣)是另一个维度。用桥接模式,订单是抽象层,支付方式是实现层。新增 Apple Pay 不影响优惠策略,新增"首单立减"不影响支付方式。
现在可能还在写简单的业务代码,但等到遇到"两个维度都要扩展"的场景时——与其让类数量爆炸,不如用桥接模式把它们拆开,各自独立发展。那时候就真的懂了。
