当前位置: 首页 > news >正文

使用“洋葱架构”构建单体应用

作为Qt/C++工程师,我们常常致力于构建强大、高效的桌面单体应用程序。在项目初期,我们可能习惯于将业务逻辑、UI代码和数据库操作随意地混杂在MainWindow或各种管理器类中。这虽然能快速见效,但随着功能迭代,代码很快就会变成一团难以维护的“意大利面条”:牵一发而动全身,测试困难,技术栈更换更是如同噩梦。

那么,有没有一种设计模式能为我们带来结构上的清晰度,同时又能充分利用Qt的强大功能呢?

答案就是:洋葱架构(Onion Architecture),又称“端口与适配器”(Ports and Adapters)架构。今天,我们将探讨如何将这一理念应用于我们的Qt/C++单体应用开发中。


一、什么是洋葱架构?它与传统分层架构有何不同?

洋葱架构由Jeffrey Palermo提出,其核心思想是依赖方向指向圆心,即内层定义接口,外层实现接口。它强调以领域模型(Domain Model) 为核心,而不是以数据库为中心。

想象一个洋葱:你从外层开始剥,最终会到达核心。代码的依赖关系也是如此:最外层的代码(如UI、数据库)依赖于内层,而不是反过来。

  • 传统分层架构(横向切割):UI层 -> 业务逻辑层 -> 数据访问层。层与层之间紧密耦合,下层的变化会直接影响到上层。
  • 洋葱架构(同心圆)
    • 核心(Domain):包含实体(Entities)和核心业务逻辑。它是最纯粹、最稳定的部分,不依赖于任何外部框架(包括Qt)。
    • 应用服务层(Application Services):协调外部世界与核心领域的交互,实现用例(Use Cases)。它依赖于核心领域。
    • 端口/接口层(Ports/Interfaces):位于外层与内层之间,是抽象的契约。例如,定义一个IUserRepository接口。
    • 适配器/基础设施层(Adapters/Infrastructure)最外层,实现内层定义的接口。例如,用SQLite实现的SqlUserRepository,或者用Qt的QWidget实现的UI。

最大的优势:你的核心业务逻辑变得可移植、可测试,因为它对GUI框架、数据库等一无所知。


二、为什么要在Qt/C++单体应用中使用它?

你可能会想:“这听起来像企业级Web后端的东西,对我的Qt应用是不是过度设计了?”

绝非如此!即使是单体应用,洋葱架构也能带来巨大好处:

  1. 极致解耦,增强可测试性:你可以轻松为领域逻辑编写单元测试,无需创建任何QApplication实例或数据库连接。只需模拟(Mock)外层的接口即可。
  2. UI框架无关性:核心逻辑不依赖Qt。这意味着未来某天如果你想将UI从Qt Widgets迁移到Qt Quick(QML),或者甚至是一个命令行工具,你的核心业务代码几乎无需改动
  3. 更清晰的代码结构:强制你将业务逻辑从QMainWindowQDialog中剥离出来,使每个类的职责更加单一,代码可读性极大提升。
  4. 应对变化:更换数据库(如从SQLite到PostgreSQL)、更换第三方库(如JSON解析库)变得异常简单,只需在基础设施层提供一个新实现,并通过依赖注入替换即可。

三、项目组织结构:将架构映射到代码目录

清晰的架构需要一个清晰的项目结构来体现。以下是一个推荐的目录组织方式,它直观地反映了洋葱架构的层次:

my_qt_onion_app/
├── CMakeLists.txt                  # 根CMake文件,组织整个项目
├── main.cpp                        # 应用程序入口,负责依赖注入的"装配"
│
├── core/                           # 核心领域层 (最内层,最稳定)
│   ├── CMakeLists.txt
│   ├── entity/                     # 领域实体
│   │   ├── user.h
│   │   └── user.cpp
│   └── repository/                 # 接口/端口定义
│       └── iuserrepository.h       # 纯虚接口类
│
├── application/                    # 应用服务层
│   ├── CMakeLists.txt
│   └── userservice.h
│   └── userservice.cpp
│
├── infrastructure/                 # 基础设施层/适配器 (最外层)
│   ├── CMakeLists.txt
│   └── persistence/                # 数据持久化适配器
│       ├── sqluserrepository.h     # 实现 IUserRepository
│       └── sqluserrepository.cpp   # 具体实现,包含QtSql代码
│
└── ui/                            # 表示层/UI适配器 (同样是最外层)├── CMakeLists.txt├── mainwindow.h               # UI类,依赖Application服务├── mainwindow.cpp└── mainwindow.ui              # Qt Designer UI文件

