系列文章:
- 单例模式:【C++/Qt 进阶】设计模式系列(1):单例模式------饿汉式内存泄漏真相、指针单例存在的意义、五种实现终极对比-CSDN博客
- 工厂模式与抽象工厂模式:【C++/Qt 进阶】设计模式系列(2):工厂模式------从游戏角色到披萨店-CSDN博客
- 建造者模式:【C++/Qt 进阶】设计模式系列(3): 建造者模式------把"复杂对象的组装"从业务代码里剥离出来-CSDN博客
- 原型模式:【C++/Qt 进阶】设计模式系列(4):原型模式------用"克隆"代替"重新构造"-CSDN博客
- 适配器模式:【C++/Qt 进阶】设计模式系列(5):适配器模式------给"不兼容的接口"贴一层翻译膜-CSDN博客
- 桥接模式(本篇)
- 过滤器模式(持续更新中)
写在前面
⚠️ 上一篇适配器是"结构型 "模式开篇,本篇是"结构型"第二站。两者常被混淆,本篇将专门对比,厘清差异。
上一讲我们通过适配器模式解决了"老接口转换为新接口"的问题。但还有一类场景,适配器难以优雅应对------
"我的类有两个独立变化的维度,若用继承强行组合,会导致类数量呈指数级爆炸。"
这在 Qt 项目中极为常见,比如画图工具中的"形状 × 颜色"组合:
cpp
// 反人类的继承写法
class RedCircle : public Circle { void draw() override { /* 红圆 */ } };
class GreenCircle : public Circle { void draw() override { /* 绿圆 */ } };
class RedSquare : public Square { void draw() override { /* 红方 */ } };
class GreenSquare : public Square { void draw() override { /* 绿方 */ } };
// 4 种形状 × 5 种颜色 = 20 个类!再加一种颜色 → 25 个!
当 N 个形状 × M 种颜色时,需创建 N×M 个具体类。而形状和颜色都可能持续扩展------这就是典型的类爆炸。
菜鸟教程定义:将抽象部分与实现部分分离,使它们可以独立地变化。 (参考)
记忆口诀:"两条独立的线" → 桥接;"一对接口" → 适配。
一、30 秒看懂桥接模式
| 维度 | 适配器模式(贴翻译膜) | 桥接模式(拆两条线) |
|---|---|---|
| 解决什么 | 老接口 → 新接口 | 两个独立维度的类爆炸 |
| 关心什么 | 翻译、兼容 | 让两个维度各自自由扩展 |
| 何时用 | 接入老系统 / 第三方 SDK | 类数量呈 N×M 关系时 |
| 典型应用 | 日志封装 / 协议转换 | 画图 / 跨平台 / 设备驱动 |
| 设计时机 | 系统已存在后 | 系统设计时就使用 |
一句话总结:用"组合(has-a)"替代"继承(is-a)",把原本"竖着继承"的结构改为"横着组合",实现两个维度的解耦与独立演化。 在 C++ 中,即通过抽象类持有一个实现接口的指针或引用。
二、UML 类图与角色
以下为纯 C++/Qt 视角 的桥接模式结构图,采用 Qt 容器 + std::unique_ptr + 虚函数声明的写法。
text
┌──────────────────────────────┐
│ class IShape │ ← 抽象(Abstraction)
│ { │ 客户直接调用的接口
│ protected: │
│ std::unique_ptr │
│ <IDrawAPI> m_drawer; │ ← 桥:持有实现接口
│ public: │
│ explicit IShape( │
│ std::unique_ptr │
│ <IDrawAPI> d) │
│ : m_drawer(std::move(d)){}
│ virtual void draw() = 0; │
│ }; │
└───────────────┬──────────────┘
│ 继承
┌───────────┴────────────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ class Circle │ │ class Square │ ← 扩展抽象
│ : public │ │ : public │ (Refined
│ IShape │ │ IShape │ Abstraction)
│ { void draw() │ │ { void draw() │
│ override; } │ │ override; } │
└────────────────┘ └────────────────┘
│
│ 桥接(组合,非继承)
▼
┌──────────────────────────────┐
│ class IDrawAPI │ ← 实现接口(Implementor)
│ { │
│ public: │
│ virtual ~IDrawAPI() = default;
│ virtual void drawCircle( │
│ int r, int x, int y) = 0;│
│ virtual void drawSquare( │
│ int side, int x, int y) = 0;│
│ }; │
└───────────────┬──────────────┘
│ 实现
┌───────────┴────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ class RedDraw │ │ class GreenDraw │ ← 具体实现
│ : public │ │ : public │ (Concrete
│ IDrawAPI │ │ IDrawAPI │ Implementor)
│ { drawCircle │ │ { ... } │
│ override{} │ │ │
└──────────────┘ └──────────────┘
四大核心角色:
| 角色 | 职责 | C++/Qt 中的对应实现 |
|---|---|---|
| 抽象(Abstraction) | 客户端直接使用的接口,持有实现接口的指针 | class IShape { std::unique_ptr<IDrawAPI> m_drawer; } |
| 扩展抽象(Refined Abstraction) | 抽象的具体化(如圆、方、三角等) | Circle : public IShape |
| 实现接口(Implementor) | 为"实现维度"提供统一抽象(如颜色、平台、驱动等) | class IDrawAPI { virtual drawCircle() = 0; } |
| 具体实现(Concrete Implementor) | 实现接口的具体类 | RedDraw : public IDrawAPI |
💡 C++ 特色实践 :桥接模式在 C++ 中常结合 pImpl(Pointer to Implementation)惯用法 使用------将对外接口与内部实现彻底隔离,通过
std::unique_ptr<Impl>实现零成本抽象。本篇第 5 节将深入剖析这一高阶技巧。
三、基础代码实现(C++/Qt 版)
以 Qt 工程师最熟悉的"跨平台日志输出"为例,演示如何将"日志格式器"与"日志目的地"两个维度解耦。
3.1 实现接口:日志目的地
cpp
// ilogdestination.h
#include <QString>
class ILogDestination {
public:
virtual ~ILogDestination() = default;
virtual void write(const QString& line) = 0;
virtual void flush() = 0;
};
3.2 具体实现:三种日志目的地
cpp
// console_destination.h
#ifndef CONSOLE_DESTINATION_H
#define CONSOLE_DESTINATION_H
#include "ilogdestination.h"
#include <iostream>
class ConsoleDestination : public ILogDestination {
public:
void write(const QString& line) override {
std::cout << line.toStdString() << std::endl;
}
void flush() override {
std::cout.flush();
}
};
#endif // CONSOLE_DESTINATION_H
cpp
// file_destination.h
#ifndef FILE_DESTINATION_H
#define FILE_DESTINATION_H
#include "ilogdestination.h"
#include <QFile>
#include <QTextStream>
class FileDestination : public ILogDestination {
private:
QFile m_file;
QTextStream m_stream;
public:
explicit FileDestination(const QString& path)
: m_file(path), m_stream(&m_file) {
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Text))
throw std::runtime_error("Failed to open log file");
}
void write(const QString& line) override {
m_stream << line << '\n';
}
void flush() override {
m_stream.flush();
}
~FileDestination() override {
m_file.close();
}
};
#endif // FILE_DESTINATION_H
cpp
// debug_output_destination.h
#ifndef DEBUG_OUTPUT_DESTINATION_H
#define DEBUG_OUTPUT_DESTINATION_H
#include "ilogdestination.h"
#include <QDebug>
class DebugOutputDestination : public ILogDestination {
public:
void write(const QString& line) override {
qInfo() << line;
}
void flush() override {
qFlush();
}
};
#endif // DEBUG_OUTPUT_DESTINATION_H
3.3 抽象层:日志格式器(桥接主干)
cpp
// ilogformatter.h
#include <QString>
class ILogFormatter {
public:
virtual ~ILogFormatter() = default;
virtual QString format(const QString& message) = 0;
};
cpp
// simple_formatter.h
#ifndef SIMPLE_FORMATTER_H
#define SIMPLE_FORMATTER_H
#include "ilogformatter.h"
class SimpleFormatter : public ILogFormatter {
public:
QString format(const QString& message) override {
return message;
}
};
#endif // SIMPLE_FORMATTER_H
cpp
// timestamp_formatter.h
#ifndef TIMESTAMP_FORMATTER_H
#define TIMESTAMP_FORMATTER_H
#include "ilogformatter.h"
#include <QDateTime>
class TimestampFormatter : public ILogFormatter {
public:
QString format(const QString& message) override {
auto now = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
return "[" + now + "] " + message;
}
};
#endif // TIMESTAMP_FORMATTER_H
3.4 桥接主类:日志记录器
cpp
// logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include "ilogdestination.h"
#include "ilogformatter.h"
#include <memory>
class Logger {
private:
std::unique_ptr<ILogFormatter> m_formatter;
std::unique_ptr<ILogDestination> m_destination;
public:
explicit Logger(
std::unique_ptr<ILogFormatter> formatter,
std::unique_ptr<ILogDestination> destination)
: m_formatter(std::move(formatter)),
m_destination(std::move(destination)) {}
void log(const QString& message) {
auto formatted = m_formatter->format(message);
m_destination->write(formatted);
}
void flush() {
m_destination->flush();
}
};
#endif // LOGGER_H
3.5 使用示例
cpp
int main() {
// 构建不同组合:格式 × 目的地
auto logger1 = std::make_unique<Logger>(
std::make_unique<SimpleFormatter>(),
std::make_unique<ConsoleDestination>()
);
auto logger2 = std::make_unique<Logger>(
std::make_unique<TimestampFormatter>(),
std::make_unique<FileDestination>("app.log")
);
auto logger3 = std::make_unique<Logger>(
std::make_unique<SimpleFormatter>(),
std::make_unique<DebugOutputDestination>()
);
logger1->log("Hello from console!");
logger2->log("Application started.");
logger3->log("Debug info.");
return 0;
}
✅ 效果:
- 仅需 6 个类(2 个格式器 + 3 个目的地 + 1 个桥接类),即可支持任意组合。
- 若新增一种格式或目的地,只需新增一个类,无需修改已有逻辑。
- 完美解决
N×M类爆炸问题。
四、桥接模式的核心优势
- 解耦两个独立变化维度:形状与颜色、格式与目的地,可分别演化。
- 避免类爆炸 :从
N×M个类 →N + M个类 + 1 个桥接类。 - 支持运行时动态切换实现:可通过配置或注入灵活更换具体实现。
- 符合开闭原则(OCP):新增维度时,无需修改原有代码。
五、进阶技巧:C++ 中的 pImpl 惯用法
在 C++ 中,桥接模式常与 pImpl(Pointer to Implementation) 模式深度融合,实现更彻底的接口与实现分离。
cpp
// logger_pimpl.h
#ifndef LOGGER_PIMPL_H
#define LOGGER_PIMPL_H
#include "ilogformatter.h"
#include "ilogdestination.h"
#include <memory>
class LoggerImpl {
public:
std::unique_ptr<ILogFormatter> formatter;
std::unique_ptr<ILogDestination> destination;
LoggerImpl(std::unique_ptr<ILogFormatter> f,
std::unique_ptr<ILogDestination> d)
: formatter(std::move(f)), destination(std::move(d)) {}
void log(const QString& msg) {
auto formatted = formatter->format(msg);
destination->write(formatted);
}
void flush() {
destination->flush();
}
};
class Logger {
private:
std::unique_ptr<LoggerImpl> m_impl;
public:
explicit Logger(std::unique_ptr<ILogFormatter> formatter,
std::unique_ptr<ILogDestination> destination)
: m_impl(std::make_unique<LoggerImpl>(std::move(formatter),
std::move(destination))) {}
void log(const QString& message) {
m_impl->log(message);
}
void flush() {
m_impl->flush();
}
};
#endif // LOGGER_PIMPL_H
✅ 优势:
- 头文件仅暴露
Logger接口,隐藏内部实现细节。- 编译依赖最小化,提升编译速度。
- 支持细粒度控制内存布局与虚函数表。
- 是 C++ 中实现桥接模式的"标准姿势"。
六、何时该用桥接模式?
✅ 适用场景:
- 存在两个及以上独立变化的维度;
- 维度之间组合方式多,且未来会持续扩展;
- 不希望因组合导致类数量爆炸;
- 需要运行时动态切换实现。
❌ 不推荐使用:
- 维度很少,组合简单(如只有 2~3 种);
- 仅一次性的功能,无扩展需求;
- 业务逻辑高度耦合,难以拆分。
七、小结
📌 桥接模式的本质 :
将"继承树 "转化为"组合网",让两个独立变化的维度各自独立演化,彻底告别类爆炸。
🔑 关键思想 :用"组合"代替"继承",用"接口指针"作为"桥梁",打通抽象与实现之间的壁垒。
🧩 典型应用场景:
- 图形系统(形状 × 颜色)
- 跨平台开发(抽象接口 × 平台实现)
- 日志系统(格式 × 输出目标)
- 设备驱动(协议 × 硬件)
🚀 建议 :在设计阶段就考虑桥接模式,尤其面对复杂维度组合时,提前规划能极大降低后期重构成本。
下一篇预告:(7) 策略模式 / 观察者模式 ------ 动态行为切换与事件通知机制,敬请期待!