Qt篇: 如何搭建MVP架构项目

MVP架构

MVP架构是一种软件设计模式,常用于用户界面(UI)的开发,特别是在需要分离业务逻辑和用户界面的情况下。MVP架构将应用程序分为三部分:

  • View(视图):负责界面的展示和用户交互。View与用户直接互动,显示数据并响应用户操作,但不包含业务逻辑;
  • Model(模型):负责数据管理,包括数据的获取、保存和处理。Model不关心界面如何展示数据,只专注于数据的逻辑处理;
  • Presenter(表现层):作为Model和View之间的桥梁,处理用户的输入并对Model进行操作,然后将Model的数据更新到View上。Presenter包含应用程序的业务逻辑。

原理剖析

MVP架构剖析
  • 各层之间的通信,都是双向的;
  • View 与 Model 不发生联系,都通过 Presenter 传递,是非常纯粹的“联络员”;
  • View 不部署任何业务逻辑,称为"被动视图"(Passive View),而 Presenter 将部署所有逻辑。

MVP架构的优点:

  • 分离关注点:MVP架构将数据逻辑与界面展示分离,易于维护和扩展。
  • 易于测试:由于逻辑与视图的分离,单元测试可以更容易地覆盖业务逻辑而不依赖UI。
  • 代码复用:业务逻辑和视图逻辑的分离允许更好地代码复用。

如何实现MVP架构原理

其实Qt提供的信号槽机制是搭建MVP架构最重要的技术栈,整体搭建工作和功能模块开发都可全依赖信号槽去实现。

模拟一个用户名设置的简单demo,用户在QLineEdit中输入用户名,点击Update Name按钮,输入的用户名会更新到QLabel中显示。

View层

View层只管界面显示,其余一概不理会。按钮点击后的将QLineEdit的值以userNameChanged信号发射出去,等待Modal层处理完用户名数据后,再通过Presenter层更新至View层。

#include "UserView.h"

UserView::UserView(QWidget *parent) : QWidget(parent)
{
    m_setNameLabel = new QLabel("Set User Name:", this);
    m_lineEdit = new QLineEdit(this);
    m_getNameLabel = new QLabel("Your Name: ");
    m_button = new QPushButton("Update Name", this);

    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->addWidget(m_setNameLabel);
    layout->addWidget(m_lineEdit);
    layout->addWidget(m_getNameLabel);
    layout->addWidget(m_button);
    setLayout(layout);

    connect(m_button, &QPushButton::clicked, this, &UserView::onSetUserName);
}

void UserView::onSetUserName() {
    emit userNameChanged(getUserName());
}

QString UserView::getUserName() const {
    return m_lineEdit->text();
}

void UserView::setUserName(const QString &name) {
    m_getNameLabel->setText("Your Name: " + name);
}

Modal层:只负责处理数据,再将数据返回至Presenter层。

// UserModal.h

#ifndef USERMODEL_H
#define USERMODEL_H

#include <QObject>

class UserModel : public QObject
{
    Q_OBJECT

    QString m_userName;

public:
    explicit UserModel(QObject *parent = nullptr);
    // model处理:获取用户名
    QString getUserName() const;
    // model处理:设置用户名
    void setUserName(const QString &name);

signals:
    void userNameChanged();

};
#endif // USERMODEL_H
// UserModal.cpp
#include "UserModel.h"

UserModel::UserModel(QObject *parent) : QObject(parent) {}

// model处理:获取用户名
QString UserModel::getUserName() const { return m_userName; }

// model处理:设置用户名
void UserModel::setUserName(const QString &name) {
    if (m_userName != name) {
        m_userName = name;
        emit userNameChanged();
    }
}

Presenter层

presenter层是作为view和modal之间的桥梁,所以需要引入view和modal实例,再通过信号槽机制进行事件关联。

// UserPresenter.h中引入view和model
#include "UserView.h"
#include "UserModel.h"
// 在UserPresenter实现view和modal事件关联

#include "UserPresenter.h"

UserPresenter::UserPresenter(UserView *view, UserModel *model, QObject *parent)
    : QObject(parent), m_view(view), m_model(model)
{
    connect(m_view, &UserView::userNameChanged, this, &UserPresenter::onUserNameChanged);
    connect(m_model, &UserModel::userNameChanged, this, &UserPresenter::onModelUserNameChanged);

    // 初始化视图显示,默认为空值
    m_view->setUserName(m_model->getUserName());
}

void UserPresenter::onUserNameChanged(const QString &name) {
    m_model->setUserName(name);
}

void UserPresenter::onModelUserNameChanged() {
    m_view->setUserName(m_model->getUserName());
}

