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),"实现"指底层机制(渲染引擎、电视硬件)。它们不是"接口与类"的关系,而是两个并行的类层次——这正是初学者最容易混淆的地方。
桥接模式只有 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,都不需要动对方体系里的任何类。
我们用"图形 × 渲染后端"演示最纯粹的桥接。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:也只写一个类。这就是加法增长。
把上面的例子升级成真正的 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 一个类即可。两个体系互不侵入。
桥接模式的每个设计决策,都能在示例代码里找到落点:
Triangle 只动抽象端;加 PdfRenderer 只动实现端;任何一端的新类都不需要修改另一端的任何代码。Renderer 抽象接口,而不是依赖 ConsoleRenderer、SvgAPI 等具体类;具体类反过来依赖抽象。方向完全倒置。MockRenderer 即可,完全不需要真实渲染环境。回到第②节的坏代码,把时间线拉长看后果:
一句话:桥接模式治的是"二维变化轴绑死在继承树上"的病。不治,类数量、修改面、测试成本一起膨胀。
适合的场景(3-5 个):
不适合的场景(2-4 个):
过度设计提醒:只有 2×2 的组合、且未来三年看不到新维度,就别上桥接(YAGNI)。先闻到"类名开始按二维组合膨胀"的味道,再动手拆桥。
| 对比项 | Bridge 桥接 | Adapter 适配器 | Strategy 策略 |
|---|---|---|---|
| 类型 | 结构型 | 结构型 | 行为型 |
| 设计意图 | 事前分离抽象与实现,面向未来变化 | 事后让已有不兼容接口协同工作 | 让算法可替换、可独立变化 |
| 变化维度 | 两个维度各自有一族类,多对多组合 | 通常只适配一个已有接口 | Context 只有一个维度(算法)变化 |
| 结构要点 | 抽象端组合持有实现接口(桥) | 适配器包装被适配者,改写接口 | Context 组合持有策略接口,行为级切换 |
| 一句话记忆 | 分接口,解耦 | 改接口,配合 | 换算法,变行为 |
桥接模式在 Qt 里不是边角料,而是架构级标配,至少有三处:
style() 返回的 QStyle 抽象;QStyleFactory::create() 按平台与设置创建 Fusion、Windows、macOS 等具体样式。控件逻辑(抽象)与外观实现(实现)彻底分离——程序运行时 app.setStyle(...) 换个样式,所有控件的外观整体改变,控件代码零修改。这与我们示例中"切换 Renderer"如出一辙。QPlatformIntegration(xcb / wayland / windows / cocoa 各有一个实现)。Qt GUI 逻辑与具体操作系统通过平台接口桥接——这是 Qt 能"一次编写、处处编译"的根基。QPaintEngine(raster / opengl / pdf 引擎)。"画到哪里"(设备抽象)与"怎么画"(绘制实现)分离。不强关联的提醒:QAbstractItemModel / QAbstractItemView 常被误认作 Bridge,其实它们之间是数据变化通知刷新的关系(更接近 Observer + Template Method),视图通过 QModelIndex 访问数据而非委托实现——别在面试里硬套。
答:核心在意图与时机。Adapter 是事后补救:两个已存在的接口不兼容,包一层让它们协同,重点在适配已有代码、尽快能用;Bridge 是事前设计:主动识别两个独立变化维度并分离,面向未来扩展。结构上 Adapter 是"包装+改写接口",Bridge 是"组合+分离接口"。一句话:Adapter 改接口去配合,Bridge 分接口去解耦。
答:结构上都用"接口+组合",但意图与维度不同。Strategy 是行为型,Context 是单个类、只有算法一个维度在变,属于"行为级替换";Bridge 是结构型,抽象端和实现端各是一族类,多对多组合,属于"结构级解耦"。检验方法:把两端的类族各数一遍——只有一端有一族类,是 Strategy;两端都有族类且自由配对,是 Bridge。
答:不是语言层面的 interface/class 关系。"抽象"指客户眼中的高层概念(Shape、遥控器、Notification),"实现"指底层机制(渲染引擎、电视硬件、发送渠道)。两者都是完整的类层次,各有自己的基类与子类。判断是否该用桥接,看是否存在两个独立变化轴,而不是看接口语法。
答:抽象端持有实现接口的引用(如 std::shared_ptr<Renderer>)并提供 setRenderer() 方法。因为依赖的是抽象接口而非具体类,运行时替换引用指向的对象,即可整体更换实现,抽象层代码零修改。这也是"组合优于继承"的关键优势:继承在编译期锁死,组合在运行期可变。
答: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 是实现层的具体类。重构完再想一想:为什么"格式"是抽象、"渠道"是实现?如果反过来拆,结构还成立吗?