C++ / Qt · 设计模式 · 结构型

Day 7:桥接模式(Bridge Pattern)

2026-08-19 · 把"抽象"与"实现"拆成两座独立的山,再用一座组合的桥连起来

①今日主题

桥接模式(Bridge Pattern):将"抽象部分"与"实现部分"分离,使两者可以各自独立地变化——抽象端持有实现端的接口引用,通过组合而不是继承来连接两端。

它是 GoF 23 种设计模式中的结构型模式,别名"Handle/Body"。一句话理解:当系统中一个类存在两个独立变化的维度(比如"形状 × 渲染方式"、"设备 × 操作系统")时,继承会让类数量按乘法爆炸(M×N),而桥接只让类数量按加法增长(M+N)。这是"组合优于继承"最典型的教科书案例。

核心句:把"是什么"和"怎么画"分开。抽象层负责"是什么"(几何、业务逻辑),实现层负责"怎么画"(渲染、平台细节),两者通过一个窄接口桥接,互不绑架。

②为什么需要这个模式

设想你要做一个图形编辑器:有 2 种形状(圆、矩形),需要支持 3 种渲染后端(SVG 文件、控制台文本、Qt 窗口绘制)。新手最常见的两种写法,都会在需求增长时崩盘。

写法一:二维继承矩阵。让每种"形状 × 后端"组合成为一个类:

// 坏味道:形状 × 渲染 的二维继承矩阵
class Shape { /* 几何数据 */ };
class Circle : public Shape { /* ... */ };
class Rect   : public Shape { /* ... */ };

class SvgCircle     : public Circle { void draw() override; };  // SVG 圆
class ConsoleCircle : public Circle { void draw() override; };  // 终端圆
class QtCircle      : public Circle { void draw() override; };  // Qt 圆
// Rect 同样要再来三份:SvgRect / ConsoleRect / QtRect
// 2 种形状 × 3 种后端 = 6 个类;再加一种渲染后端,就要 +2 个类
// 再加一种形状(三角形),就要 +3 个类 —— 类数量按乘法膨胀

写法二:if-else 中央分发。把组合逻辑写进一个分发函数:

// 坏味道:if-else 层层嵌套,调用方被拖下水
void drawAll(ShapeType st, RenderType rt) {
    if (rt == RenderType::SVG) {
        if (st == ShapeType::CIRCLE)      drawSvgCircle();
        else if (st == ShapeType::RECT)   drawSvgRect();
    } else if (rt == RenderType::CONSOLE) {
        if (st == ShapeType::CIRCLE)      drawConsoleCircle();
        else if (st == ShapeType::RECT)   drawConsoleRect();
    } // 每加一种后端:外层多套一层 if;每加一种形状:每个分支都要改
}

这两种写法的问题,恰好对应 SOLID 原则的违背:

本质问题:类有两个独立的"变化轴",却被硬绑在同一个继承体系里。桥接模式正是要把这两根轴拆开。

③核心思想

桥接的核心思想极其朴素:识别出系统中两个独立变化的维度,为它们各建一个继承体系,然后在抽象端"持有"实现端的接口引用,用组合搭桥。从此形状不知道 SVG 是什么,渲染器也不知道圆是什么——它们只通过一个窄接口对话。

三个生活类比帮你建立直觉:

注意区分两个"实现"的含义:桥接里的"抽象"指客户眼中的高层接口(Shape、RemoteControl),"实现"指底层机制(渲染引擎、电视硬件)。它们不是"接口与类"的关系,而是两个并行的类层次——这正是初学者最容易混淆的地方。

④UML 与角色关系

桥接模式只有 4 个角色,结构清晰:

        ┌───────────────────────────────┐
        │   Abstraction(Shape)        │
        │   - renderer_: Renderer*      │  ← 持有实现接口(桥)
        └───────────────┬───────────────┘
                        │ 组合(composition)
        ┌───────────────▼───────────────┐
        │   Implementor(Renderer)     │  <<interface>>
        │   + drawCircle(...)           │
        │   + drawRect(...)             │
        └───────────────┬───────────────┘
        ┌───────────────┴───────────────┐
        │  ConcreteImplementor          │
        │  ConsoleRenderer / SvgAPI     │
        │  PainterRenderer              │
        └───────────────────────────────┘

   RefinedAbstraction(Circle / Rect)
        └─ 继承 Abstraction,扩展抽象层行为(moveBy/scale)
