第 4 课:MVC、MVP 与 MVVM 架构

第一阶段 · 第 4 课:MVC、MVP 与 MVVM 架构

上一课我们学习了分层架构,把 Qt 工程拆成 UI 层、业务层、设备层和公共基础层。

这一课进一步解决一个实际问题:Qt 界面类越来越臃肿,按钮槽函数里既有业务逻辑,又有设备操作和数据处理,应该怎么办?

我们以你熟悉的 Qt/C++ 电源控制程序为例,学习三种常见架构。

一、先理解三种架构的区别

1. MVC --- Model-View-Controller

模型-视图-控制器

  • Model:保存数据、处理业务状态。
  • View:显示界面。
  • Controller:接收操作并协调 Model 与 View。
    适合:理解传统桌面应用和 Web 应用的职责划分。

2. MVP --- Model-View-Presenter

模型-视图-展示器

  • Model:数据与业务。
  • View:负责显示和转发用户操作。
  • Presenter:处理界面相关逻辑,调用 Model,再通知 View 更新。
    适合:希望把界面逻辑独立出来,便于单元测试的桌面程序。

3. MVVM --- Model-View-ViewModel

模型-视图-视图模型

  • Model:数据与业务规则。
  • View:显示界面。
  • ViewModel:向界面提供可显示的数据和操作接口。
    适合:数据状态较多、界面需要频繁更新的应用。

注意:三者并非互相排斥的绝对标准。不同项目可能采用混合结构,尤其在 Qt Widgets 项目中很常见。

二、先看最常见的问题:界面类承担太多职责

例如下面这种写法:

cpp 复制代码
void FormPower::on_pushButtonSet_clicked()
{
    double voltage = ui->doubleSpinBoxVoltage->value();

    // 检查参数
    if (voltage < 0 || voltage > 10)
        return;

    // 设备通信
    m_power->setVoltage(voltage);

    // 更新界面
    ui->labelStatus->setText("设置完成");

    // 保存配置
    m_settings->setValue("Voltage", voltage);
}

这个槽函数同时承担了参数校验、设备控制、界面更新和配置保存。

问题是:如果以后要增加自动测试、定时测试或命令行控制,也需要重复这些逻辑。

我们可以先把界面逻辑抽出去。对于 Qt Widgets 项目,MVP 是一种容易上手的选择。

三、用 MVP 改造 Qt 程序

  1. 定义 View 接口
    让 Presenter 不直接操作 QLabel、QDoubleSpinBox 等具体控件。
cpp 复制代码
class IPowerView
{
public:
    virtual ~IPowerView() = default;

    virtual double voltage() const = 0;
    virtual void showStatus(const QString& text) = 0;
};

这里的 IPowerView 只定义 Presenter 需要的界面能力。

  1. 定义 Presenter

Presenter 接收用户操作,执行校验、调用业务对象,并将结果交给 View。

cpp 复制代码
class PowerPresenter
{
public:
    PowerPresenter(IPowerView* view, IPowerDevice* power)
        : m_view(view), m_power(power)
    {
    }

    void setVoltage()
    {
        if (!m_view || !m_power)
            return;

        const double voltage = m_view->voltage();

        if (voltage < 0.0 || voltage > 10.0) {
            m_view->showStatus("电压范围错误");
            return;
        }

        if (!m_power->setVoltage(voltage)) {
            m_view->showStatus("设置失败");
            return;
        }

        m_view->showStatus("设置成功");
    }

private:
    IPowerView* m_view;       // 非拥有指针
    IPowerDevice* m_power;    // 非拥有指针
};

这里继续使用了前面学过的接口抽象和依赖注入。Presenter 不需要知道具体是哪一个 FormPower,也不需要知道具体电源型号。

  1. 让 Qt 窗口实现 View
cpp 复制代码
class FormPower : public QWidget, public IPowerView
{
    Q_OBJECT

public:
    explicit FormPower(QWidget* parent = nullptr)
        : QWidget(parent)
    {
        // setupUi(this);
    }

    double voltage() const override
    {
        return ui->doubleSpinBoxVoltage->value();
    }

    void showStatus(const QString& text) override
    {
        ui->labelStatus->setText(text);
    }

private slots:
    void on_pushButtonSet_clicked()
    {
        if (m_presenter)
            m_presenter->setVoltage();
    }

private:
    PowerPresenter* m_presenter = nullptr; // 非拥有指针
};

实际工程中,记得在构造函数或初始化函数中创建并设置 m_presenter。Presenter 的生命周期必须覆盖它被使用的时间。

  1. 在组装位置连接对象
cpp 复制代码
Power2657 power;
FormPower form;
PowerPresenter presenter(&form, &power);

// presenter 在 form 使用期间保持存活
form.setPresenter(&presenter);
form.show();


