桥接模式:抽象与实现分离的艺术

桥接模式,说白了就是「把抽象和实现拆开,让它们各自独立变化」。

我刚开始接触设计模式时,总觉得这玩意儿有点多余。直到有一次,我在一个图形绘制系统里被多重继承折磨得死去活来……嗯,从那以后,我对桥接模式就再也不敢小瞧了。

为什么需要桥接模式?

你想想看,假设我们要开发一个跨平台的绘图程序。抽象层面有「圆形」、「矩形」这些形状,实现层面有「Windows渲染」、「Linux渲染」这些平台。

如果不用桥接模式,最直接的做法就是:

  • Windows圆形类
  • Windows矩形类
  • Linux圆形类
  • Linux矩形类

这还只是2种形状×2个平台。如果再加一种形状,或者再加一个平台,类数量就会爆炸式增长。这就是典型的「类爆炸」问题。

核心思想:用组合代替继承。把「抽象」和「实现」放在两个独立的类层次结构中,通过组合关系连接它们。

桥接模式的结构

桥接模式有四个角色:

  • Abstraction(抽象类):定义抽象接口,持有Implementor的引用
  • RefinedAbstraction(扩展抽象类):扩展抽象接口
  • Implementor(实现类接口):定义实现类的接口
  • ConcreteImplementor(具体实现类):实现Implementor接口

我习惯把桥接模式理解成「遥控器」和「电视机」的关系。遥控器是抽象,电视机是实现。你换遥控器不影响电视机,换电视机也不影响遥控器——只要接口对得上就行。

SVG结构图:桥接模式核心逻辑

桥接模式结构图 Abstraction(抽象类) - implementor: Implementor + operation() RefinedAbstraction + refinedOperation() Implementor(接口) <<interface>> + operationImpl() 组合关系 ConcreteImplementorA + operationImpl() ConcreteImplementorB + operationImpl() 抽象与实现分离,各自独立变化

Java实现:图形绘制系统

咱们直接上代码。先定义实现接口:

// 实现类接口
public interface DrawingAPI {
    void drawCircle(double x, double y, double radius);
    void drawRectangle(double x, double y, double width, double height);
}

// 具体实现:Windows平台
public class WindowsDrawingAPI implements DrawingAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.println("Windows绘制圆形,中心(" + x + "," + y + "),半径" + radius);
    }
    
    @Override
    public void drawRectangle(double x, double y, double width, double height) {
        System.out.println("Windows绘制矩形,左上角(" + x + "," + y + "),宽" + width + "高" + height);
    }
}

// 具体实现:Linux平台
public class LinuxDrawingAPI implements DrawingAPI {
    @Override
    public void drawCircle(double x, double y, double radius) {
        System.out.println("Linux绘制圆形,中心(" + x + "," + y + "),半径" + radius);
    }
    
    @Override
    public void drawRectangle(double x, double y, double width, double height) {
        System.out.println("Linux绘制矩形,左上角(" + x + "," + y + "),宽" + width + "高" + height);
    }
}

然后是抽象类:

// 抽象类
public abstract class Shape {
    protected DrawingAPI drawingAPI;
    
    protected Shape(DrawingAPI drawingAPI) {
        this.drawingAPI = drawingAPI;
    }
    
    public abstract void draw();  // 委托给实现
    public abstract void resize(double factor);
}

// 扩展抽象类:圆形
public class Circle extends Shape {
    private double x, y, radius;
    
    public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }
    
    @Override
    public void draw() {
        drawingAPI.drawCircle(x, y, radius);
    }
    
    @Override
    public void resize(double factor) {
        radius *= factor;
    }
}

// 扩展抽象类:矩形
public class Rectangle extends Shape {
    private double x, y, width, height;
    
