桥接模式:抽象化与实现化分离
说实话,桥接模式是我在实际项目中用得比较多的一个设计模式。为什么?因为它解决了一个很现实的问题——当你的系统需要沿着多个维度变化时,怎么避免类爆炸。
我记得有一次接手一个跨平台图形绘制框架,刚开始设计时只考虑了Windows和Linux两个平台,画圆形、画矩形,一共4个类,挺清爽的。结果产品经理过来说:下周要支持macOS,还要支持画三角形、画五边形。我一算,3个平台×4种图形=12个类。如果再增加一个平台或者一种图形,类数量就成倍增长。这显然不是好办法。
桥接模式就是用来解决这个问题的。它的核心思想很简单:把抽象部分和实现部分拆开,让它们可以独立变化。
什么是桥接模式
桥接模式的定义是:将抽象部分与它的实现部分分离,使它们都可以独立地变化。说白了,就是用组合关系代替继承关系。
你想想看,如果我用继承来实现「不同平台画不同图形」,那每个图形都要为每个平台写一个子类。但用桥接模式,我把「图形」和「绘制方式」拆成两个独立的类层次,中间用组合搭个桥。
核心要点:
- 抽象化角色:定义抽象接口,持有实现化角色的引用
- 修正抽象化角色:扩展抽象化角色
- 实现化角色:定义实现接口
- 具体实现化角色:实现实现化接口
为什么需要桥接模式
我遇到过不少团队,一开始图省事,直接用继承堆功能。结果呢?
- 类数量爆炸:M种抽象 × N种实现 = M×N个类
- 修改困难:改一个抽象,所有实现都得跟着改
- 复用性差:实现代码和抽象代码绑死在一起
桥接模式把「M×N」变成了「M+N」。抽象和实现各自独立扩展,互不干扰。
案例一:跨平台图形绘制
先看一个图形绘制的例子。假设我们要画圆形和矩形,支持Windows和Linux两种绘制方式。
用桥接模式,我们先定义「绘制实现」的接口:
// 实现化角色:绘制接口
interface DrawAPI {
void drawCircle(int x, int y, int radius);
void drawRectangle(int x, int y, int width, int height);
}
// 具体实现化角色:Windows绘制
class WindowsDrawAPI implements DrawAPI {
@Override
public void drawCircle(int x, int y, int radius) {
System.out.println("Windows绘制圆形,圆心(" + x + "," + y + "),半径" + radius);
}
@Override
public void drawRectangle(int x, int y, int width, int height) {
System.out.println("Windows绘制矩形,左上角(" + x + "," + y + "),宽" + width + ",高" + height);
}
}
// 具体实现化角色:Linux绘制
class LinuxDrawAPI implements DrawAPI {
@Override
public void drawCircle(int x, int y, int radius) {
System.out.println("Linux绘制圆形,圆心(" + x + "," + y + "),半径" + radius);
}
@Override
public void drawRectangle(int x, int y, int width, int height) {
System.out.println("Linux绘制矩形,左上角(" + x + "," + y + "),宽" + width + ",高" + height);
}
}
然后定义「图形」的抽象类,它持有DrawAPI的引用:
// 抽象化角色:图形
abstract class Shape {
protected DrawAPI drawAPI;
protected Shape(DrawAPI drawAPI) {
this.drawAPI = drawAPI;
}
public abstract void draw();
}
// 修正抽象化角色:圆形
class Circle extends Shape {
private int x, y, radius;
public Circle(int x, int y, int radius, DrawAPI drawAPI) {
super(drawAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
drawAPI.drawCircle(x, y, radius);
}
}
// 修正抽象化角色:矩形
class Rectangle extends Shape {
private int x, y, width, height;
public Rectangle(int x, int y, int width, int height, DrawAPI drawAPI) {
super(drawAPI);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Override
public void draw() {
drawAPI.drawRectangle(x, y, width, height);
}
}
客户端使用起来非常灵活:
public class Client {
public static void main(String[] args) {
Shape circle = new Circle(10, 20, 5, new WindowsDrawAPI());
circle.draw(); // Windows绘制圆形
Shape rectangle = new Rectangle(30, 40, 100, 50, new LinuxDrawAPI());
rectangle.draw(); // Linux绘制矩形
// 新增macOS支持?只需要加一个MacDrawAPI,不用改任何图形类
Shape circleOnMac = new Circle(15, 25, 8, new MacDrawAPI());
circleOnMac.draw();
}
}
我的经验:桥接模式特别适合「抽象维度」和「实现维度」都可能变化的情况。比如图形绘制中,「图形种类」是一个维度,「绘制平台」是另一个维度。两个维度独立扩展,互不耦合。
案例二:消息发送系统
再看一个消息发送的例子。假设系统需要发送「普通消息」「紧急消息」「加急消息」,发送方式支持「邮件」「短信」「微信」。
用桥接模式,实现化角色是「消息发送方式」:
// 实现化角色:消息发送方式
interface MessageSender {
void send(String message, String recipient);
}
// 具体实现化角色
class EmailSender implements MessageSender {
@Override
public void send(String message, String recipient) {
System.out.println("发送邮件给" + recipient + ":" + message);
}
}
class SmsSender implements MessageSender {
@Override
public void send(String message, String recipient) {
System.out.println("发送短信给" + recipient + ":" + message);
}
}
class WeChatSender implements MessageSender {
@Override
public void send(String message, String recipient) {
System.out.println("发送微信给" + recipient + ":" + message);
}
}
抽象化角色是「消息类型」:
// 抽象化角色:消息
abstract class Message {
protected MessageSender sender;
protected Message(MessageSender sender) {
this.sender = sender;
}
public abstract void send(String message, String recipient);
}
// 修正抽象化角色:普通消息
class NormalMessage extends Message {
public NormalMessage(MessageSender sender) {
super(sender);
}
@Override
public void send(String message, String recipient) {
sender.send("[普通]" + message, recipient);
}
}
// 修正抽象化角色:紧急消息
class UrgentMessage extends Message {
public UrgentMessage(MessageSender sender) {
super(sender);
}
@Override
public void send(String message, String recipient) {
sender.send("[紧急]" + message, recipient);
}
}
// 修正抽象化角色:加急消息
class ExpressMessage extends Message {
public ExpressMessage(MessageSender sender) {
super(sender);
}
@Override
public void send(String message, String recipient) {
sender.send("[加急]" + message, recipient);
}
}
客户端使用:
public class Client {
public static void main(String[] args) {
// 普通消息通过邮件发送
Message msg1 = new NormalMessage(new EmailSender());
msg1.send("你好,这是一封普通邮件", "alice@example.com");
// 紧急消息通过短信发送
Message msg2 = new UrgentMessage(new SmsSender());
msg2.send("服务器宕机,请立即处理!", "13800138000");
// 加急消息通过微信发送
Message msg3 = new ExpressMessage(new WeChatSender());
msg3.send("系统升级通知", "运维群");
}
}
我曾经踩过的坑:一开始我把消息类型和发送方式用继承做,结果来了个「紧急邮件」「紧急短信」「紧急微信」……类数量直接翻倍。后来改成桥接模式,新增一种发送方式只需要加一个Sender实现类,消息类型类完全不用动。这才是正确的扩展姿势。
桥接模式的结构图
下面这张图展示了桥接模式的核心结构,我习惯用这种图来跟团队沟通设计思路:
桥接模式的适用场景
我个人总结了几种适合用桥接模式的情况:
| 场景 | 说明 | 例子 |
|---|---|---|
| 多维度变化 | 系统有多个独立变化的维度 | 图形×平台、消息类型×发送方式 |
| 避免类爆炸 | 继承导致子类数量过多 | M种抽象×N种实现 → M+N个类 |
| 运行时切换 | 需要在运行时动态选择实现 | 根据配置文件选择绘制方式 |
| 实现细节隐藏 | 不想让客户端知道具体实现 | 客户端只关心Shape.draw() |
桥接模式的优缺点
优点:
- 抽象和实现分离,各自独立扩展
- 符合开闭原则,新增维度不影响已有代码
- 实现细节对客户端透明
- 组合优于继承,更灵活
缺点:
- 增加了系统的复杂度,需要额外设计两个独立层次
- 如果维度不多,用桥接模式反而显得过度设计
我的建议:不要一上来就用桥接模式。先观察变化趋势,当发现两个维度确实都在频繁变化时,再引入桥接。我一般会在第三个变化点出现时重构——前两次先忍着,第三次就说明这不是偶然了。
桥接模式与适配器模式的区别
这两个模式容易搞混,我简单说一下:
- 适配器模式:解决「接口不兼容」的问题,让原本不匹配的接口能一起工作。重点是事后补救。
- 桥接模式:解决「抽象和实现分离」的问题,让它们能独立变化。重点是事前设计。
说白了,适配器是「让旧东西能用在新系统里」,桥接是「让新系统设计得更灵活」。
总结
桥接模式的核心就一句话:用组合代替继承,把抽象和实现拆成两个独立维度。
我在实际项目中,桥接模式用得最多的地方就是跨平台组件和消息系统。每次产品经理说「再加一个平台」或者「再加一种消息类型」,我都能淡定地说:没问题,加一个实现类就行,其他代码不用动。
这种底气,就是桥接模式给的。
公众号:蓝海资料掘金营,微信deep3321