角色本课示例职责
Abstraction(抽象类)Shape定义高层接口,持有 Implementor 引用(组合桥),管理抽象层自身行为
RefinedAbstraction(扩展抽象)Circle / Rect在抽象层增加新的行为(移动、缩放),实现 draw() 把数据交给实现端
Implementor(实现接口)Renderer / DrawAPI定义实现层次的窄接口,与抽象接口完全解耦
ConcreteImplementor(具体实现)ConsoleRenderer / SvgAPI / PainterRenderer真正执行底层机制(画到终端、生成 SVG、用 QPainter 绘制)

关键点:两个继承体系之间只有一条"桥"(Abstraction 对 Implementor 的组合引用),除此之外互不相识。新增一个 ConcreteImplementor 或一个 RefinedAbstraction,都不需要动对方体系里的任何类。

⑤最小 C++ 示例

我们用"图形 × 渲染后端"演示最纯粹的桥接。2 种形状 × 2 种后端,只需要 4 个类(而不是 4 个组合类)。代码为 C++17,RAII + 智能指针,可直接编译运行:

#include <iostream>
#include <memory>
#include <vector>

// ============ 实现层次(Implementor):绘制后端接口 ============
class DrawAPI {
public:
    virtual ~DrawAPI() = default;
    virtual void drawCircle(double cx, double cy, double r) = 0;
    virtual void drawRect(double x, double y, double w, double h) = 0;
};

// 具体实现 A:输出 SVG 片段
class SvgAPI : public DrawAPI {
public:
    void drawCircle(double cx, double cy, double r) override {
        std::cout << "  <circle cx=\"" << cx << "\" cy=\"" << cy
                  << "\" r=\"" << r << "\" />\n";
    }
    void drawRect(double x, double y, double w, double h) override {
        std::cout << "  <rect x=\"" << x << "\" y=\"" << y
                  << "\" width=\"" << w << "\" height=\"" << h << "\" />\n";
    }
};

// 具体实现 B:控制台文本输出
class ConsoleAPI : public DrawAPI {
public:
    void drawCircle(double cx, double cy, double r) override {
        std::cout << "[控制台] 圆:圆心(" << cx << ", " << cy
                  << ") 半径 " << r << "\n";
    }
    void drawRect(double x, double y, double w, double h) override {
        std::cout << "[控制台] 矩形:(" << x << ", " << y << ") "
                  << w << " x " << h << "\n";
    }
};

// ============ 抽象层次(Abstraction):形状 ============
class Shape {
public:
    explicit Shape(std::shared_ptr<DrawAPI> api) : api_(std::move(api)) {}
    virtual ~Shape() = default;

    virtual void draw() = 0;               // 桥接入口:委托给实现层次
    virtual void scale(double factor) = 0; // 抽象层自身行为:缩放

protected:
    std::shared_ptr<DrawAPI> api_;        // 桥:持有实现接口(组合)
};

class Circle : public Shape {
public:
    Circle(double cx, double cy, double r, std::shared_ptr<DrawAPI> api)
        : Shape(std::move(api)), cx_(cx), cy_(cy), r_(r) {}

    void draw() override { api_->drawCircle(cx_, cy_, r_); }
    void scale(double factor) override { r_ *= factor; }

private:
    double cx_, cy_, r_;
};

class Rect : public Shape {
public:
    Rect(double x, double y, double w, double h, std::shared_ptr<DrawAPI> api)
        : Shape(std::move(api)), x_(x), y_(y), w_(w), h_(h) {}

    void draw() override { api_->drawRect(x_, y_, w_, h_); }
    void scale(double factor) override { w_ *= factor; h_ *= factor; }

private:
    double x_, y_, w_, h_;
};