上面示例中的 setPresenter() 需要在 FormPower 中提供对应的 setter:
void setPresenter(PowerPresenter* presenter)
{
    m_presenter = presenter;
}

实际项目可以使用构造函数注入,让依赖关系更加明确。

现在职责变成:

  • FormPower:读取控件值、显示状态、转发点击操作。
  • PowerPresenter:处理界面相关流程和输入校验。
  • IPowerDevice:抽象电源能力。
  • Power2657:负责具体设备操作。
    四、MVP 和 MVVM 到底有什么区别?
    MVP 中,Presenter 通常会主动调用 View 的方法来更新界面。

MVVM 则通常让 ViewModel 暴露状态和命令,View 通过数据绑定或信号槽响应这些状态变化。

比较 MVP MVVM

界面更新 Presenter 调用 View 接口 View 响应 ViewModel 的状态

依赖关系 Presenter 持有 View 接口 View 观察或绑定 ViewModel

测试 可以独立测试 Presenter 可以独立测试 ViewModel

Qt Widgets 很容易用接口实现 可用属性、信号槽实现

Qt Quick/QML 可用 通常很自然

在 Qt 中,MVVM 不要求必须使用 QML。你也可以在 Qt Widgets 中使用 QObject、Q_PROPERTY、signals 和 slots 实现 ViewModel。

例如:

cpp 复制代码
class PowerViewModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString status READ status NOTIFY statusChanged)

public:
    explicit PowerViewModel(QObject* parent = nullptr)
        : QObject(parent)
    {
    }

    QString status() const
    {
        return m_status;
    }

    void setStatus(const QString& status)
    {
        if (m_status == status)
            return;

        m_status = status;
        emit statusChanged();
    }

signals:
    void statusChanged();

private:
    QString m_status;
};

当 m_status 变化时,ViewModel 发出 statusChanged(),界面便可以连接这个信号并刷新状态显示。若需要完整的数据绑定体验,Qt Quick/QML 提供了更直接的机制。

五、结合你的 Qt 项目,怎么选?

我的建议是:

  • 普通 Qt Widgets 工具界面:先使用分层架构,复杂界面可以引入 MVP。
  • 大量参数、设备状态、异步采集和实时曲线:可以把数据状态抽成独立 Model 或 ViewModel,避免窗口类越来越大。
  • Qt Quick/QML 项目:优先考虑 MVVM 风格。
  • 已有大型项目:不必为了模式而整体重写。先把最臃肿的槽函数拆出去,逐步引入接口和业务类。
    尤其要注意:MVP 的 Presenter 不应承担所有设备协议和底层通信。Presenter 负责界面交互流程,设备控制流程仍然可以由上一课的 PowerController 或 TestService 处理。
    六、课后练习
    问题:你的红外相机界面需要显示采集状态、温度、实时图像,并响应"开始采集"按钮。下面哪种设计更合理?

A. 所有逻辑全部写进 FormCamera 的槽函数

B. 界面负责显示和交互,控制类负责采集流程,设备类负责相机通信;状态通过信号槽传递

C. 每个控件单独创建一个线程,避免界面阻塞

D. 所有类都继承 QObject,就自然实现了分层架构

提交答案

下一课可以进入第一阶段的最后一个主题:如何设计模块边界、头文件依赖和 .pro 工程结构,把这些架构原则落到实际 Qt 工程中。

相关推荐
Dawson Zhu2 小时前
【分布式训练④】张量并行与序列并行:把单层参数切碎分到多张卡
人工智能·语言模型·架构·aigc·agi
码客-ygluu5 小时前
一个通用游戏数据服务器
c++·架构
炫酷的大高个7 小时前
98% 连跑 20 次只剩 67%,我算了大模型重试账
人工智能·架构·开源
用户6602676118797 小时前
AI 代码上线前的第 24 道门禁:上线失败时,怎样在 10 分钟内退回?
后端·架构
humcomm8 小时前
设计与架构 · 01 sidecar 会消失吗:从 Istio ambient GA 到 AWS App Mesh 关停,一次服务网格数据面的重新洗牌
架构·istio·aws
Eyfcom9 小时前
〖物流低代码实战 01〗物流页面可视化搭建的底层架构:协议、引擎与产物的三重奏
低代码·架构·快递系统·物料体系·物流低代码实战
浪遏17 小时前
recruitment-cli:让 AI 接手求职招聘,从架构到一次发送超时
架构·typescript·node.js
小雉系统20 小时前
BcoreOS安全通信架构:FsSession 抗算法后门安全设计
算法·安全·架构
JAVA前线21 小时前
用《实践论》分析前沿部署工程师:AI时代听见炮声的人
架构·ai编程
深入云栈1 天前
Netty 4.2.x 源码深度解析 (二十三):ByteBuf 核心设计 —— 索引、类型与零拷贝
java·后端·架构