Qt 设计模式:策略模式
系列:Qt / VTK 设计模式 · Qt 04/11
参考:Qt 6 QIODevice、QAbstractItemModel、QStyle
引子
同段业务代码,一会儿要从磁盘文件读取,一会儿要从 TCP 网络 Socket 读取,一会儿又要从内存 QBuffer 中读取——如果直接写满 if-else 分支,代码将变得极其臃肿。
解决此问题的经典方案是策略模式(Strategy Pattern):定义一族算法/行为,将它们封装起来,并使它们可以相互替换。在 Qt 中,策略模式普遍体现为“抽象基类定义统一接口 + 运行时动态注入实现”,做到了业务调用方与底层算法的具体实现解耦。
核心痛点与 GoF 策略结构
痛点场景:大量条件分支的硬编码
// 坏方案:直接根据类型分支调用具体读写函数
void processData(int type, const QByteArray& input) {
if (type == 1) {
// 处理文件 IO 细节
} else if (type == 2) {
// 处理 Socket 网络 IO 细节
} else if (type == 3) {
// 处理内存 Buffer 细节
}
}- 违反开闭原则(OCP):每当新增一种数据源或计算算法,都必须修改核心控制逻辑。
- 高耦合难测试:无法通过 Mock 对象单独测试核心业务逻辑。
GoF 策略模式结构映射
| GoF 策略角色 | Qt 对应实现 | 职责说明 |
|---|---|---|
| 抽象策略 (Strategy) | QIODevice / QAbstractItemModel / QStyle | 定义统一的抽象操作接口(如 readData(), data(), drawControl()) |
| 具体策略 (ConcreteStrategy) | QFile, QTcpSocket, QBuffer / QFusionStyle | 实现具体的算法、IO 读写逻辑或界面绘制逻辑 |
| 上下文 (Context) | 持有 QIODevice* 或 QStyle* 的应用层代码 | 维护策略对象的引用,通过抽象接口调用策略功能 |
底层原理与策略切换流向
策略模式的核心是依赖倒置原则(DIP):高层业务逻辑只依赖抽象接口,不依赖具体的子类实现。
Qt 策略体系经典落点
1. QIODevice 体系(IO 策略)
QIODevice 抽象基类定义了 read(), write(), seek() 接口:
QFile:磁盘文件读写策略。QTcpSocket/QUdpSocket:网络套接字读写策略。QBuffer:内存字节数组读写策略。
调用方(如 QTextStream 或 QDataStream)只需接收 QIODevice* 指针,即可无缝切换策略。
2. QStyle 体系(GUI 控件绘制策略)
Qt Widgets 界面控件的绘制行为完全被抽象到了 QStyle 策略类中:
- 通过
QApplication::setStyle(QStyleFactory::create("Fusion"))可以在运行时动态更换整个应用的渲染风格,而无需动任何QPushButton或QSlider控件源码。
3. QAbstractItemModel(数据获取策略)
QListView 或 QTableView 视图组件只依赖 QAbstractItemModel* 接口获取数据:
- 运行时可以在
QSqlTableModel(数据库策略)、QStandardItemModel(内存节点策略)之间无缝切换。
完整代码示例
1. 基于 QIODevice 的可替换数据处理
#include <QCoreApplication>
#include <QFile>
#include <QBuffer>
#include <QDebug>
// 业务上下文函数:只依赖抽象策略 QIODevice*
void parseStreamData(QIODevice* device) {
if (!device->isOpen() && !device->open(QIODevice::ReadOnly)) {
qWarning() << "Failed to open device";
return;
}
while (!device->atEnd()) {
QByteArray line = device->readLine();
qDebug() << "Parsed Line:" << line.trimmed();
}
}
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
// 策略 1:内存数据策略
QBuffer memoryBuffer;
memoryBuffer.setData("Line1: Memory\nLine2: Buffer\n");
parseStreamData(&memoryBuffer);
// 策略 2:文件数据策略(完全无缝替换)
QFile file("test.txt");
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
file.write("Line1: File\nLine2: Disk\n");
file.close();
}
parseStreamData(&file);
return 0;
}2. Modern C++ 函数对象 / Lambda 策略
在 C++11 后,对于轻量级算法策略,通常不需要创建复杂的类继承体系,直接使用 std::function 注入策略即可:
#include <QList>
#include <algorithm>
#include <functional>
#include <QDebug>
using SortStrategy = std::function<bool(int, int)>;
void customSort(QList<int>& data, SortStrategy strategy) {
std::sort(data.begin(), data.end(), strategy);
}
// 使用策略
QList<int> numbers = { 5, 2, 8, 1, 9 };
// 升序策略
customSort(numbers, [](int a, int b) { return a < b; });
// 降序策略
customSort(numbers, [](int a, int b) { return a > b; });易混淆模式对比
| 设计模式 | 核心目的 | 结构区别 |
|---|---|---|
| 策略模式 (Strategy) | 算法互换:在运行期由客户端指定具体的实现算法 | 策略类平级,通常没有主从层次关系 |
| 状态模式 (State) | 状态驱动行为:由对象内部状态的改变自动切换行为 | 状态类内部保存转换逻辑,客户端无需感知 |
| 模板方法模式 (Template Method) | 固定骨架:由父类定义步骤顺序,子类填空 | 基于继承机制(编译期多态),策略基于组合(运行期多态) |
最佳实践与踩坑避坑
- 优先依赖抽象接口:业务代码中应传递
QIODevice*或QAbstractItemModel*,避免直接绑定QFile*或QStandardItemModel*。 - 策略对象的生命周期控制:确保 Context 在调用策略时,策略对象未被提前销毁。如果策略由外部传入,应明确所有权(如通过
std::unique_ptr或QObject父子树管理)。 - 单元测试中的 Mock 优势:测试需要读取网络/文件的复杂业务逻辑时,直接注入
QBuffer策略,无需搭建真实的服务器或创建物理磁盘文件。
重点与注意
重点:策略模式的核心是 “面向接口编程、运行期自由替换”;Qt 中的
QIODevice和QAbstractItemModel是策略模式的典范。
重点:在 Modern C++ 中,std::function与 Lambda 表达式是轻量策略调度的首选。
注意:策略模式适合算法独立、平级可替换的场景;如果行为随内部状态流转而自动变化,应选 状态模式。
小结
Qt 策略模式打破了繁重条件分支的枷锁,使数据读取、格式渲染与排序算法能够弹性扩展。
延伸阅读