文章目录
- 一、概述
-
- [1.1 结构与角色](#1.1 结构与角色)
- [1.2 适用场景](#1.2 适用场景)
- 二、实现方式
-
- [2.1 基础实现](#2.1 基础实现)
- [2.2 扩展维度演示](#2.2 扩展维度演示)
- [2.3 实际应用示例------消息发送系统](#2.3 实际应用示例——消息发送系统)
- [2.4 类爆炸对比](#2.4 类爆炸对比)
- [三、桥接模式 vs 适配器模式](#三、桥接模式 vs 适配器模式)
-
- [3.1 意图对比](#3.1 意图对比)
- [3.2 结构对比](#3.2 结构对比)
- [3.3 代码对比](#3.3 代码对比)
- [3.4 选择指南](#3.4 选择指南)
- 四、总结
一、概述
在软件开发中,经常会遇到这样的场景:一个类存在两个独立变化的维度 ,比如一个"图形"类既包含形状(圆形、矩形、三角形),又包含颜色(红色、绿色、蓝色)。如果采用传统的多层继承方式,每增加一种形状或颜色,就需要创建一个新的子类,导致类的数量呈指数级增长:
Shape
RedCircle
GreenCircle
BlueCircle
RedRectangle
GreenRectangle
BlueRectangle
6 个子类只是 2 种形状 × 3 种颜色的组合。如果再增加"三角形"和"黄色",子类数量将激增到 12 个。这就是所谓的类爆炸问题。
桥接模式(Bridge Pattern)正是为了解决这个问题而诞生的------它将抽象部分 与实现部分分离,使它们可以独立变化,从而避免使用多层继承造成的类爆炸。
生活中的桥接例子:
- 手机与APP:手机品牌(华为、苹果、小米)和应用程序(微信、抖音、淘宝)是两个独立变化的维度,APP 不需要为每个手机品牌单独开发
- 电器与开关:电器(灯、风扇、电视)和开关(拉线开关、按钮开关、遥控开关)可以独立变化,通过桥接模式将两者解耦
- 日志与输出目标:日志格式(简单格式、详细格式)和输出目标(文件、数据库、控制台)可以独立扩展
核心:将抽象部分与实现部分分离,使它们都可以独立变化
1.1 结构与角色
桥接模式包含以下角色:
持有
继承
实现
实现
委托调用
Client 客户端
Abstraction 抽象类
Implementor 实现接口
RefinedAbstraction 扩展抽象类
ConcreteImplementorA 具体实现A
ConcreteImplementorB 具体实现B
- Abstraction(抽象类):定义抽象部分的接口,并持有对实现接口的引用,通过组合关系桥接到实现部分
- RefinedAbstraction(扩展抽象类):继承抽象类,扩展抽象部分的接口
- Implementor(实现接口):定义实现部分的接口,不一定要与抽象部分的接口完全一致
- ConcreteImplementor(具体实现):实现实现接口的具体类
- Client(客户端):通过抽象类与对象交互
1.2 适用场景
- 一个类存在两个独立变化的维度,且两个维度都需要扩展
- 不希望使用多层继承导致类爆炸的场景
- 需要在抽象化和具体化之间增加灵活性,避免两个层次之间的永久绑定
- 想要对客户程序隐藏实现部分的细节
二、实现方式
桥接模式的核心实现思路是:将抽象部分 (如形状)和实现部分(如颜色)分别定义为独立的继承体系,再通过组合关系将两者桥接起来。
2.1 基础实现
以"图形与颜色"为例,形状为抽象部分,颜色为实现部分:
持有
继承
继承
实现
实现
实现
委托调用
客户端
Shape 抽象类
Color 实现接口
Circle 圆形
Rectangle 矩形
Red 红色
Green 绿色
Blue 蓝色
(1)实现接口------颜色
java
/**
* 实现接口:颜色
*/
public interface Color {
/**
* 填充颜色
*/
String fill();
}
(2)具体实现------红色、绿色、蓝色
java
/**
* 具体实现:红色
*/
public class Red implements Color {
@Override
public String fill() {
return "红色";
}
}
/**
* 具体实现:绿色
*/
public class Green implements Color {
@Override
public String fill() {
return "绿色";
}
}
/**
* 具体实现:蓝色
*/
public class Blue implements Color {
@Override
public String fill() {
return "蓝色";
}
}
(3)抽象类------图形
java
/**
* 抽象类:图形
* 持有 Color 实现接口的引用,将颜色操作委托给 Color 对象
*/
public abstract class Shape {
protected Color color;
public Shape(Color color) {
this.color = color;
}
/**
* 绘制图形
*/
public abstract void draw();
}
(4)扩展抽象类------圆形、矩形
java
/**
* 扩展抽象类:圆形
*/
public class Circle extends Shape {
public Circle(Color color) {
super(color);
}
@Override
public void draw() {
System.out.println("绘制" + color.fill() + "圆形");
}
}
/**
* 扩展抽象类:矩形
*/
public class Rectangle extends Shape {
public Rectangle(Color color) {
super(color);
}
@Override
public void draw() {
System.out.println("绘制" + color.fill() + "矩形");
}
}
(5)客户端调用
java
public class BridgeDemo {
public static void main(String[] args) {
// 红色圆形
Shape redCircle = new Circle(new Red());
redCircle.draw();
// 绘制红色圆形
// 绿色矩形
Shape greenRectangle = new Rectangle(new Green());
greenRectangle.draw();
// 绘制绿色矩形
// 蓝色圆形
Shape blueCircle = new Circle(new Blue());
blueCircle.draw();
// 绘制蓝色圆形
}
}
关键点 :抽象类
Shape通过组合持有Color接口的引用,将颜色操作委托给Color对象。形状和颜色可以独立扩展------新增形状不需要修改颜色类,新增颜色也不需要修改形状类。
2.2 扩展维度演示
桥接模式的最大优势在于两个维度可以独立扩展,下面演示如何新增一个维度:
(1)新增颜色------黄色
java
/**
* 具体实现:黄色
* 新增颜色,无需修改任何已有的形状类
*/
public class Yellow implements Color {
@Override
public String fill() {
return "黄色";
}
}
(2)新增形状------三角形
java
/**
* 扩展抽象类:三角形
* 新增形状,无需修改任何已有的颜色类
*/
public class Triangle extends Shape {
public Triangle(Color color) {
super(color);
}
@Override
public void draw() {
System.out.println("绘制" + color.fill() + "三角形");
}
}
(3)自由组合
java
public class BridgeExtendDemo {
public static void main(String[] args) {
// 黄色三角形------新形状 + 新颜色
Shape yellowTriangle = new Triangle(new Yellow());
yellowTriangle.draw();
// 绘制黄色三角形
// 黄色圆形------旧形状 + 新颜色
Shape yellowCircle = new Circle(new Yellow());
yellowCircle.draw();
// 绘制黄色圆形
// 蓝色三角形------新形状 + 旧颜色
Shape blueTriangle = new Triangle(new Blue());
blueTriangle.draw();
// 绘制蓝色三角形
}
}
对比:如果使用多层继承,3 种形状 × 4 种颜色 = 12 个子类;而使用桥接模式,只需要 3 个形状类 + 4 个颜色类 = 7 个类。每新增一种形状只需 1 个类,每新增一种颜色也只需 1 个类。
2.3 实际应用示例------消息发送系统
以一个消息发送系统为例,消息类型(普通消息、加急消息、特急消息)和发送方式(邮件发送、短信发送、微信发送)是两个独立变化的维度:
持有
继承
继承
实现
实现
实现
委托调用
客户端
Message 抽象消息类
MessageSender 实现接口
CommonMessage 普通消息
UrgencyMessage 加急消息
EmailSender 邮件发送
SmsSender 短信发送
WechatSender 微信发送
(1)实现接口------消息发送器
java
/**
* 实现接口:消息发送器
*/
public interface MessageSender {
/**
* 发送消息
*
* @param message 消息内容
*/
void send(String message);
}
(2)具体实现------邮件、短信、微信发送器
java
/**
* 具体实现:邮件发送器
*/
public class EmailSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("【邮件】发送消息:" + message);
}
}
/**
* 具体实现:短信发送器
*/
public class SmsSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("【短信】发送消息:" + message);
}
}
/**
* 具体实现:微信发送器
*/
public class WechatSender implements MessageSender {
@Override
public void send(String message) {
System.out.println("【微信】发送消息:" + message);
}
}
(3)抽象类------消息
java
/**
* 抽象类:消息
* 持有 MessageSender 实现接口的引用
*/
public abstract class Message {
protected MessageSender sender;
public Message(MessageSender sender) {
this.sender = sender;
}
/**
* 发送消息
*
* @param message 消息内容
*/
public abstract void sendMessage(String message);
}
(4)扩展抽象类------普通消息、加急消息
java
/**
* 扩展抽象类:普通消息
*/
public class CommonMessage extends Message {
public CommonMessage(MessageSender sender) {
super(sender);
}
@Override
public void sendMessage(String message) {
sender.send("【普通】" + message);
}
}
/**
* 扩展抽象类:加急消息
*/
public class UrgencyMessage extends Message {
public UrgencyMessage(MessageSender sender) {
super(sender);
}
@Override
public void sendMessage(String message) {
sender.send("【加急】" + message);
}
}
(5)客户端调用
java
public class MessageBridgeDemo {
public static void main(String[] args) {
// 普通消息 ------ 通过邮件发送
Message commonEmail = new CommonMessage(new EmailSender());
commonEmail.sendMessage("系统维护通知");
// 【邮件】发送消息:【普通】系统维护通知
// 加急消息 ------ 通过短信发送
Message urgencySms = new UrgencyMessage(new SmsSender());
urgencySms.sendMessage("服务器宕机告警");
// 【短信】发送消息:【加急】服务器宕机告警
// 加急消息 ------ 通过微信发送
Message urgencyWechat = new UrgencyMessage(new WechatSender());
urgencyWechat.sendMessage("数据库连接异常");
// 【微信】发送消息:【加急】数据库连接异常
}
}
优势体现 :如果未来需要新增"钉钉发送器",只需创建一个
DingTalkSender实现类,无需修改任何消息类;如果需要新增"特急消息",只需创建一个EmergencyMessage继承Message,无需修改任何发送器类。
2.4 类爆炸对比
以 3 种形状 × 4 种颜色为例:
| 方式 | 类的数量 | 新增一种形状 | 新增一种颜色 |
|---|---|---|---|
| 多层继承 | 12(3×4) | +4 个子类 | +3 个子类 |
| 桥接模式 | 7(3+4) | +1 个类 | +1 个类 |
维度越多、每个维度的种类越多,桥接模式的优势越明显。假设有 n 个维度,每个维度有 m 种变化,多层继承需要 (m^n) 个子类,而桥接模式只需要 (n \times m) 个类。
三、桥接模式 vs 适配器模式
桥接模式和适配器模式都属于结构型模式,而且都涉及"接口"和"组合",但它们的意图和使用场景截然不同。
关于适配器模式可参见博客:设计模式-适配器模式
3.1 意图对比
| 对比维度 | 桥接模式 | 适配器模式 |
|---|---|---|
| 核心意图 | 分离抽象与实现,使两者独立变化 | 将一个接口转换为另一个接口,使不兼容的类协同工作 |
| 解决的问题 | 类爆炸(多层继承导致的子类激增) | 接口不兼容 |
| 设计时机 | 设计阶段(预先规划) | 事后补救(已有代码不兼容) |
| 关系性质 | 抽象与实现之间是组合关系,两者独立变化 |
适配器与被适配者之间是包装关系,接口转换 |
| 变化维度 | 两个维度同时独立扩展 | 通常只关心接口的统一 |
3.2 结构对比
适配器模式
持有/继承
Target
Adapter
Adaptee
桥接模式
持有组合
Abstraction
Implementor
RefinedAbstraction
ConcreteImplementor
桥接模式:Abstraction 通过组合持有 Implementor 的引用,两者是平等的合作关系,可以独立扩展。
适配器模式:Adapter 包装 Adaptee,将 Adaptee 的接口转换为 Target 接口,Adaptee 是已有的、不可修改的。
3.3 代码对比
以"日志系统"为例,对比两种模式的应用方式:
桥接模式的用法------日志格式与输出目标独立变化:
java
// 实现接口:输出目标
public interface LogOutput {
void output(String message);
}
// 具体实现:文件输出
public class FileOutput implements LogOutput {
@Override
public void output(String message) {
System.out.println("写入文件:" + message);
}
}
// 抽象类:日志格式
public abstract class Logger {
protected LogOutput output;
public Logger(LogOutput output) {
this.output = output;
}
public abstract void log(String message);
}
// 扩展抽象:简单日志
public class SimpleLogger extends Logger {
public SimpleLogger(LogOutput output) {
super(output);
}
@Override
public void log(String message) {
output.output("[INFO] " + message);
}
}
// 客户端:自由组合
Logger logger = new SimpleLogger(new FileOutput());
logger.log("系统启动");
适配器模式的用法------已有日志框架接口不兼容:
java
// 目标接口:项目期望的日志接口
public interface AppLogger {
void info(String message);
}
// 被适配者:第三方日志框架(接口不兼容,无法修改)
public class ThirdPartyLogger {
public void writeLog(int level, String message) {
System.out.println("Level=" + level + " : " + message);
}
}
// 适配器:将第三方日志框架适配为项目接口
public class LoggerAdapter implements AppLogger {
private final ThirdPartyLogger thirdPartyLogger;
public LoggerAdapter(ThirdPartyLogger thirdPartyLogger) {
this.thirdPartyLogger = thirdPartyLogger;
}
@Override
public void info(String message) {
thirdPartyLogger.writeLog(1, message);
}
}
// 客户端:通过适配器使用第三方框架
AppLogger logger = new LoggerAdapter(new ThirdPartyLogger());
logger.info("系统启动");
关键区别:桥接模式是预先设计,将两个维度解耦以支持独立扩展;适配器模式是事后补救,将不兼容的接口转换为兼容的接口。
3.4 选择指南
| 场景 | 选择 |
|---|---|
| 一个类有两个独立变化的维度 | 桥接模式 |
| 需要在抽象与实现之间增加灵活性 | 桥接模式 |
| 已有类的接口与需求不匹配 | 适配器模式 |
| 需要复用第三方库或遗留代码 | 适配器模式 |
| 需要同时支持多种实现且可以切换 | 桥接模式 |
四、总结
桥接模式的核心思想是将抽象部分与实现部分分离,使它们都可以独立变化,从而避免多层继承造成的类爆炸问题。
优点:
- 分离抽象与实现:两个维度独立变化,互不影响
- 避免类爆炸:用组合代替继承,大幅减少子类数量
- 符合开闭原则:新增形状或颜色只需新增一个类,无需修改已有代码
- 符合合成复用原则:使用组合代替继承,降低耦合度
- 提高可扩展性:两个维度可以独立扩展,灵活组合
缺点:
- 增加系统复杂度:需要额外定义接口和抽象类,增加了类的数量
- 需要正确识别维度:必须准确识别系统中独立变化的维度,否则会增加理解难度
- 过度设计风险:如果两个维度不需要独立变化,使用桥接模式反而增加了不必要的复杂度
适用场景:
- 一个类存在两个独立变化的维度,且两个维度都需要扩展
- 不希望使用多层继承导致类数量激增
- 需要在抽象化和具体化之间增加灵活性
- 需要动态切换实现方式
桥接 vs 适配器 :桥接模式是
设计阶段的决策,意在将抽象与实现解耦以支持独立扩展;适配器模式是事后补救的措施,意在将不兼容的接口转换为兼容的接口。两者虽然都使用了组合,但意图截然不同。
参考博客:
桥接模式 | 菜鸟教程:https://www.runoob.com/design-pattern/bridge-pattern.html