目标读者:已经看完前 1--9 期,对信号槽、对象生命周期、事件循环、元对象、多线程、文件/网络/数据库、模型视图都有一定实践经验,但在项目整体架构上还停留在「一个大 MainWindow + 一堆槽函数」阶段的 Qt/C++ 工程师。
开发环境示例:Qt 5.12/5.15 + Qt Creator + CMake;操作系统不限(Windows / Linux / macOS)。
目录
[一、问题背景:功能越加越多,「MainWindow 神类」越来越可怕](#一、问题背景:功能越加越多,「MainWindow 神类」越来越可怕)
[1. MainWindow 神类:上万行代码,谁都不敢动](#1. MainWindow 神类:上万行代码,谁都不敢动)
[2. 模块之间强耦合:改一个小功能,需要全工程搜一遍](#2. 模块之间强耦合:改一个小功能,需要全工程搜一遍)
[3. 无法扩展:新需求总是「硬塞」](#3. 无法扩展:新需求总是「硬塞」)
[二、核心思路:用「服务接口 + 依赖注入 + 插件扩展」搭骨架](#二、核心思路:用「服务接口 + 依赖注入 + 插件扩展」搭骨架)
[1. 全局入口:IAppContext(应用上下文接口)](#1. 全局入口:IAppContext(应用上下文接口))
[2. 基础服务接口:ILogger / IDatabase / IUserService](#2. 基础服务接口:ILogger / IDatabase / IUserService)
[3. 依赖注入:AppContext 负责组合一切](#3. 依赖注入:AppContext 负责组合一切)
[4. 插件扩展:IPlugin 接口 + Qt 插件机制](#4. 插件扩展:IPlugin 接口 + Qt 插件机制)
[1. 目录结构](#1. 目录结构)
[2. 顶层 CMakeLists.txt](#2. 顶层 CMakeLists.txt)
[3. 应用上下文:appcontext.h / appcontext.cpp](#3. 应用上下文:appcontext.h / appcontext.cpp)
[4. 日志模块:ilogger.h / logger_console.h / logger_console.cpp](#4. 日志模块:ilogger.h / logger_console.h / logger_console.cpp)
[5. 数据库模块:idatabase.h / database_sqlite.h / database_sqlite.cpp](#5. 数据库模块:idatabase.h / database_sqlite.h / database_sqlite.cpp)
[6. 用户服务模块:iuserservice.h / userservice_impl.h / userservice_impl.cpp](#6. 用户服务模块:iuserservice.h / userservice_impl.h / userservice_impl.cpp)
[7. 主窗口:mainwindow.h / mainwindow.cpp](#7. 主窗口:mainwindow.h / mainwindow.cpp)
[8. 程序入口:main.cpp](#8. 程序入口:main.cpp)
[四、实战中的坑与优化:从 Demo 走向真实项目](#四、实战中的坑与优化:从 Demo 走向真实项目)
[1. 不要让 UI 层直接接触 QSqlQuery / QFile](#1. 不要让 UI 层直接接触 QSqlQuery / QFile)
[2. 接口命名与粒度](#2. 接口命名与粒度)
[3. 生命周期与所有权](#3. 生命周期与所有权)
[4. 为插件化预留接口](#4. 为插件化预留接口)
[5. 单元测试友好性](#5. 单元测试友好性)
[五、小结:第 10 期的实践守则](#五、小结:第 10 期的实践守则)
一、问题背景:功能越加越多,「MainWindow 神类」越来越可怕
写 Qt 程序很容易写成下面这个样子:
- 整个项目只有一个
MainWindow; - 所有逻辑都堆在这个类里;
- UI 控件通过
ui->xxx到处被访问、修改; - 网络、数据库、配置、业务都揉在一起。
短期内看起来「能用就行」,长期维护时就会暴露出典型的架构问题:
1. MainWindow 神类:上万行代码,谁都不敢动
典型症状:
mainwindow.h/.cpp两个文件加起来几千甚至上万行;- 每次新功能都是「在这上面再加一点」;
- 任何小改动,都可能拖出一大串连锁反应。
2. 模块之间强耦合:改一个小功能,需要全工程搜一遍
- 用户管理模块直接访问订单模块的内部数据结构;
- 报表模块自己去 new 网络客户端、new 数据库连接;
- 配置模块到处被静态访问,完全没有边界。
这种结构的典型特征就是:一旦一个地方发生变动,其他地方必然要改,开发成本呈指数级上升。
3. 无法扩展:新需求总是「硬塞」
- 想支持插件:没抽象接口,所有东西都绑死在一个 exe 里;
- 想做脚本扩展:没有稳定 API,只能用「半自动代码生成」凑合;
- 想做自动化测试:UI 和业务逻辑紧密耦合,几乎没法单测。
讲到底,这些问题和 Qt 本身关系不大,更大程度上是架构 的问题:
我们缺少一套清晰、可落地的应用结构,把「核心能力」和「业务扩展」分开。
这一期我们就用一篇文章,配一个可以跑起来的工程,解决下面三个问题:
- 应用内的模块应该怎么划分?
- 模块之间如何通过接口/服务通信,而不是互相 include?
- 如何为以后做插件化、热插拔预留空间?
二、核心思路:用「服务接口 + 依赖注入 + 插件扩展」搭骨架
为了避免空谈,这里先把这期要用到的几个设计思路讲清楚,再用代码演示。
1. 全局入口:IAppContext(应用上下文接口)
和直接乱用全局单例不同,我们定义一个应用上下文接口:
cpp
class ILogger;
class IDatabase;
class IUserService;
class IAppContext
{
public:
virtual ~IAppContext() = default;
virtual ILogger* logger() const = 0;
virtual IDatabase* database() const = 0;
virtual IUserService* userService() const = 0;
// 后面可以继续加其他全局服务获取方法
};
- 应用层(Main 应用)负责提供这个接口的实现;
- 业务模块只依赖这个接口,而不是依赖 MainWindow 或某个具体类;
- 将来如果你要改日志实现、改数据库实现,只要在应用层替换,不动业务。
2. 基础服务接口:ILogger / IDatabase / IUserService
每个核心能力都先定义一个接口:
ILogger:日志服务IDatabase:数据库访问服务(对上层隐藏 QSqlDatabase)IUserService:用户领域服务(隐藏具体持久化细节)
比如日志接口:
cpp
class ILogger
{
public:
enum Level {
Debug,
Info,
Warn,
Error
};
virtual ~ILogger() = default;
virtual void log(Level level, const QString &msg) = 0;
virtual void setLevel(Level level) = 0;
};
数据库接口:
cpp
class IDatabase
{
public:
virtual ~IDatabase() = default;
virtual bool exec(const QString &sql,
const QVariantMap ¶ms = {}) = 0;
virtual QList<QVariantMap> query(const QString &sql,
const QVariantMap ¶ms = {}) = 0;
};
用户服务接口:
cpp
class IUserService
{
public:
virtual ~IUserService() = default;
virtual QList<QVariantMap> allUsers() = 0;
virtual bool addUser(const QString &name, const QString &email) = 0;
virtual bool removeUser(int id) = 0;
};
关键点:所有业务代码只和接口打交道,而接口的实现可以按需替换(比如切换到网络 API、测试桩、Mock 等)。
3. 依赖注入:AppContext 负责组合一切
我们在应用层提供 AppContext 的一个实现类,把所有服务的真实实现放到这里组装:
cpp
class AppContext : public IAppContext
{
public:
AppContext();
~AppContext() override;
ILogger* logger() const override { return m_logger; }
IDatabase* database() const override { return m_database; }
IUserService* userService() const override { return m_userService; }
private:
ILogger *m_logger = nullptr;
IDatabase *m_database = nullptr;
IUserService *m_userService = nullptr;
};
构造时:
- 创建
FileLogger或ConsoleLogger; - 创建
SqliteDatabase; - 创建
UserServiceImpl,并把IDatabase*注入进去。
外部模块获取这些服务时只需要一个 IAppContext*,而不关心实现细节。
4. 插件扩展:IPlugin 接口 + Qt 插件机制
简单版本的插件接口可以这样定义:
cpp
class IPlugin
{
public:
virtual ~IPlugin() = default;
virtual QString id() const = 0;
virtual QString name() const = 0;
virtual void initialize(IAppContext *ctx) = 0;
};
插件实现类需要:
- 继承 QObject 和 IPlugin;
- 使用
Q_PLUGIN_METADATA和Q_INTERFACES宏导出; - 在
initialize里通过ctx获取服务。
主应用通过 QPluginLoader 在运行时动态加载:
cpp
QPluginLoader loader(path);
QObject *obj = loader.instance();
if (auto plugin = qobject_cast<IPlugin*>(obj)) {
plugin->initialize(appContext);
}
通过这种方式,我们可以在不修改主程序的前提下,为系统追加新模块(报表、统计、诊断工具等)。
三、代码实战:用一个小项目走完整套架构
下面是一个完整的示例工程,项目名叫 ArchDemo10 ,你可以直接创建一个空的 CMake 项目,把这些文件按结构复制进去,然后使用 Qt Creator 打开 CMakeLists.txt 构建运行。
1. 目录结构
cpp
ArchDemo10/
├── CMakeLists.txt
├── include/
│ ├── appcontext.h // IAppContext + AppContext 实现
│ ├── ilogger.h // 日志接口
│ ├── logger_console.h // 控制台日志实现
│ ├── idatabase.h // 数据库接口
│ ├── database_sqlite.h // SQLite 实现
│ ├── iuserservice.h // 用户服务接口
│ ├── userservice_impl.h // 用户服务实现
│ └── mainwindow.h // 主窗口
└── src/
├── main.cpp
├── appcontext.cpp
├── logger_console.cpp
├── database_sqlite.cpp
├── userservice_impl.cpp
└── mainwindow.cpp
本文先重点演示整体架构和模块划分 ,插件机制留个扩展点,避免第一次上手信息量过大。将来你可以在此基础上单独建 plugins/ 目录扩展。
2. 顶层 CMakeLists.txt
3. 应用上下文:appcontext.h / appcontext.cpp
include/appcontext.h
cpp
#ifndef APPCONTEXT_H
#define APPCONTEXT_H
#include <QString>
class ILogger;
class IDatabase;
class IUserService;
/**
* @brief IAppContext
* 应用上下文接口,对外提供统一的服务获取入口
*/
class IAppContext
{
public:
virtual ~IAppContext() = default;
virtual ILogger* logger() const = 0;
virtual IDatabase* database() const = 0;
virtual IUserService* userService() const = 0;
};
/**
* @brief AppContext
* 应用上下文实现类,负责组装各模块
*/
class AppContext : public IAppContext
{
public:
AppContext();
~AppContext() override;
ILogger* logger() const override;
IDatabase* database() const override;
IUserService* userService() const override;
private:
ILogger* m_logger = nullptr;
IDatabase* m_database = nullptr;
IUserService* m_userService = nullptr;
};
#endif // APPCONTEXT_H
src/appcontext.cpp
cpp
#include "appcontext.h"
#include "ilogger.h"
#include "logger_console.h"
#include "idatabase.h"
#include "database_sqlite.h"
#include "iuserservice.h"
#include "userservice_impl.h"
#include <QStandardPaths>
#include <QDir>
AppContext::AppContext()
{
// 1. 创建日志
m_logger = new ConsoleLogger;
m_logger->setLevel(ILogger::Info);
// 2. 创建数据库
QString dbDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(dbDir);
if (!dir.exists()) {
dir.mkpath(".");
}
QString dbPath = dir.filePath("archdemo10.db");
m_database = new SqliteDatabase(dbPath);
if (!m_database->isOpen()) {
m_logger->log(ILogger::Error, QStringLiteral("数据库打开失败:%1").arg(dbPath));
} else {
m_logger->log(ILogger::Info, QStringLiteral("数据库已打开:%1").arg(dbPath));
}
// 3. 创建用户服务
m_userService = new UserServiceImpl(m_database, m_logger);
}
AppContext::~AppContext()
{
delete m_userService;
delete m_database;
delete m_logger;
}
ILogger* AppContext::logger() const
{
return m_logger;
}
IDatabase* AppContext::database() const
{
return m_database;
}
IUserService* AppContext::userService() const
{
return m_userService;
}
4. 日志模块:ilogger.h / logger_console.h / logger_console.cpp
include/ilogger.h
include/logger_console.h
src/logger_console.cpp
5. 数据库模块:idatabase.h / database_sqlite.h / database_sqlite.cpp
include/idatabase.h
include/database_sqlite.h
src/database_sqlite.cpp
6. 用户服务模块:iuserservice.h / userservice_impl.h / userservice_impl.cpp
include/iuserservice.h
include/userservice_impl.h
src/userservice_impl.cpp
完整代码可以找我要,这里不再粘贴!后期整个主题完成后会上传到资源。
7. 主窗口:mainwindow.h / mainwindow.cpp
include/mainwindow.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class IAppContext;
class QTableWidget;
class QLineEdit;
/**
* @brief MainWindow
* 主窗口:只做 UI 和简单交互,业务全部走 UserService
*/
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(IAppContext *ctx, QWidget *parent = nullptr);
~MainWindow() override;
private slots:
void onAddUser();
void onRemoveUser();
void onRefresh();
private:
void setupUi();
void loadUsers();
private:
IAppContext *m_ctx = nullptr;
QTableWidget *m_table = nullptr;
QLineEdit *m_filterEdit = nullptr;
};
#endif // MAINWINDOW_H
src/mainwindow.cpp
cpp
#include "mainwindow.h"
#include "appcontext.h"
#include "iuserservice.h"
#include "ilogger.h"
#include <QTableWidget>
#include <QHeaderView>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QMessageBox>
MainWindow::MainWindow(IAppContext *ctx, QWidget *parent)
: QMainWindow(parent),
m_ctx(ctx)
{
setWindowTitle(QStringLiteral("ArchDemo10 - 应用架构示例"));
resize(800, 500);
setupUi();
loadUsers();
}
MainWindow::~MainWindow()
{
}
void MainWindow::setupUi()
{
auto *central = new QWidget(this);
auto *mainLayout = new QVBoxLayout(central);
// 顶部搜索 + 按钮
auto *topLayout = new QHBoxLayout();
m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(QStringLiteral("暂不实现过滤,这里留个位置"));
auto *addBtn = new QPushButton(QStringLiteral("添加用户"), this);
auto *removeBtn = new QPushButton(QStringLiteral("删除选中"), this);
auto *refreshBtn = new QPushButton(QStringLiteral("刷新"), this);
topLayout->addWidget(m_filterEdit, 1);
topLayout->addWidget(addBtn);
topLayout->addWidget(removeBtn);
topLayout->addWidget(refreshBtn);
// 表格
m_table = new QTableWidget(this);
m_table->setColumnCount(4);
QStringList headers;
headers << "ID" << "用户名" << "邮箱" << "创建时间";
m_table->setHorizontalHeaderLabels(headers);
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
mainLayout->addLayout(topLayout);
mainLayout->addWidget(m_table, 1);
setCentralWidget(central);
// 信号槽
connect(addBtn, &QPushButton::clicked,
this, &MainWindow::onAddUser);
connect(removeBtn, &QPushButton::clicked,
this, &MainWindow::onRemoveUser);
connect(refreshBtn, &QPushButton::clicked,
this, &MainWindow::onRefresh);
}
void MainWindow::loadUsers()
{
if (!m_ctx || !m_ctx->userService()) return;
auto users = m_ctx->userService()->allUsers();
m_table->setRowCount(users.size());
for (int i = 0; i < users.size(); ++i) {
const auto &u = users.at(i);
m_table->setItem(i, 0, new QTableWidgetItem(QString::number(u.value("id").toInt())));
m_table->setItem(i, 1, new QTableWidgetItem(u.value("username").toString()));
m_table->setItem(i, 2, new QTableWidgetItem(u.value("email").toString()));
m_table->setItem(i, 3, new QTableWidgetItem(u.value("created_at").toString()));
}
}
void MainWindow::onAddUser()
{
bool ok = false;
QString name = QInputDialog::getText(this, QStringLiteral("添加用户"),
QStringLiteral("用户名:"),
QLineEdit::Normal, "user1", &ok);
if (!ok || name.trimmed().isEmpty()) return;
QString email = QInputDialog::getText(this, QStringLiteral("添加用户"),
QStringLiteral("邮箱:"),
QLineEdit::Normal, "user1@example.com", &ok);
if (!ok || email.trimmed().isEmpty()) return;
if (!m_ctx || !m_ctx->userService()) return;
bool res = m_ctx->userService()->addUser(name.trimmed(), email.trimmed());
if (!res) {
QMessageBox::warning(this, QStringLiteral("失败"), QStringLiteral("添加用户失败"));
}
loadUsers();
}
void MainWindow::onRemoveUser()
{
auto idx = m_table->currentIndex();
if (!idx.isValid()) {
QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("请先选中要删除的用户"));
return;
}
int row = idx.row();
int id = m_table->item(row, 0)->text().toInt();
QString name = m_table->item(row, 1)->text();
auto ret = QMessageBox::question(
this,
QStringLiteral("确认删除"),
QStringLiteral("是否删除用户 %1 (ID=%2) ?").arg(name).arg(id)
);
if (ret != QMessageBox::Yes) return;
if (!m_ctx || !m_ctx->userService()) return;
bool ok = m_ctx->userService()->removeUser(id);
if (!ok) {
QMessageBox::warning(this, QStringLiteral("失败"), QStringLiteral("删除失败"));
}
loadUsers();
}
void MainWindow::onRefresh()
{
loadUsers();
}
8. 程序入口:main.cpp
src/main.cpp
cpp
#include "appcontext.h"
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
a.setApplicationName("ArchDemo10");
a.setOrganizationName("QtAdvancedSeries");
a.setStyle("Fusion");
AppContext ctx;
MainWindow w(&ctx);
w.show();
return a.exec();
}
编译运行之后,你会看到一个简单的用户管理界面,但它已经具备了:
- 独立的日志服务;
- 独立的数据库服务;
- 独立的用户服务(业务层);
- UI 层只依赖
IAppContext和IUserService,而不关心具体实现。
这就是一个可扩展应用骨架的最小闭环版本。
四、实战中的坑与优化:从 Demo 走向真实项目
结合上面的代码结构,再补充几点工程实践中的经验。
1. 不要让 UI 层直接接触 QSqlQuery / QFile
所有对数据库、文件、网络的直接操作,都放在服务层 (如 IDatabase 实现)里。
UI 层只关心「我要什么」,不关心「怎么拿」。
2. 接口命名与粒度
- 接口名建议统一以
I开头,如ILogger、IDatabase、IUserService; - 接口方法尽量语义化:
addUser/removeUser/allUsers(); - 避免在接口中暴露过多与底层强相关的方法(比如直接把
QSqlDatabase泄露出去)。
3. 生命周期与所有权
AppContext负责创建和销毁所有服务(ILogger、IDatabase、IUserService);- UI 层不持有它们的所有权,只保存裸指针引用;
- 将来如果引入依赖注入容器,可以以
QSharedPointer或智能指针来管理服务生命周期。
4. 为插件化预留接口
虽然这期没有真正实现 QPluginLoader,但已经具备:
- 清晰的
IAppContext; - 独立的服务接口。
以后你只需要再定义一个 IPlugin 接口 + 一个插件加载器,就可以把某些功能搬到单独的动态库中。
5. 单元测试友好性
因为服务都通过接口暴露,写单元测试时可以轻松构造假的实现:
cpp
class FakeDatabase : public IDatabase {
// 内存数组模拟数据库
};
class FakeLogger : public ILogger {
// 把日志存到 QStringList,测试时断言内容
};
然后把这些 Fake 实现注入到 UserServiceImpl,在不启动 Qt GUI 的情况下单独测试业务逻辑。
五、小结:第 10 期的实践守则
如果要把本期内容归纳成几条可以立即用在项目里的「架构规则」,我会这样写:
-
任何可被多个模块复用的能力,都先定义接口,再写实现
- 日志:
ILogger - 数据库:
IDatabase - 用户/订单等业务:
IUserService、IOrderService等。
- 日志:
-
应用层(AppContext)负责组装所有服务
- 构造函数里 new 实现;
- 析构时统一 delete;
- 提供
logger()/database()/userService()这样的访问方法。
-
UI 层只依赖接口,不关心实现
- 通过构造函数把
IAppContext*传给 MainWindow; - 所有业务调用都走
ctx->userService()等接口方法。
- 通过构造函数把
-
模块之间严禁直接 include 对方实现类头文件
- 只能 include 对方的接口头文件;
- 如果发现 A 模块 include 了 B 模块的实现类,说明架构有味道了,要重构。
-
为将来的插件机制保留扩展点
- 定义好
IAppContext,把主应用对插件开放的能力写清楚; - 将来增加
IPlugin接口,通过QPluginLoader加载。
- 定义好
你可以先把这套 ArchDemo10 跑起来,把它当成一个「模板工程」来复用:
以后新项目直接复制这套结构,把第 8 期的数据库访问、第 9 期的模型视图、第 7 期的网络请求都接到这个架构上去,长线维护会轻松很多。
这一期写完,你的 Qt 项目基本已经从「能跑」升级到了「能长期演进」。