CMake配置的关键点

  • core 库不链接任何Qt模块(Qt5::Core可能除外,仅用于基础类型)。
  • application 库依赖 core,并链接 core
  • infrastructure 库依赖 core,并链接 Qt5::Sql 等模块。
  • ui 可执行文件 依赖 applicationinfrastructure,并链接 Qt5::Widgets 等UI模块。
  • 依赖方向严格遵循:ui -> application -> coreinfrastructure -> core禁止 coreapplication 反向依赖 infrastructureui

这种结构使架构可见且可执行,任何开发者都能一眼看懂项目的依赖关系。


四、实战:如何用C++和Qt实现洋葱架构

让我们通过一个简单的“用户管理”示例来拆解各层的C++实现。

1. 核心领域层 (Core Domain Layer)

这是我们的核心,不包含任何Qt代码。

// core/entity/user.h
#pragma once
#include <string>namespace Domain::Entity {class User {public:User(int id, const std::string& name, const std::string& email);// ... 其他领域行为方法,如 validateEmail()int getId() const;std::string getName() const;std::string getEmail() const;private:int m_id;std::string m_name;std::string m_email;};
}

2. 端口/接口层 (Ports/Interfaces Layer)

定义抽象接口(端口),核心领域和应用服务层依赖于它。

// core/repository/iuserrepository.h
#pragma once
#include <memory>
#include <vector>
#include <core/entity/user.h> // 注意路径根据项目结构调整namespace Domain::Repository {class IUserRepository {public:virtual ~IUserRepository() = default;virtual std::unique_ptr<Entity::User> findById(int id) = 0;virtual std::vector<std::unique_ptr<Entity::User>> findAll() = 0;virtual void add(std::unique_ptr<Entity::User> user) = 0;// ... 其他方法如 update, remove};
}

3. 应用服务层 (Application Services Layer)

协调业务用例,它依赖抽象的仓库接口。

// application/userservice.h
#pragma once
#include <memory>
#include <core/repository/iuserrepository.h> // 依赖内层的接口namespace Application {class UserService {public:// 依赖注入:通过构造函数注入一个实现了 IUserRepository 的对象explicit UserService(std::unique_ptr<Domain::Repository::IUserRepository> repository);std::vector<std::unique_ptr<Domain::Entity::User>> getAllUsers();void createUser(const std::string& name, const std::string& email);private:std::unique_ptr<Domain::Repository::IUserRepository> m_repository;};
}

4. 基础设施层 (Infrastructure Layer) - Qt适配器

这是唯一允许大量使用Qt代码的地方。我们在这里实现内层定义的接口。

// infrastructure/persistence/sqluserrepository.h
#pragma once
#include <memory>
#include <core/repository/iuserrepository.h> // 实现内层定义的接口
// 注意:这里我们引入了QtSql,但在头文件中只做前向声明
#include <QSqlDatabase>namespace Infrastructure::Persistence {class SqlUserRepository : public Domain::Repository::IUserRepository {public:explicit SqlUserRepository(QSqlDatabase& database);~SqlUserRepository() override = default;std::unique_ptr<Domain::Entity::User> findById(int id) override;std::vector<std::unique_ptr<Domain::Entity::User>> findAll() override;void add(std::unique_ptr<Domain::Entity::User> user) override;private:QSqlDatabase& m_database;};
}
// infrastructure/persistence/sqluserrepository.cpp
#include "sqluserrepository.h"
#include <QSqlQuery>
#include <QSqlError>
// ... 具体实现使用 QtSql 进行数据库操作

5. UI层 (UI Layer) - Qt Widgets

UI层是另一个“适配器”,它依赖应用服务层。

// ui/mainwindow.h
#pragma once
#include <QMainWindow>
#include <memory>// 前向声明,避免引入应用服务层的头文件导致依赖核心领域
namespace Application {class UserService;
}QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECT
public:// 依赖注入:通过构造函数注入 UserServiceexplicit MainWindow(std::unique_ptr<Application::UserService> userService, QWidget* parent = nullptr);~MainWindow() override;private slots:void on_refreshButton_clicked();void on_addUserButton_clicked();private:Ui::MainWindow* ui;std::unique_ptr<Application::UserService> m_userService;void refreshUserList();
};

main.cpp中,我们完成依赖注入的“ wiring up ”工作:

// main.cpp
#include <QApplication>
#include <QSqlDatabase>
#include <memory>
#include "ui/mainwindow.h"
#include "application/userservice.h"
#include "infrastructure/persistence/sqluserrepository.h"int main(int argc, char* argv[]) {QApplication a(argc, argv);// 1. 初始化最外层基础设施:数据库QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");db.setDatabaseName("app.db");if (!db.open()) {qCritical() << "Cannot open database!";return -1;}// 2. 创建具体实现(适配器)auto userRepository = std::make_unique<Infrastructure::Persistence::SqlUserRepository>(db);// 3. 注入到应用服务auto userService = std::make_unique<Application::UserService>(std::move(userRepository));// 4. 创建UI并注入所需服务MainWindow w(std::move(userService));w.show();return a.exec();
}

