Qt篇: 如何搭建MVP架构项目
MVP架构
MVP架构是一种软件设计模式,常用于用户界面(UI)的开发,特别是在需要分离业务逻辑和用户界面的情况下。MVP架构将应用程序分为三部分:
- View(视图):负责界面的展示和用户交互。View与用户直接互动,显示数据并响应用户操作,但不包含业务逻辑;
- Model(模型):负责数据管理,包括数据的获取、保存和处理。Model不关心界面如何展示数据,只专注于数据的逻辑处理;
- Presenter(表现层):作为Model和View之间的桥梁,处理用户的输入并对Model进行操作,然后将Model的数据更新到View上。Presenter包含应用程序的业务逻辑。
原理剖析
- 各层之间的通信,都是双向的;
- 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放在独立的目录中。这样可以在保持模块化的同时,避免代码重复。