Qt 设计模式:模板方法模式
系列:Qt / VTK 设计模式 · Qt 05/11
参考:Qt 6 QThread、The Event System
引子
写一个自定义 QThread 线程类时,你只需要在子类中重写 run() 方法;至于底层的操作系统线程创建、优先级设置、信号发射以及清理工作,全由基类 QThread 事先编排妥当——你只需要填入“可变的那一步”。
这就是模板方法模式(Template Method Pattern):定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构,即可重定义该算法的某些特定步骤。在 Qt 中,这一模式被广泛应用于线程入口、事件分发管线以及绘制流程中。
核心痛点与 GoF 结构
痛点场景:算法骨架重复与漏步骤
cpp
// 坏方案:每个线程实现类都重复编写线程初始化与清理骨架
void workerThreadA() {
setupOSThread(); // 重复的底管代码
doTaskA(); // 业务代码
cleanupThread(); // 容易遗漏的清理代码
}- 代码大量冗余:相同的流程框架被散落在各个派生类中。
- 一致性难保障:容易遗漏关键的初始化或清理步骤(产生资源泄漏)。
GoF 模板方法结构映射
| GoF 抽象角色 | Qt 对应实现 | 职责说明 |
|---|---|---|
| 抽象类 (AbstractClass) | QThread / QWidget / QAbstractItemModel | 定义算法骨架方法 (start(), event()) 并声明虚钩子函数 |
| 模板方法 (TemplateMethod) | QThread::start() / QWidget::event() | 编排固定的算法流程与调用顺序 |
| 原语操作 (PrimitiveOperation) | QThread::run() / QWidget::paintEvent() | 由子类重写的高精细度定制步骤(钩子函数) |
底层原理:好莱坞原则 (Don't call us, we'll call you)
模板方法模式的核心思想是控制反转——好莱坞原则(由基类掌控流程并反向调用子类):
对于事件分发,QWidget::event() 同样充当了模板方法骨架,根据事件类型分发给相应的钩子:
完整代码示例
1. QThread::run() 自定义线程执行体
cpp
#include <QCoreApplication>
#include <QThread>
#include <QDebug>
// 子类只需重写可变的 run() 方法
class DataProcessingThread : public QThread {
Q_OBJECT
public:
explicit DataProcessingThread(QString taskName) : m_taskName(std::move(taskName)) {}
protected:
// 模板方法中的原语操作 (Primitive Operation)
void run() override {
qDebug() << "Thread Start Processing Task:" << m_taskName << "on Thread ID:" << QThread::currentThreadId();
for (int i = 0; i < 3; ++i) {
QThread::msleep(100);
qDebug() << m_taskName << "Progress:" << (i + 1) * 33 << "%";
}
qDebug() << "Task Finished:" << m_taskName;
}
private:
QString m_taskName;
};
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
DataProcessingThread thread("Point Cloud Calculation");
// 关键动作:调用基类的模板方法 start(),而非直接调用 run()
thread.start();
// 等待线程执行完成
thread.wait();
return 0;
}
#include "main.moc"易混淆模式对比
| 设计模式 | 扩展机制 | 核心区别 |
|---|---|---|
| 模板方法模式 (Template Method) | 继承机制(编译期确定骨架) | 基类写死算法逻辑框架,子类重写局部虚函数步骤 |
| 策略模式 (Strategy) | 组合机制(运行期注入对象) | 客户端通过持有抽象接口指针,在运行时整体替换算法 |
最佳实践与踩坑避坑
- 绝对不要在业务中重写
QThread::start():start()是固定流程的模板方法(负责初始化线程环境)。如果覆盖重写了start(),原生线程将无法正常启动。
- 现代 Qt 线程使用建议:
- 尽管
QThread::run()是模板方法的经典体现,但在现代 Qt 开发中,处理轻量任务更推荐使用QObject::moveToThread()或QtConcurrent/QThreadPool。
- 尽管
paintEvent中的父类调用:- 在重写
QWidget::paintEvent()编写自定义控件时,如果需要保留父控件的默认背景或阴影绘制,务必在最开头显式调用QWidget::paintEvent(e)。
- 在重写
重点与注意
重点:模板方法 = 基类固定算法骨架,子类填充特定步骤(通过重写虚函数)。
重点:QThread::start()是模板方法,QThread::run()是供子类实现的钩子。
注意:模板方法基于类继承;策略模式基于接口组合。多用组合少用继承,但在框架底层设计中模板方法极其实用。
小结
Qt 模板方法模式隐藏了繁琐的系统底层初始化逻辑,让开发者只需专注实现核心业务切面。
延伸阅读