    public Rectangle(double x, double y, double width, double height, DrawingAPI drawingAPI) {
        super(drawingAPI);
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    
    @Override
    public void draw() {
        drawingAPI.drawRectangle(x, y, width, height);
    }
    
    @Override
    public void resize(double factor) {
        width *= factor;
        height *= factor;
    }
}

客户端使用:

public class Client {
    public static void main(String[] args) {
        Shape circle = new Circle(10, 20, 5, new WindowsDrawingAPI());
        Shape rectangle = new Rectangle(30, 40, 100, 50, new LinuxDrawingAPI());
        
        circle.draw();     // Windows绘制圆形...
        rectangle.draw();  // Linux绘制矩形...
        
        // 运行时切换实现
        circle = new Circle(10, 20, 5, new LinuxDrawingAPI());
        circle.draw();     // Linux绘制圆形...
    }
}

C++实现:多重继承的替代方案

在C++里,很多人遇到这种场景第一反应就是多重继承。但多重继承带来的菱形继承、虚继承等问题,说实话挺让人头疼的。

桥接模式提供了一种更优雅的方案:

// 实现类接口
class DrawingAPI {
public:
    virtual ~DrawingAPI() = default;
    virtual void drawCircle(double x, double y, double radius) = 0;
    virtual void drawRectangle(double x, double y, double width, double height) = 0;
};

// 具体实现
class WindowsDrawingAPI : public DrawingAPI {
public:
    void drawCircle(double x, double y, double radius) override {
        std::cout << "Windows绘制圆形" << std::endl;
    }
    void drawRectangle(double x, double y, double width, double height) override {
        std::cout << "Windows绘制矩形" << std::endl;
    }
};

class LinuxDrawingAPI : public DrawingAPI {
public:
    void drawCircle(double x, double y, double radius) override {
        std::cout << "Linux绘制圆形" << std::endl;
    }
    void drawRectangle(double x, double y, double width, double height) override {
        std::cout << "Linux绘制矩形" << std::endl;
    }
};

// 抽象类
class Shape {
protected:
    DrawingAPI* drawingAPI;
public:
    Shape(DrawingAPI* api) : drawingAPI(api) {}
    virtual ~Shape() = default;
    virtual void draw() = 0;
    virtual void resize(double factor) = 0;
};

class Circle : public Shape {
    double x, y, radius;
public:
    Circle(double x, double y, double r, DrawingAPI* api)
        : Shape(api), x(x), y(y), radius(r) {}
    void draw() override {
        drawingAPI->drawCircle(x, y, radius);
    }
    void resize(double factor) override {
        radius *= factor;
    }
};

我的经验:在C++项目中,如果遇到「一个类需要从多个维度变化」的场景,我通常会先考虑桥接模式,而不是直接上多重继承。桥接模式让代码更容易测试,也更容易扩展。

JDBC驱动桥接:最经典的例子

说到桥接模式的实际应用,JDBC驱动绝对是最经典的例子。你想想看:

  • 抽象部分:JDBC API(Connection、Statement、ResultSet)
  • 实现部分:各种数据库驱动(MySQL驱动、Oracle驱动、PostgreSQL驱动)

你在代码里写的是JDBC标准接口,底层实际调用的是具体驱动的实现。这就是桥接模式——抽象和实现分离,各自独立变化。

// JDBC桥接模式示意
// 抽象:JDBC Connection
Connection conn = DriverManager.getConnection(url, user, password);
// 实际返回的是 MySQLConnection 或 OracleConnection

// 抽象:Statement
Statement stmt = conn.createStatement();
// 实际返回的是 MySQLStatement 或 OracleStatement

// 你只依赖抽象接口,不依赖具体实现
ResultSet rs = stmt.executeQuery("SELECT * FROM users");

为什么JDBC要用桥接模式?因为数据库厂商太多了。如果不用桥接,每换一个数据库就得改一遍业务代码,那不得累死?

桥接模式 vs 适配器模式

这两个模式容易搞混,我简单说说区别:

对比维度 桥接模式 适配器模式
设计目的 分离抽象与实现,让它们独立变化 让不兼容的接口协同工作
使用时机 设计阶段,提前规划 后期维护,适配已有代码
关系方向 抽象依赖实现(组合) 适配器包装被适配者
变化维度 抽象和实现两个维度独立变化 通常只适配一个维度

注意:桥接模式不是万能的。如果抽象和实现之间没有独立变化的可能,强行使用桥接模式只会增加复杂度。我曾经在一个小项目里过度设计,用了桥接模式,结果代码量翻了一倍,维护起来反而更麻烦。

什么时候用桥接模式?

我个人总结了几个适用场景:

  1. 多维度变化:抽象和实现都需要独立扩展。比如形状和平台、消息和发送方式
  2. 运行时切换:需要在运行时动态选择实现。比如根据配置文件选择数据库驱动
  3. 避免类爆炸:如果不用桥接,类数量会呈乘积增长
  4. 共享实现:多个抽象对象可以共享同一个实现对象

避坑指南

我曾经在一个项目中,把桥接模式和策略模式搞混了。桥接模式关注的是「抽象与实现的分离」,策略模式关注的是「算法的封装与替换」。虽然结构上有点像,但意图完全不同。

还有一次,我在一个只有两个变化维度的场景里用了桥接模式,结果发现直接用工厂模式更简单。所以我的建议是:

  • 如果只有两个变化维度,先考虑工厂模式
  • 如果变化维度超过两个,或者需要在运行时动态切换,再考虑桥接模式
  • 不要为了用设计模式而用设计模式

桥接模式的核心价值,说白了就是「解耦」。把抽象和实现拆开,让它们各自独立演化。这个思想在很多地方都能看到,比如操作系统和硬件驱动、前端框架和渲染引擎……嗯,理解了桥接模式,你就掌握了一种应对复杂变化的思维方式。

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