int main() {
    // 两个实现后端
    auto svg     = std::make_shared<SvgAPI>();
    auto console = std::make_shared<ConsoleAPI>();

    // 4 种组合(2 形状 × 2 后端),只需 4 个对象、4 个类
    std::vector<std::shared_ptr<Shape>> shapes;
    shapes.push_back(std::make_shared<Circle>(1.0, 2.0, 3.0, svg));
    shapes.push_back(std::make_shared<Rect>(0.0, 0.0, 4.0, 2.0, svg));
    shapes.push_back(std::make_shared<Circle>(5.0, 5.0, 1.5, console));
    shapes.push_back(std::make_shared<Rect>(2.0, 2.0, 2.0, 2.0, console));

    for (const auto& s : shapes) {
        s->draw();
    }

    // 抽象层行为:缩放,与后端无关
    shapes[0]->scale(2.0);
    std::cout << "--- 把第一个圆放大 2 倍后 ---\n";
    shapes[0]->draw();
    return 0;
}

程序输出:

  <circle cx="1" cy="2" r="3" />
  <rect x="0" y="0" width="4" height="2" />
[控制台] 圆:圆心(5, 5) 半径 1.5
[控制台] 矩形:(2, 2) 2 x 2
--- 把第一个圆放大 2 倍后 ---
  <circle cx="1" cy="2" r="6" />

解读:Circle 和 Rect 完全不知道 SVG 与控制台的存在,只调用 DrawAPI 接口;SvgAPI 和 ConsoleAPI 也完全不知道"圆"和"矩形"的存在。以后加一个 Triangle:只写一个类;加一个 QtAPI:也只写一个类。这就是加法增长。

⑥Qt 实战示例

把上面的例子升级成真正的 Qt6 程序:同一个 Shape 体系,一端桥接到 QPainter(在窗口中实时绘制),另一端桥接到控制台(打印文本)。界面提供一个下拉框,运行时整体切换渲染后端——这正是桥接"实现可独立变化"的杀手锏。

工程文件结构:renderer.h、shapes.h、renderer_console.h/.cpp、renderer_qt.h/.cpp、main.cpp、CMakeLists.txt。

① renderer.h —— 实现层次接口(桥的一端)

#pragma once
#include <QString>

// 实现层次(Implementor):渲染后端接口
// 只关心"怎么画",不关心"画的是什么"
class Renderer {
public:
    virtual ~Renderer() = default;

    virtual void drawCircle(double cx, double cy, double r) const = 0;
    virtual void drawRect(double x, double y, double w, double h) const = 0;
    virtual QString name() const = 0;   // 后端名称(界面展示用)
};

② shapes.h —— 抽象层次(桥的另一端)

#pragma once

#include <memory>
#include <QString>
#include "renderer.h"

// 抽象层次(Abstraction):图形基类
// 职责:管理几何数据与自身行为(移动),绘制全部委托给 Renderer
class Shape {
public:
    explicit Shape(std::shared_ptr<Renderer> renderer)
        : renderer_(std::move(renderer)) {}
    virtual ~Shape() = default;

    virtual void draw() const = 0;                  // 子类把几何数据交给 renderer
    virtual void moveBy(double dx, double dy) = 0;  // 抽象层自己的行为
    virtual QString describe() const = 0;

    // 运行期切换实现(桥接"实现可独立变化"的直接体现)
    void setRenderer(std::shared_ptr<Renderer> renderer) {
        renderer_ = std::move(renderer);
    }

protected:
    std::shared_ptr<Renderer> renderer_;  // 桥:组合持有实现接口
};

// 具体抽象:圆
class Circle : public Shape {
public:
    Circle(double cx, double cy, double r, std::shared_ptr<Renderer> renderer)
        : Shape(std::move(renderer)), cx_(cx), cy_(cy), r_(r) {}

    void draw() const override { renderer_->drawCircle(cx_, cy_, r_); }
    void moveBy(double dx, double dy) override { cx_ += dx; cy_ += dy; }
    QString describe() const override {
        return QStringLiteral("圆(圆心=%1,%2 半径=%3)").arg(cx_).arg(cy_).arg(r_);
    }

    double centerX() const { return cx_; }
    double centerY() const { return cy_; }
    double radius()   const { return r_; }

private:
    double cx_, cy_, r_;
};

// 具体抽象:矩形
class Rect : public Shape {
public:
    Rect(double x, double y, double w, double h, std::shared_ptr<Renderer> renderer)
        : Shape(std::move(renderer)), x_(x), y_(y), w_(w), h_(h) {}

