桥接模式:抽象化与实现化分离

说实话,桥接模式是我在实际项目中用得比较多的一个设计模式。为什么?因为它解决了一个很现实的问题——当你的系统需要沿着多个维度变化时,怎么避免类爆炸。

我记得有一次接手一个跨平台图形绘制框架,刚开始设计时只考虑了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实现类,消息类型类完全不用动。这才是正确的扩展姿势。

桥接模式的结构图

下面这张图展示了桥接模式的核心结构,我习惯用这种图来跟团队沟通设计思路:

桥接模式结构图 抽象化角色 (Shape) - drawAPI: DrawAPI Circle + draw() Rectangle + draw() 桥接 实现化角色 (DrawAPI) + drawCircle() + drawRectangle() WindowsDrawAPI + drawCircle() LinuxDrawAPI + drawCircle() 关键说明 • 左侧:抽象化层次(图形),可独立扩展新图形 • 右侧:实现化层次(绘制方式),可独立扩展新平台 • 中间桥接:Shape 持有 DrawAPI 引用,组合代替继承 • 新增一种图形:只需加一个 Shape 子类 • 新增一个平台:只需加一个 DrawAPI 实现类

桥接模式的适用场景

我个人总结了几种适合用桥接模式的情况:

场景 说明 例子
多维度变化 系统有多个独立变化的维度 图形×平台、消息类型×发送方式
避免类爆炸 继承导致子类数量过多 M种抽象×N种实现 → M+N个类
运行时切换 需要在运行时动态选择实现 根据配置文件选择绘制方式
实现细节隐藏 不想让客户端知道具体实现 客户端只关心Shape.draw()

桥接模式的优缺点

优点:

  • 抽象和实现分离,各自独立扩展
  • 符合开闭原则,新增维度不影响已有代码
  • 实现细节对客户端透明
  • 组合优于继承,更灵活

缺点:

  • 增加了系统的复杂度,需要额外设计两个独立层次
  • 如果维度不多,用桥接模式反而显得过度设计

我的建议:不要一上来就用桥接模式。先观察变化趋势,当发现两个维度确实都在频繁变化时,再引入桥接。我一般会在第三个变化点出现时重构——前两次先忍着,第三次就说明这不是偶然了。

桥接模式与适配器模式的区别

这两个模式容易搞混,我简单说一下:

  • 适配器模式:解决「接口不兼容」的问题,让原本不匹配的接口能一起工作。重点是事后补救
  • 桥接模式:解决「抽象和实现分离」的问题,让它们能独立变化。重点是事前设计

说白了,适配器是「让旧东西能用在新系统里」,桥接是「让新系统设计得更灵活」。

总结

桥接模式的核心就一句话:用组合代替继承,把抽象和实现拆成两个独立维度

我在实际项目中,桥接模式用得最多的地方就是跨平台组件和消息系统。每次产品经理说「再加一个平台」或者「再加一种消息类型」,我都能淡定地说:没问题,加一个实现类就行,其他代码不用动。

这种底气,就是桥接模式给的。


公众号:蓝海资料掘金营,微信deep3321