实验虽然小,但却能简单了解MVP是如何实现的,如何将业务数据逻辑和UI界面相分离的,在项目开发中当然所面临的会很复杂,但是懂得基本原理,对于整体搭建刚开始可能会觉得难,难在哪?在于这种模式可能会把人逻辑绕完,不太习惯这样去实现功能,这只有在各种实战中慢慢习惯良好的编程风格,总结和实践才是搭建完美MVP架构的真理。

MVP架构目录组织方式

在大型项目中,项目目录结构的规划对于代码的维护、扩展和团队协作至关重要。通常有两种常见的目录组织方式,这两种方式各有优缺点,选择哪种方式取决于项目的规模、复杂度、团队的熟悉程度和具体需求。

  • 基于架构层次的分层结构(如MVP的Model、View、Presenter);
  • 基于功能模块的分组结构。

基于架构层次的分层结构

在这种方式下,代码根据MVP的架构层次来组织,即将Model、View和Presenter分为不同的目录。这种方式的优点是能够清晰地展示代码的架构层次,使得层次之间的依赖关系明确,适合业务逻辑较为简单的项目。

/project-root
├── /src
│   ├── /model          # 数据和业务逻辑
│   │   ├── UserModel.h
│   │   ├── UserModel.cpp
│   │   └── ...
│   ├── /view           # 用户界面和UI逻辑
│   │   ├── UserView.h
│   │   ├── UserView.cpp
│   │   └── ...
│   ├── /presenter      # 负责交互和业务逻辑处理
│   │   ├── UserPresenter.h
│   │   ├── UserPresenter.cpp
│   │   └── ...
│   ├── /utils          # 工具类或通用函数
│   │   └── ...
│   ├── /resources      # 资源文件(如图片、语言文件等)
│   │   └── ...
│   └── main.cpp
├── /tests              # 单元测试代码
│   └── ...
├── /doc                # 项目文档
│   └── ...
├── /build              # 构建输出
│   └── ...
├── CMakeLists.txt      # 构建系统配置文件
└── README.md           # 项目说明

优点

  • 清晰的层次分明:不同的职责明确,代码结构易于理解和导航;
  • 依赖管理简单:同一层次内的文件依赖关系清晰。

缺点

  • 随着功能的增多,不同层次的文件可能需要频繁跨目录修改;
  • 对于大型项目,代码的分散可能导致管理复杂度增加。

基于功能模块的分组结构

这种项目结构方式也是我比较倾向的。在这种方式下,代码根据功能模块进行分组,每个模块包含Model、View和Presenter。这种方式的优点是模块化更强,每个功能模块是相对独立的单元,适合大型项目和团队开发。

/project-root
├── /src
│   ├── /user           # 用户模块
│   │   ├── /model      # 该模块的Model
│   │   │   ├── UserModel.h
│   │   │   └── UserModel.cpp
│   │   ├── /view       # 该模块的View
│   │   │   ├── UserView.h
│   │   │   └── UserView.cpp
│   │   └── /presenter  # 该模块的Presenter
│   │       ├── UserPresenter.h
│   │       └── UserPresenter.cpp
│   ├── /auth           # 认证模块
│   │   ├── /model
│   │   ├── /view
│   │   └── /presenter
│   ├── /settings       # 设置模块
│   │   ├── /model
│   │   ├── /view
│   │   └── /presenter
│   ├── /common         # 公共模块(如公共的Model、View或工具类)
│   │   ├── /utils
│   │   └── /widgets
│   ├── /resources      # 资源文件(如图片、语言文件等)
│   │   └── ...
│   └── main.cpp
├── /tests              # 单元测试代码
│   └── ...
├── /doc                # 项目文档
│   └── ...
├── /build              # 构建输出
│   └── ...
├── CMakeLists.txt      # 构建系统配置文件
└── README.md           # 项目说明

优点

  • 模块化强:每个模块是相对独立的,容易进行独立开发、测试和维护。
  • 扩展性好:新增功能时可以直接添加新的模块目录。

缺点

  • 可能会导致重复代码:不同模块可能存在相似的Model、View或Presenter代码。
  • 依赖管理复杂:模块之间的依赖需要明确管理,特别是公共模块的依赖。

如何选择

对于大型项目,基于功能模块的分组结构通常是更好的选择,尤其是在项目功能复杂、多团队协作的情况下。它能更好地支持独立开发、测试和维护,提高代码的可扩展性和可读性。

在实际项目中,可以结合两种方式的优点。例如,在功能模块分组的基础上,将公共的Model、View和Presenter放在独立的目录中。这样可以在保持模块化的同时,避免代码重复。