    void draw() const override { renderer_->drawRect(x_, y_, w_, h_); }
    void moveBy(double dx, double dy) override { x_ += dx; y_ += dy; }
    QString describe() const override {
        return QStringLiteral("矩形(左下角=%1,%2 宽=%3 高=%4)")
            .arg(x_).arg(y_).arg(w_).arg(h_);
    }

    double x() const { return x_; }
    double y() const { return y_; }
    double width()  const { return w_; }
    double height() const { return h_; }

private:
    double x_, y_, w_, h_;
};

③ renderer_console.h / renderer_console.cpp —— 具体实现 A:终端渲染

#pragma once
#include "renderer.h"

// 具体实现 A:字符终端渲染(不依赖任何 Qt 绘图组件)
class ConsoleRenderer : public Renderer {
public:
    void drawCircle(double cx, double cy, double r) const override;
    void drawRect(double x, double y, double w, double h) const override;
    QString name() const override { return QStringLiteral("Console"); }
};
#include "renderer_console.h"

#include <iostream>

void ConsoleRenderer::drawCircle(double cx, double cy, double r) const {
    std::cout << "[Console] 画圆:圆心(" << cx << ", " << cy
              << ") 半径 " << r << '\n';
}

void ConsoleRenderer::drawRect(double x, double y, double w, double h) const {
    std::cout << "[Console] 画矩形:(" << x << ", " << y << ") "
              << w << " x " << h << '\n';
}

④ renderer_qt.h / renderer_qt.cpp —— 具体实现 B:QPainter 窗口渲染

#pragma once
#include "renderer.h"

class QPainter;

// 具体实现 B:用 QPainter 在窗口中绘制
// painter 由外部(paintEvent 的栈对象)提供,本类不持有所有权
class PainterRenderer : public Renderer {
public:
    explicit PainterRenderer(QPainter* painter) : painter_(painter) {}

    void drawCircle(double cx, double cy, double r) const override;
    void drawRect(double x, double y, double w, double h) const override;
    QString name() const override { return QStringLiteral("QPainter"); }

private:
    QPainter* painter_; // 非拥有指针:生命周期由 paintEvent 管理
};
#include "renderer_qt.h"

#include <QPainter>

void PainterRenderer::drawCircle(double cx, double cy, double r) const {
    painter_->setBrush(Qt::NoBrush);
    painter_->setPen(QPen(Qt::blue, 0.08));   // 线宽 0.08(世界坐标单位)
    painter_->drawEllipse(QPointF(cx, cy), r, r);
}

void PainterRenderer::drawRect(double x, double y, double w, double h) const {
    painter_->setBrush(Qt::NoBrush);
    painter_->setPen(QPen(Qt::red, 0.08));
    painter_->drawRect(QRectF(x, y, w, h));
}

⑤ main.cpp —— 组装:画布窗口 + 后端切换下拉框

#include <QApplication>
#include <QComboBox>
#include <QPainter>
#include <QVBoxLayout>
#include <QWidget>

#include <memory>
#include <vector>

#include "renderer_console.h"
#include "renderer_qt.h"
#include "shapes.h"

// 画布:paintEvent 中临时创建 PainterRenderer 并桥接绘制
class Canvas : public QWidget {
public:
    explicit Canvas(std::vector<std::shared_ptr<Shape>> shapes)
        : shapes_(std::move(shapes)) {
        setMinimumSize(480, 320);
    }

protected:
    void paintEvent(QPaintEvent*) override {
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);
        // 坐标系改造成"数学坐标":原点居中、y 向上、放大 10 倍
        painter.translate(width() / 2.0, height() / 2.0);
        painter.scale(10.0, -10.0);

        // 桥接:把 QPainter 包装成 Renderer 实现,注入给所有图形
        const auto qtRenderer = std::make_shared<PainterRenderer>(&painter);
        for (const auto& shape : shapes_) {
            shape->setRenderer(qtRenderer);
            shape->draw();
        }
    }

