用分层架构打造单体应用
在桌面端、工业控制、嵌入式 HMI 等领域,Qt/C++ 单体应用依然是主流。但很多开发者一听到“单体”,脑海中浮现的就是“大泥球”——代码耦合严重、牵一发而动全身、新人接手如读天书。其实,单体 ≠ 混乱。今天,我们就来揭秘如何用经典的 分层架构(Layered Architecture),为你的 Qt/C++ 项目打下坚实、清晰、易维护的根基。
构建一个结构良好、易于维护的 Qt/C++ 单体应用,关键在于良好的架构设计、清晰的代码组织、有效的解耦和一致的编码规范。
一、为什么选择分层架构?
在微服务大行其道的今天,分层架构依然是单体应用的黄金标准,尤其适合:
- 中小型桌面应用程序
- 对启动速度和资源占用敏感的场景
- 团队规模较小,希望降低运维复杂度
- 项目初期快速迭代验证
分层架构的核心价值在于:
- 关注点分离 —— UI、逻辑、数据各司其职
- 高内聚低耦合 —— 修改界面不影响业务逻辑
- 易于测试 —— 业务层可独立单元测试
- 便于维护与扩展 —— 新功能按层添加,结构清晰
- 团队协作友好 —— 前端、后端开发人员可并行工作
二、四层经典结构:Qt/C++ 单体应用的骨架
推荐采用以下四层结构构建你的 Qt 应用:
┌──────────────────────────────────────┐
│ 表现层 (Presentation) │ ← 用户看得见、摸得着的界面
├──────────────────────────────────────┤
│ 业务逻辑层 (Business Logic) │ ← 应用的大脑,处理核心规则
├──────────────────────────────────────┤
│ 数据访问层 (Data Access) │ ← 与数据库、文件、网络打交道
├──────────────────────────────────────┤
│ 公共工具层 (Common) │ ← 日志、配置、工具函数等基础设施
└──────────────────────────────────────┘
2.1 第一层:表现层(UI Layer)—— 只负责“展示”和“传达”
职责:
- 渲染界面(Widgets 或 QML)
- 接收用户输入(点击、输入、拖拽)
- 将用户操作“传达”给业务层,不处理任何业务逻辑!
Qt 实现技巧:
// MainWindow.h —— 纯粹的 UI 容器
class MainWindow : public QMainWindow {Q_OBJECT
public:explicit MainWindow(IUserService* userService, QWidget *parent = nullptr);private slots:void onLoginButtonClicked(); // 槽函数:只负责“传达”private:Ui::MainWindow *ui;IUserService* m_userService; // 依赖注入!
};
// MainWindow.cpp
void MainWindow::onLoginButtonClicked() {QString username = ui->lineEditUsername->text();QString password = ui->lineEditPassword->text();// 正确做法:把数据交给业务层处理try {User user = m_userService->login(username, password);emit userLoggedIn(user); // 通知其他组件accept();} catch (const LoginException& e) {QMessageBox::warning(this, "登录失败", e.what());}// 错误做法:在这里写数据库查询、密码加密、权限判断...
}
关键点:
- UI 类中不出现
QSqlQuery
、QFile
、复杂算法- 使用 依赖注入 传入业务服务接口
- 用 信号槽机制 与业务层通信,避免直接调用具体实现
2.2 第二层:业务逻辑层(Service Layer)—— 应用真正的“大脑”
职责:
- 实现核心业务规则(如:用户登录验证、订单计算、数据校验)
- 协调多个数据访问操作
- 处理事务、异常、日志
- 不关心数据从哪来、界面长什么样
Qt/C++ 实现技巧:
// IUserService.h —— 定义接口,解耦实现
class IUserService {
public:virtual ~IUserService() = default;virtual User login(const QString& username, const QString& password) = 0;virtual void logout() = 0;
};// UserService.h —— 具体实现
class UserService : public QObject, public IUserService {Q_OBJECT
public:explicit UserService(IUserRepository* repo, QObject* parent = nullptr);User login(const QString& username, const QString& password) override;void logout() override;signals:void userLoggedIn(const User& user);void userLoggedOut();private:IUserRepository* m_userRepository;Logger* m_logger;
};
// UserService.cpp
User UserService::login(const QString& username, const QString& password) {m_logger->info("用户尝试登录: " + username);if (username.isEmpty() || password.isEmpty()) {throw LoginException("用户名或密码不能为空");}User user = m_userRepository->findByUsername(username); // 调用数据层if (user.isNull() || !verifyPassword(password, user.passwordHash())) {throw LoginException("用户名或密码错误");}if (!user.isActive()) {throw LoginException("用户已被禁用");}m_logger->info("用户登录成功: " + user.name());emit userLoggedIn(user);return user;
}
关键点:
- 使用 纯虚类接口 (
IUserService
) 实现依赖倒置- 业务类可继承
QObject
以便使用 信号槽- 依赖的数据层对象通过 构造函数注入
- 抛出自定义异常,由 UI 层捕获并友好提示
2.3 第三层:数据访问层(Repository Layer)—— 专注“CRUD”
职责:
- 封装数据库操作(增删改查)
- 对象-关系映射(ORM)
- 管理数据库连接、事务
- 不包含任何业务规则!
Qt 实现技巧:
// IUserRepository.h
class IUserRepository {
public:virtual ~IUserRepository() = default;virtual User findByUsername(const QString& username) = 0;virtual bool save(const User& user) = 0;virtual bool deleteById(int id) = 0;
};// UserRepository.h
class UserRepository : public IUserRepository {
public:explicit UserRepository(QSqlDatabase* db);User findByUsername(const QString& username) override;bool save(const User& user) override;private:QSqlDatabase* m_db;
};// UserRepository.cpp
User UserRepository::findByUsername(const QString& username) {QSqlQuery query(*m_db);query.prepare("SELECT id, name, password_hash, is_active FROM users WHERE username = ?");query.addBindValue(username);if (query.exec() && query.next()) {User user;user.setId(query.value("id").toInt());user.setName(query.value("name").toString());user.setPasswordHash(query.value("password_hash").toString());user.setActive(query.value("is_active").toBool());return user;}return User(); // or throw, depending on your policy
}
关键点:
- 使用 Repository 模式 封装数据访问
- 数据库操作集中管理,避免 SQL 散落在各处
- 返回业务模型对象(
User
),而非QSqlRecord
- 可轻松替换为 Mock 实现用于单元测试
2.4 第四层:公共工具层(Common Layer)—— 基础设施大本营
职责:
- 日志系统(Logger)
- 配置管理(ConfigManager)
- 工具函数(字符串处理、文件操作、加密)
- 自定义异常类
- 常量定义
// Logger.h
class Logger {
public:static void info(const QString& message);static void error(const QString& message);static void setLogFile(const QString& path);
};// ConfigManager.h
class ConfigManager {
public:static QString getDatabasePath();static int getMaxRetryCount();static void setLanguage(const QString& lang);
};
三、层与层之间如何优雅通信?
推荐方式:
-
依赖注入(构造函数 / Setter)
→ 表现层 → 业务层 → 数据层 -
Qt 信号与槽(跨层通知)
// 业务层发出信号 connect(m_userService, &UserService::userLoggedIn,this, &MainWindow::updateWelcomeLabel);
-
接口调用(同步请求/响应)
User user = m_userService->login(username, password); // 同步
避免:
- 表现层直接调用
QSqlQuery
- 业务层
#include "MainWindow.h"
- 数据层抛出
QMessageBox
四、可测试性
得益于清晰的分层和接口抽象:
- 业务逻辑层 → 可脱离 UI 和数据库,用 Mock 对象进行单元测试
- 数据访问层 → 可用内存数据库(如 SQLite in-memory)测试
- 表现层 → 可用 QTest 模拟用户操作进行UI自动化测试
// TestUserService.cpp (使用 Qt Test)
void TestUserService::testLoginWithInvalidPassword() {MockUserRepository mockRepo;EXPECT_CALL(mockRepo, findByUsername("alice")).WillOnce(Return(User("alice", "hashed_wrong")));UserService service(&mockRepo);QVERIFY_EXCEPTION_THROWN(service.login("alice", "wrong"), LoginException);
}
五、项目结构建议(CMake + Qt)
MyQtApp/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── common/
│ │ ├── Logger.h/cpp
│ │ └── ConfigManager.h/cpp
│ ├── presentation/
│ │ ├── MainWindow.h/cpp
│ │ └── controllers/
│ │ └── UserController.h/cpp
│ ├── business/
│ │ ├── services/
│ │ │ ├── UserService.h/cpp
│ │ │ └── IUserService.h
│ │ └── models/
│ │ └── User.h/cpp
│ └── data/
│ ├── repositories/
│ │ ├── UserRepository.h/cpp
│ │ └── IUserRepository.h
│ └── DatabaseManager.h/cpp
└── tests/└── TestUserService.cpp
六、总结:分层不是枷锁,而是翅膀
分层架构不是增加复杂度,而是用结构化的思维管理复杂度。对于 Qt/C++ 单体应用:
“前期多花 10% 的时间设计架构,后期节省 200% 的维护成本。”
当你下次新建 Qt 项目时,不妨从这四层开始:
- 表现层 —— 画界面、传消息
- 业务层 —— 定规则、做计算
- 数据层 —— 存数据、取数据
- 公共层 —— 打基础、供弹药