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

桥接模式,说实话,是我在项目中用得最频繁的结构型模式之一。为什么?因为它解决了一个很实在的问题——当你的抽象和实现都需要独立变化时,继承体系会迅速膨胀到失控。我见过太多项目,一开始用继承挺爽,后来改一个需求要动十几个类,那叫一个酸爽。

模式动机与定义

先聊聊动机。你想想看,假设我们要开发一个图形绘制系统。有圆形、矩形、三角形这些形状,每种形状又可以用不同的方式绘制——比如用OpenGL、DirectX或者软件渲染。

如果用继承,你会得到什么?

// 糟糕的继承设计
class CircleOpenGL : public Shape { ... };
class CircleDirectX : public Shape { ... };
class CircleSoftware : public Shape { ... };
class RectOpenGL : public Shape { ... };
class RectDirectX : public Shape { ... };
class RectSoftware : public Shape { ... };
// ... 每增加一种形状或绘制方式,类数量就翻倍

嗯,这里要注意。3种形状 × 3种绘制方式 = 9个类。如果变成10种形状、5种绘制方式呢?50个类。这还不算完,如果绘制方式需要新增参数,你得改多少个类?

桥接模式的核心思想很简单:把抽象和实现解耦,让它们可以独立变化。说白了,就是把「形状」和「绘制方式」这两个维度拆开,各自维护自己的继承体系,然后用一个「桥」把它们连起来。

定义:桥接模式(Bridge Pattern)将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种结构型设计模式,通过组合关系替代继承关系,从而降低抽象和实现之间的耦合度。

角色与结构

桥接模式涉及四个核心角色,我习惯这么记:

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

结构上,Abstraction「桥接」到Implementor,而不是通过继承绑定。这样两边各玩各的,互不干扰。

个人经验:我在重构一个老旧图形引擎时,发现绘制逻辑和形状逻辑完全耦合在一起。每次加新形状都要复制粘贴绘制代码。用桥接模式重构后,绘制方式独立成一个接口,新形状只需要关心自己的几何数据,绘制的事情交给具体的Implementor去处理。代码量减少了40%,而且测试起来也方便多了。

下面这张图展示了桥接模式的核心结构:

抽象层次 (Abstraction) Shape(抽象类) + draw() - renderer: Renderer* Circle Rectangle 实现层次 (Implementor) Renderer(接口) + renderCircle(x, y, r) + renderRect(x, y, w, h) OpenGLRenderer DirectXRenderer 桥接 组合关系 抽象与实现通过组合关系连接,各自独立变化

C++代码实现

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

// 实现类接口 - 绘制方式
class Renderer {
public:
    virtual ~Renderer() = default;
    virtual void renderCircle(float x, float y, float radius) = 0;
    virtual void renderRect(float x, float y, float width, float height) = 0;
};

然后是两个具体实现:

class OpenGLRenderer : public Renderer {
public:
    void renderCircle(float x, float y, float radius) override {
        std::cout << "[OpenGL] 绘制圆形 at (" << x << ", " << y 
                  << "), 半径: " << radius << std::endl;
    }
    
    void renderRect(float x, float y, float width, float height) override {
        std::cout << "[OpenGL] 绘制矩形 at (" << x << ", " << y 
                  << "), 尺寸: " << width << "x" << height << std::endl;
    }
};

class DirectXRenderer : public Renderer {
public:
    void renderCircle(float x, float y, float radius) override {
        std::cout << "[DirectX] 绘制圆形 at (" << x << ", " << y 
                  << "), 半径: " << radius << std::endl;
    }
    
    void renderRect(float x, float y, float width, float height) override {
        std::cout << "[DirectX] 绘制矩形 at (" << x << ", " << y 
                  << "), 尺寸: " << width << "x" << height << std::endl;
    }
};

抽象类持有实现接口的引用:

class Shape {
protected:
    Renderer* renderer_;  // 桥接的关键:组合而非继承
    
public:
    explicit Shape(Renderer* renderer) : renderer_(renderer) {
        if (!renderer_) throw std::invalid_argument("Renderer不能为空");
    }
    
    virtual ~Shape() = default;
    virtual void draw() = 0;
    
    // 运行时切换实现
    void setRenderer(Renderer* renderer) { renderer_ = renderer; }
};

具体形状类:

class Circle : public Shape {
    float x_, y_, radius_;
    
public:
    Circle(Renderer* renderer, float x, float y, float r)
        : Shape(renderer), x_(x), y_(y), radius_(r) {}
    
    void draw() override {
        renderer_->renderCircle(x_, y_, radius_);
    }
    
    void resize(float factor) { radius_ *= factor; }
};

class Rectangle : public Shape {
    float x_, y_, width_, height_;
    
public:
    Rectangle(Renderer* renderer, float x, float y, float w, float h)
        : Shape(renderer), x_(x), y_(y), width_(w), height_(h) {}
    
    void draw() override {
        renderer_->renderRect(x_, y_, width_, height_);
    }
};

使用示例:

int main() {
    OpenGLRenderer opengl;
    DirectXRenderer directx;
    
    // 同一个形状,不同的绘制方式
    Circle circle1(&opengl, 100, 100, 50);
    Circle circle2(&directx, 200, 200, 30);
    
    circle1.draw();  // [OpenGL] 绘制圆形...
    circle2.draw();  // [DirectX] 绘制圆形...
    
    // 运行时切换实现
    circle1.setRenderer(&directx);
    circle1.draw();  // 现在用DirectX绘制了
    
    return 0;
}

避坑指南:我曾经在项目中犯过一个错误——把Renderer的创建逻辑也放到了Shape里。结果Shape既要管形状,又要管渲染器的生命周期,耦合度又上去了。记住:桥接模式只负责连接,不负责创建。Renderer应该由外部传入,Shape只负责使用。

分离抽象与实现

桥接模式最核心的价值,就是让抽象和实现可以各自独立演化。我总结了几点实践经验:

  • 什么时候用?当你发现一个类有两个独立变化的维度,而且用继承会导致类爆炸时。比如形状×绘制方式、消息×发送渠道、设备×操作系统。
  • 什么时候别用?如果只有一个维度会变化,或者变化频率极低,用桥接反而增加了复杂度。别为了用模式而用模式。
  • 性能考量:桥接模式引入了一层间接调用,对性能有微小影响。但在99%的业务场景中,这点开销可以忽略不计。如果你在做实时渲染引擎,可能需要权衡。

我的习惯:在设计初期,我会先问自己一个问题:「这个类的哪些部分可能会变化?」如果答案是「多个维度」,我就会考虑桥接模式。另外,我建议把Implementor设计得尽量稳定,因为它是被多个Abstraction依赖的。一旦Implementor接口频繁变动,所有Abstraction都得跟着改,那就得不偿失了。

桥接模式还有一个容易被忽略的好处——它让单元测试变得简单。你可以为Renderer写一个Mock实现,然后在测试Shape时注入Mock,验证Shape是否正确调用了Renderer的方法。这在继承体系中很难做到,因为实现和抽象是绑死的。

嗯,总结一下。桥接模式不是什么高深莫测的东西,它就是一个很朴素的道理:当事情变得复杂时,拆开来各自处理,然后用一个桥连起来。你在项目中遇到类似的问题时,不妨试试这个思路。


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