private:
    std::vector<std::shared_ptr<Shape>> shapes_;
};

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);

    // 1. 先创建"实现层次":控制台渲染后端
    auto console = std::make_shared<ConsoleRenderer>();

    // 2. 创建"抽象层次"对象,构造时注入实现(组合成桥)
    auto circle = std::make_shared<Circle>(0.0, 0.0, 1.2, console);
    auto rect   = std::make_shared<Rect>(-1.8, -0.8, 3.6, 1.6, console);

    // 3. 先用控制台后端画一次(输出到 stdout)
    circle->draw();
    rect->draw();

    // 4. 界面:画布 + 渲染后端切换下拉框
    auto canvas = new Canvas({circle, rect});
    auto combo  = new QComboBox;
    combo->addItem(QStringLiteral("Console 终端输出"));
    combo->addItem(QStringLiteral("QPainter 窗口绘制"));

    // 选中 Console 时:运行时整体切换实现层次,控制台输出
    QObject::connect(combo, &QComboBox::currentIndexChanged, canvas,
        [circle, rect](int index) {
            if (index == 0) {
                const auto backend = std::make_shared<ConsoleRenderer>();
                circle->setRenderer(backend);
                rect->setRenderer(backend);
                circle->draw();
                rect->draw();
            }
        });

    auto window = new QWidget;
    auto layout = new QVBoxLayout(window);
    layout->addWidget(combo);
    layout->addWidget(canvas);
    window->setWindowTitle(QStringLiteral("Day 7 桥接模式(Bridge)Demo"));
    window->resize(520, 420);
    window->show();

    return app.exec();
}

⑥ CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(bridge_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Widgets)

add_executable(bridge_demo
    main.cpp
    renderer_console.cpp
    renderer_qt.cpp
)
target_link_libraries(bridge_demo PRIVATE Qt6::Widgets)

构建与运行:

cmake -B build -S .
cmake --build build -j
./build/bridge_demo

启动后终端会先打印两个图形,窗口中则用 QPainter 画出蓝色圆与红色矩形;把下拉框切到"Console 终端输出",控制台会立刻再次打印——同一批 Shape 对象,实现后端在运行时整体更换,抽象层一行代码都没改。注意 Canvas 的 paintEvent 每次都会重新注入 PainterRenderer,所以窗口绘制永远走 QPainter 后端,这就是桥接的灵活性:每个使用点可以按需选择桥接对象。

⑦代码执行流程

以 Qt 示例为例,程序完整走一遍:

① 构造阶段(main):先创建具体实现 ConsoleRenderer,再创建 Circle/Rect 并把实现通过构造函数依赖注入——桥在对象出生时就已经搭好。

② 首次调用(main 第 3 步):circle->draw() 进入抽象层,Circle::draw() 把自己的几何数据 (cx, cy, r) 通过 renderer_->drawCircle(...) 转发给实现层,ConsoleRenderer 在 stdout 打印一行文本。

③ 窗口绘制(paintEvent):Qt 触发重绘时,Canvas::paintEvent 创建栈对象 QPainter,把它包装成 PainterRenderer 注入所有形状,再逐个调用 shape->draw()——同一批形状对象这一次走的是 QPainter 后端,画到屏幕上。

④ 运行时切换(下拉框):用户切回 Console 项,lambda 里创建新的 ConsoleRenderer,对两个形状调用 setRenderer() 换桥,然后立即 draw() 验证——形状的几何状态(位置、大小)原样保留,只有"桥"换了。

⑤ 演化阶段:若明天要加三角形,新增 Triangle : Shape 一个类即可;若加 PDF 导出,新增 PdfRenderer 一个类即可。两个体系互不侵入。

⑧为什么这样设计

桥接模式的每个设计决策,都能在示例代码里找到落点:

⑨不使用会怎样

回到第②节的坏代码,把时间线拉长看后果:

一句话:桥接模式治的是"二维变化轴绑死在继承树上"的病。不治,类数量、修改面、测试成本一起膨胀。

⑩何时使用

适合的场景(3-5 个):

不适合的场景(2-4 个):

过度设计提醒:只有 2×2 的组合、且未来三年看不到新维度,就别上桥接(YAGNI)。先闻到"类名开始按二维组合膨胀"的味道,再动手拆桥。

⑪与其他模式的区别

