桥接模式:模式动机与定义

你有没有遇到过这种场景?

一个类,因为两个维度的变化,膨胀得像个气球。比如一个Window类,既要支持不同的操作系统(Windows、Linux、macOS),又要支持不同的窗口风格(普通、对话框、全屏)。

我早年接手过一个图形库,里面就是这种写法。每个新风格都要为每个操作系统写一遍实现。那代码,啧啧,又臭又长。后来我重构时用了桥接模式,才把这两个维度彻底解耦。

说白了,桥接模式就是解决「多维变化」问题的利器。它的核心思想很简单:将抽象部分与它的实现部分分离,使它们都可以独立地变化

你想想看,如果不用桥接,你会怎么处理?

  • 用继承?那得有多少子类?2个维度 × 3种变化 = 6个类。3个维度 × 5种变化 = 15个类。指数级爆炸。
  • 用if-else?那代码里全是switch-case,改一个地方得动整个类。

桥接模式就是告诉你:别把抽象和实现绑死。让它们通过组合的方式协作,而不是继承。

模式动机:当一个类存在两个独立变化的维度,且这两个维度都需要扩展时,使用桥接模式可以避免类爆炸,让系统更灵活。

角色与结构

桥接模式有四个核心角色。我习惯这么记:

角色 名称 职责
Abstraction 抽象类 定义抽象接口,持有Implementor的引用
RefinedAbstraction 扩展抽象类 对Abstraction进行扩展,增加新功能
Implementor 实现类接口 定义实现类的接口,通常比Abstraction更底层
ConcreteImplementor 具体实现类 实现Implementor接口,提供具体操作

嗯,这里要注意:Abstraction和Implementor之间是组合关系,不是继承。这是桥接模式的关键。

下面这张图,我画了桥接模式的核心结构。你看一眼就明白了。

Abstraction - impl: Implementor RefinedAbstraction Implementor + operationImpl() ConcreteImplA ConcreteImplB 组合

你看,Abstraction 通过组合持有 Implementor 的引用。这样,抽象和实现就可以各自独立变化了。

C++代码实现

光说不练假把式。咱们直接上代码。我以一个跨平台的绘图程序为例。

先定义实现类接口:

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

然后是两个具体实现:

class DrawingAPI1 : public DrawingAPI {
public:
    void drawCircle(double x, double y, double radius) override {
        std::cout << "API1: 在 (" << x << ", " << y 
                  << ") 画圆,半径 " << radius << std::endl;
    }
};

class DrawingAPI2 : public DrawingAPI {
public:
    void drawCircle(double x, double y, double radius) override {
        std::cout << "API2: 在 (" << x << ", " << y 
                  << ") 画圆,半径 " << radius << 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 CircleShape : public Shape {
private:
    double x, y, radius;
public:
    CircleShape(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;
    }
};

使用起来是这样的:

int main() {
    DrawingAPI1 api1;
    DrawingAPI2 api2;

    CircleShape circle1(1, 2, 3, &api1);
    CircleShape circle2(5, 7, 11, &api2);

    circle1.draw();  // 输出:API1: 在 (1, 2) 画圆,半径 3
    circle2.draw();  // 输出:API2: 在 (5, 7) 画圆,半径 11

    circle1.resize(2);
    circle1.draw();  // 输出:API1: 在 (1, 2) 画圆,半径 6

    return 0;
}
个人习惯:我一般把Implementor的接口设计得尽量小,只包含最底层的操作。这样具体实现类写起来轻松,也方便单元测试。

解决多维变化问题

回到开头的问题。为什么桥接模式能解决多维变化?

因为每个维度都变成了一个独立的类层次。你想想看:

  • 「形状」是一个维度:圆形、矩形、三角形……
  • 「绘制方式」是另一个维度:OpenGL、DirectX、软件渲染……

如果没有桥接,你要写 OpenGLCircleDirectXCircleOpenGLRect…… 每新增一个形状或绘制方式,都要新增一堆类。

用了桥接之后:

  • 新增形状?继承 Shape 就行。
  • 新增绘制方式?继承 DrawingAPI 就行。
  • 两者互不干扰。

我在项目中遇到过类似的需求:一个消息通知系统,既要支持不同的消息类型(文本、图片、视频),又要支持不同的发送渠道(邮件、短信、推送)。当时就是用桥接模式搞定的。后来产品经理说要加一个「语音消息」类型,我只加了一个类就完事了。要是用继承,那得改多少地方啊。

避坑指南:我曾经犯过一个错误——把桥接模式和策略模式搞混。桥接模式关注的是「抽象与实现的分离」,而策略模式关注的是「算法的替换」。如果你发现你的Implementor接口里只有一个方法,那大概率是策略模式,不是桥接。

最后总结一下:

  • 桥接模式适用于多维变化的场景。
  • 它通过组合代替继承,把抽象和实现解耦。
  • 代码结构清晰,扩展起来很舒服。

嗯,桥接模式就讲到这里。你可以在自己的项目里试试,特别是那些「一个类要处理多种变化」的地方。

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