第 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 Zhu3 小时前
【CUDA编程入门⑥】CPU多线程并行优化:OpenMP、NUMA与SIMD向量化
人工智能·语言模型·架构·aigc·agi
FII工业富联科技服务3 小时前
工业经验如何转化为AI Skills?详解知识调用、任务编排与多智能体协同
大数据·人工智能·架构·自动化·omniverse·柔性生产
Dawson Zhu4 小时前
【CUDA编程入门③】经典算子实战:Reduce归约——从朴素实现到高性能优化的演进之路
人工智能·语言模型·架构·aigc·agi
黄俊懿5 小时前
【架构师从入门到进阶】第七章:缓存和存储的优化思路——第一节:缓存概述
数据结构·数据库·分布式·缓存·架构·系统架构·1024程序员节
Oiiouui5 小时前
Godot-GridGame架构数据层篇
架构·godot·1024程序员节
comedate5 小时前
【SMLib】SMLib 五层架构深度解析:SMLib / SM_API / SM_API_USD / SmPyLib 各管什么
架构·cad·1024程序员节·openusd·smlib·nurbs
Dawson Zhu6 小时前
【CUDA编程入门⑦】HPC算子开发实战:从优化方法论到业务落地
人工智能·语言模型·架构·aigc·agi
合尘猫6 小时前
AI客服国标落地一个月:从工程角度看,真正要改的是三处数据结构
数据库·人工智能·架构
智慧光迅AINOPOL15 小时前
园区全光网IP+POL架构怎么设计:组网拓扑与带宽演进
网络协议·tcp/ip·架构