对比项Bridge 桥接Adapter 适配器Strategy 策略
类型结构型结构型行为型
设计意图事前分离抽象与实现,面向未来变化事后让已有不兼容接口协同工作让算法可替换、可独立变化
变化维度两个维度各自有一族类,多对多组合通常只适配一个已有接口Context 只有一个维度(算法)变化
结构要点抽象端组合持有实现接口(桥)适配器包装被适配者,改写接口Context 组合持有策略接口,行为级切换
一句话记忆分接口,解耦改接口,配合换算法,变行为

⑫Qt 源码中的体现

桥接模式在 Qt 里不是边角料,而是架构级标配,至少有三处:

不强关联的提醒:QAbstractItemModel / QAbstractItemView 常被误认作 Bridge,其实它们之间是数据变化通知刷新的关系(更接近 Observer + Template Method),视图通过 QModelIndex 访问数据而非委托实现——别在面试里硬套。

⑬面试常见问题

Q1:桥接模式和适配器模式有什么区别?

答:核心在意图与时机。Adapter 是事后补救:两个已存在的接口不兼容,包一层让它们协同,重点在适配已有代码、尽快能用;Bridge 是事前设计:主动识别两个独立变化维度并分离,面向未来扩展。结构上 Adapter 是"包装+改写接口",Bridge 是"组合+分离接口"。一句话:Adapter 改接口去配合,Bridge 分接口去解耦。

Q2:桥接模式和策略模式结构很像,怎么区分?

答:结构上都用"接口+组合",但意图与维度不同。Strategy 是行为型,Context 是单个类、只有算法一个维度在变,属于"行为级替换";Bridge 是结构型,抽象端和实现端各是一族类,多对多组合,属于"结构级解耦"。检验方法:把两端的类族各数一遍——只有一端有一族类,是 Strategy;两端都有族类且自由配对,是 Bridge。

Q3:"抽象部分"和"实现部分"指什么?是接口和实现类吗?

答:不是语言层面的 interface/class 关系。"抽象"指客户眼中的高层概念(Shape、遥控器、Notification),"实现"指底层机制(渲染引擎、电视硬件、发送渠道)。两者都是完整的类层次,各有自己的基类与子类。判断是否该用桥接,看是否存在两个独立变化轴,而不是看接口语法。

Q4:桥接模式如何做到运行时切换实现?

答:抽象端持有实现接口的引用(如 std::shared_ptr<Renderer>)并提供 setRenderer() 方法。因为依赖的是抽象接口而非具体类,运行时替换引用指向的对象,即可整体更换实现,抽象层代码零修改。这也是"组合优于继承"的关键优势:继承在编译期锁死,组合在运行期可变。

Q5:QStyle 为什么是桥接模式的体现?

答:QWidget 把绘制(边框、背景、滚动条、下拉箭头等)委托给 style() 返回的 QStyle 抽象对象;QStyleFactory 按平台/设置创建不同的具体样式(Fusion、Windows、macOS 等)。控件逻辑(抽象端)与外观实现(实现端)通过 QStyle 接口桥接,新增一种样式不需要修改任何控件类,运行时 setStyle() 即可整体换肤。

⑭今日练习

需求(15-30 分钟):现有消息通知代码有 4 个类——TextEmail、HtmlEmail、TextSms、HtmlSms(2 种内容格式 × 2 种发送渠道)。请用桥接模式重构,使得:新增"微信渠道"只需加 1 个类,新增"Markdown 格式"只需加 1 个类。

提示 1:先定义渠道接口:virtual void send(const QString& title, const QString& body) = 0;。

提示 2:Notification 抽象持有 std::shared_ptr<Channel>;Text 与 Html 是抽象层的扩展,Email / Sms 是实现层的具体类。重构完再想一想:为什么"格式"是抽象、"渠道"是实现?如果反过来拆,结构还成立吗?

⑮今日总结

一句话记忆:"形状与画笔分家,中间搭一座组合的桥——抽象与实现,各自独立演化。"

代码特征信号(闻到这些味道,就该考虑桥接):

明日预告:Day 8 组合模式(Composite)——用树形结构统一"单个对象"与"组合对象",让叶子与容器对外表现一致,递归之美;QObject 父子树、QWidget 嵌套布局都是它的影子。