五、总结与最佳实践

将洋葱架构引入您的Qt/C++项目并非一蹴而就,但带来的长期收益是显著的。

  • 起步建议:从一个新的、规模适中的模块开始尝试,而不是重构整个遗留系统。
  • 依赖注入(DI):这是实现洋葱架构的关键技术。在C++中,通常通过构造函数注入来实现,手动管理(如本例)或使用轻量级DI容器。
  • 智能指针:大量使用std::unique_ptrstd::shared_ptr来明确资源所有权和生命周期。
  • Qt的适配QObject的生命周期管理和信号槽机制可能与纯领域模型有冲突。常见的做法是让核心领域保持为纯POCOs(Plain Old C++ Objects),而在外层(如应用服务或UI层)再使用QObject来管理或进行线程间通信。
  • 明确的项目结构:采用反映架构的分层目录结构,并利用CMake的target_link_libraries来强制实施依赖规则,这是成功落地洋葱架构的重要保障。

文章转载自:

http://Ol2h49TW.wjjxr.cn
http://qDWsq4Nr.wjjxr.cn
http://7KZxWQm7.wjjxr.cn
http://ZfJrmdpT.wjjxr.cn
http://bD23B6qa.wjjxr.cn
http://YVoWxmud.wjjxr.cn
http://4dmOAMGd.wjjxr.cn
http://wkBLlIny.wjjxr.cn
http://7KWhiKBL.wjjxr.cn
http://xNYCKPCw.wjjxr.cn
http://6APeAvol.wjjxr.cn
http://jSISGuEs.wjjxr.cn
http://NNHVQUFB.wjjxr.cn
http://VIDfBoMM.wjjxr.cn
http://d2NYcGfP.wjjxr.cn
http://YcZGV2Ci.wjjxr.cn
http://aR0ZYxwP.wjjxr.cn
http://G2BkePzl.wjjxr.cn
http://HF3A4BGl.wjjxr.cn
http://AOnEnYKK.wjjxr.cn
http://fvwy93QF.wjjxr.cn
http://BQhbIN3t.wjjxr.cn
http://llByUyIG.wjjxr.cn
http://NzsJqHtS.wjjxr.cn
http://NerLRMYM.wjjxr.cn
http://UaamoCte.wjjxr.cn
http://5299JM1A.wjjxr.cn
http://wy62LAwL.wjjxr.cn
http://oHoDrR5H.wjjxr.cn
http://WxeH54K4.wjjxr.cn
http://www.dtcms.com/a/382133.html

相关文章:

  • DAY 27 函数专题2:装饰器-2025.9.14
  • 浅析Linux进程信号处理机制:基本原理及应用
  • php学习(第五天)
  • C盘清理技巧分享的技术文章大纲
  • PINN物理信息神经网络驱动的三维声波波动方程求解MATLAB代码
  • 深度学习优化器进化史:从SGD到AdamW的原理与选择
  • 计算机视觉(opencv)实战十九——角点检测图像特征(Harris 角点、Shi-Tomasi 角点)
  • 【限流器设计】固定窗口计数法
  • Estimator and Confidence interval
  • 构建AI智能体:三十二、LangChain智能体:打造会使用工具(Tools)、有记忆(Memory)的AI助手
  • AI内容标识新规实施后,大厂AI用户协议有何变化?(六)科大讯飞
  • 机械应答到自然交流,声网AI陪练改变我的口语
  • 贪心算法应用:信用评分分箱问题详解
  • 【Spring AI】Filter 简单使用
  • html各种常用标签
  • Linux 进程信号之信号的捕捉
  • 实验-高级acl(简单)
  • C++之特殊类设计
  • stm32教程:USART串口通信
  • 地级市绿色创新、碳排放与环境规制数据
  • ES——(二)基本语法
  • 中级统计师-统计法规-第十一章 统计法律责任
  • 拥抱直觉与创造力:走进VibeCoding的新世界
  • Python进程和线程——多进程
  • 论文阅读 2025-9-13 论文阅读随心记
  • leecode56 合并区间
  • 用R获取 芯片探针与基因的对应关关系 bioconductor的包的 三者对应关系
  • xxl-job的使用
  • 2025 年 9 月 12 日科技前沿动态全览
  • 高德地图自定义 Marker:点击 悬停 显示信息框InfoWindow实战(Vue + AMap 2.0)