用分层架构打造单体应用

发布于:2025-09-13 ⋅ 阅读:(21) ⋅ 点赞:(0)

在桌面端、工业控制、嵌入式 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 类中不出现 QSqlQueryQFile、复杂算法
  • 使用 依赖注入 传入业务服务接口
  • 信号槽机制 与业务层通信,避免直接调用具体实现

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);
};

三、层与层之间如何优雅通信?

推荐方式:

  1. 依赖注入(构造函数 / Setter)
    → 表现层 → 业务层 → 数据层

  2. Qt 信号与槽(跨层通知)

    // 业务层发出信号
    connect(m_userService, &UserService::userLoggedIn,
            this, &MainWindow::updateWelcomeLabel);
    
  3. 接口调用(同步请求/响应)

    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 项目时,不妨从这四层开始:

  1. 表现层 —— 画界面、传消息
  2. 业务层 —— 定规则、做计算
  3. 数据层 —— 存数据、取数据
  4. 公共层 —— 打基础、供弹药