Qt5 进阶【10】应用架构与插件化设计实战:从「单体窗口」走向「可扩展框架」

目标读者:已经看完前 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 本身关系不大,更大程度上是架构 的问题:

我们缺少一套清晰、可落地的应用结构,把「核心能力」和「业务扩展」分开。

这一期我们就用一篇文章,配一个可以跑起来的工程,解决下面三个问题:

  1. 应用内的模块应该怎么划分?​
  2. 模块之间如何通过接口/服务通信,而不是互相 include?​
  3. 如何为以后做插件化、热插拔预留空间?​

二、核心思路:用「服务接口 + 依赖注入 + 插件扩展」搭骨架

为了避免空谈,这里先把这期要用到的几个设计思路讲清楚,再用代码演示。

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 &params = {}) = 0;

    virtual QList<QVariantMap> query(const QString &sql,
                                     const QVariantMap &params = {}) = 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;
};

构造时:

  • 创建 FileLoggerConsoleLogger
  • 创建 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_METADATAQ_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 层只依赖 IAppContextIUserService,而不关心具体实现。

这就是一个可扩展应用骨架的最小闭环版本。


四、实战中的坑与优化:从 Demo 走向真实项目

结合上面的代码结构,再补充几点工程实践中的经验。

1. 不要让 UI 层直接接触 QSqlQuery / QFile

所有对数据库、文件、网络的直接操作,都放在服务层 (如 IDatabase 实现)里。

UI 层只关心「我要什么」,不关心「怎么拿」。

2. 接口命名与粒度

  • 接口名建议统一以 I 开头,如 ILoggerIDatabaseIUserService
  • 接口方法尽量语义化:addUser/removeUser/allUsers()
  • 避免在接口中暴露过多与底层强相关的方法(比如直接把 QSqlDatabase 泄露出去)。

3. 生命周期与所有权

  • AppContext 负责创建和销毁所有服务(ILoggerIDatabaseIUserService);
  • UI 层不持有它们的所有权,只保存裸指针引用;
  • 将来如果引入依赖注入容器,可以以 QSharedPointer 或智能指针来管理服务生命周期。

4. 为插件化预留接口

虽然这期没有真正实现 QPluginLoader,但已经具备:

  • 清晰的 IAppContext
  • 独立的服务接口。

以后你只需要再定义一个 IPlugin 接口 + 一个插件加载器,就可以把某些功能搬到单独的动态库中。

5. 单元测试友好性

因为服务都通过接口暴露,写单元测试时可以轻松构造假的实现

cpp 复制代码
class FakeDatabase : public IDatabase {
    // 内存数组模拟数据库
};

class FakeLogger : public ILogger {
    // 把日志存到 QStringList,测试时断言内容
};

然后把这些 Fake 实现注入到 UserServiceImpl,在不启动 Qt GUI 的情况下单独测试业务逻辑。


五、小结:第 10 期的实践守则

如果要把本期内容归纳成几条可以立即用在项目里的「架构规则」,我会这样写:

  1. 任何可被多个模块复用的能力,都先定义接口,再写实现

    • 日志:ILogger
    • 数据库:IDatabase
    • 用户/订单等业务:IUserServiceIOrderService 等。
  2. 应用层(AppContext)负责组装所有服务

    • 构造函数里 new 实现;
    • 析构时统一 delete;
    • 提供 logger()/database()/userService() 这样的访问方法。
  3. UI 层只依赖接口,不关心实现

    • 通过构造函数把 IAppContext* 传给 MainWindow;
    • 所有业务调用都走 ctx->userService() 等接口方法。
  4. 模块之间严禁直接 include 对方实现类头文件

    • 只能 include 对方的接口头文件;
    • 如果发现 A 模块 include 了 B 模块的实现类,说明架构有味道了,要重构。
  5. 为将来的插件机制保留扩展点

    • 定义好 IAppContext,把主应用对插件开放的能力写清楚;
    • 将来增加 IPlugin 接口,通过 QPluginLoader 加载。

你可以先把这套 ArchDemo10 跑起来,把它当成一个「模板工程」来复用:

以后新项目直接复制这套结构,把第 8 期的数据库访问、第 9 期的模型视图、第 7 期的网络请求都接到这个架构上去,长线维护会轻松很多。

这一期写完,你的 Qt 项目基本已经从「能跑」升级到了「能长期演进」。

相关推荐
瓦特what?1 小时前
C++编程防坑指南(小说版)
android·c++·kotlin
sjjhd6521 小时前
C++模拟器开发实践
开发语言·c++·算法
七夜zippoe1 小时前
大模型低成本高性能演进 从GPT到DeepSeek的技术实战手记
人工智能·gpt·算法·架构·deepseek
曹天骄1 小时前
Cloudflare CDN 预热全面实战指南(含全球 PoP 解析 + 预热覆盖模型)
运维·开发语言·缓存
Queenie_Charlie2 小时前
素数(线性筛法)
c++·线性筛法·质数·简单数论
Paraverse_徐志斌2 小时前
针对 SAAS 私有化部署,如何优雅合并微服务
java·微服务·架构·saas·私有化
csbysj20202 小时前
传输对象模式(Object Transfer Pattern)
开发语言
zandy10112 小时前
衡石科技实践:如何基于统一指标平台,实现从传统BI到Agentic BI的架构演进
科技·架构
步达硬件2 小时前
【Matlab】把视频里每一帧存为单独的图片
开发语言·matlab·音视频