工业软件架构2:(QT和C++实现)


在更复杂的场景中,比如按键响应包含耗时操作、每个界面参数的读取和写入需要独立处理、并且希望更清晰地组织代码,命令模式、页面导航、和 MVVM 模式将发挥重要作用。

1. 命令模式的使用

  • 场景: 当按键触发某些耗时操作时,我们可以使用命令模式将这些操作封装起来,委托给线程池执行,从而避免主线程的阻塞。
  • 改进:命令模式: 封装每个按键操作为独立的命令对象,并将这些命令委托给线程池执行。这样可以确保耗时操作不会阻塞主线程,并且可以轻松实现操作的撤销、重做等高级功能。

命令模式(Command Pattern)

命令模式是一种行为设计模式,它将一个请求封装为一个对象,从而使得你可以用不同的请求、队列或者日志来参数化其他对象。命令模式还支持撤销操作。

命令模式的基本概念

在命令模式中,关键的参与者包括:

  • 命令(Command):命令是一个接口,声明了执行操作的接口方法(通常是 execute() 方法)。
  • 具体命令(Concrete Command):具体命令类实现了 Command 接口,包含了一个接收者对象(Receiver),并实现了该操作。具体命令将接收者对象和操作绑定在一起,调用接收者对象的操作方法来实现命令的执行。
  • 接收者(Receiver):接收者是命令要操作的对象,它包含实际执行命令的逻辑。具体命令调用接收者的方法来完成请求。
  • 调用者(Invoker):调用者是使用命令对象的类,它调用命令对象的 execute() 方法来执行命令。
  • 客户端(Client):客户端创建并配置具体命令对象,并将接收者对象与之关联。客户端还将命令对象传递给调用者。

命令模式的运作机制

  1. 客户端创建具体命令对象,并将相应的接收者对象传递给具体命令对象。
  2. 调用者持有具体命令对象的引用,并在需要时调用命令对象的 execute() 方法。
  3. 具体命令对象调用接收者对象的方法来执行请求。

1. 定义命令接口

首先,我们定义一个 Command 接口,该接口声明了 execute() 方法。

class Command 
{
public:
    virtual ~Command() {}
    virtual void execute() = 0;
};

2. 实现具体命令

具体命令实现了 Command 接口,并将接收者对象与特定的操作绑定在一起。

class ExpensiveOperationCommand : public Command 
{
public:
    ExpensiveOperationCommand(SensorModule *sensorModule, int sensorId,
    Page1ViewModel *sourceViewModel, Page2ViewModel *targetViewModel,
    HardwareControlModule *hardwareModule, int operationId)
        : sensorModule(sensorModule)
        , sensorId(sensorId)
        , sourceViewModel(sourceViewModel)
        , targetViewModel(targetViewModel)
        , hardwareModule(hardwareModule)
        , operationId(operationId) {}
        
    void execute() override
     {
        // 获取源 ViewModel 的信息
        QVariant sourceInfo = sourceViewModel->getParameter("someKey");
        
        double sensorValue = sensorModule->getSensorData(sensorId);
        
        // 将耗时操作移到后台线程执行
        QtConcurrent::run([this]()
         {
            // 假设有个复杂的计算或操作基于 sourceInfo和sensorValue
            hardwareModule->performOperation(operationId,sourceInfo, sensorValue);
            
 			// 更新目标 ViewModel
            QMetaObject::invokeMethod(targetViewModel, [this, result]() {
                targetViewModel->setParameter("resultKey", result);
            }, Qt::QueuedConnection);
        });
        
    }

private:
    Page1ViewModel *sourceViewModel;
    Page2ViewModel *targetViewModel;
    HardwareControlModule *hardwareModule;
    int operationId;
};

3. 调用者类

调用者(Invoker)持有命令对象的引用,可以是一个用户界面按钮,也可以是另一个触发器。在需要执行命令时,调用者调用 execute() 方法。
在 ButtonHandlerModule 中,可以将按键操作绑定到命令对象,并在触发时执行命令。

void ButtonHandlerModule::handleButtonPress(int buttonId) 
{
    Command *command = createCommandForButton(buttonId);
    if (command) 
    {
        command->execute();
        delete command;
    }
}

Command* ButtonHandlerModule::createCommandForButton(int buttonId)
 {
    // 根据 buttonId 返回对应的命令对象
    return new ExpensiveOperationCommand(hardwareModule, buttonId);
}

4.扩展命令模式的功能

命令模式的一个强大之处在于它可以很容易地扩展,比如添加撤销功能、宏命令(同时执行多个命令)等。

撤销命令:

我们可以通过在 Command 接口中添加 undo() 方法来支持撤销操作。

class Command 
{
public:
    virtual ~Command() {}
    virtual void execute() = 0;
    virtual void undo() = 0;  // 新增撤销方法
};

class SetThresholdCommand : public Command 
{
public:
    SetThresholdCommand(SystemSettings *settings, int newThreshold)
        : settings(settings), threshold(newThreshold), previousThreshold(settings->getThreshold()) {}

    void execute() override 
    {
        previousThreshold = settings->getThreshold();
        settings->setThreshold(threshold);
    }

    void undo() override
     {
        settings->setThreshold(previousThreshold);
    }

private:
    SystemSettings *settings;
    int threshold;
    int previousThreshold;
};

宏命令:

宏命令是包含多个命令的复合命令,执行时会依次执行所有包含的命令。

class MacroCommand : public Command 
{
public:
    void addCommand(Command *command)
     {
        commands.append(command);
    }

    void execute() override 
    {
        for (Command *command : commands)
         {
            command->execute();
        }
    }

    void undo() override 
    {
        for (Command *command : commands) 
        {
            command->undo();
        }
    }

private:
    QList<Command*> commands;
};

总结

命令模式通过将操作封装为对象,解耦了请求的发出者与接收者,使得系统具有更好的灵活性和可扩展性。它特别适用于以下场景:

  1. 需要对操作进行参数化:将操作封装为命令对象,允许操作参数化。
  2. 需要在不同时间执行操作:命令对象可以在创建时延迟执行,或者通过队列机制进行排队执行。
  3. 需要支持撤销/重做:命令对象可以记录操作状态,允许撤销和重做操作。
  4. 需要记录日志:通过命令模式,可以轻松记录操作日志,并在需要时重放这些操作。
    在 Qt 应用程序中,命令模式可以用于管理界面操作、系统设置变更等需要封装和延迟执行的操作。

2. MVVM 模式的使用

  • 场景: 每个界面需要独立管理参数的读取和写入,并且希望界面与业务逻辑分离。MVVM 模式是一个理想的选择,它可以将界面逻辑与业务逻辑彻底分离,使得代码更易于维护和测试。
  • 改进:ViewModel: 将界面参数的读取和写入逻辑放入 ViewModel 中,每个界面(View)都有一个对应的 ViewModel 处理数据交互。这样界面与数据处理的逻辑分离,增强了代码的可维护性和可测试性。
class Page1ViewModel : public QObject
 {
    Q_OBJECT

public:
    Page1ViewModel(ParameterManager *paramManager, QObject *parent = nullptr)
        : QObject(parent), paramManager(paramManager) {}

    Q_INVOKABLE QVariant getParameter(const QString &key)
     {
        return paramManager->getParameter(key);
    }

    Q_INVOKABLE void setParameter(const QString &key, const QVariant &value)
     {
        paramManager->setParameter(key, value);
        emit parameterChanged(key, value);
    }

signals:
    void parameterChanged(const QString &key, const QVariant &value);

private:
    ParameterManager *paramManager;
};

View(界面)部分则通过绑定与 ViewModel 交互:

class Page1View : public QWidget
 {
    Q_OBJECT

public:
    Page1View(Page1ViewModel *viewModel, QWidget *parent = nullptr)
        : QWidget(parent), viewModel(viewModel)
         {
        setupUI();
    }

private:
    Page1ViewModel *viewModel;

    void setupUI() 
    {
        QVBoxLayout *layout = new QVBoxLayout(this);
        QLabel *label = new QLabel("Parameter value", this);
        layout->addWidget(label);

        QLineEdit *paramInput = new QLineEdit(this);
        layout->addWidget(paramInput);

        connect(paramInput, &QLineEdit::textChanged, this, &Page1View::onParameterChanged);
        connect(viewModel, &Page1ViewModel::parameterChanged, this, &Page1View::updateView);
    }

private slots:
    void onParameterChanged(const QString &newValue) 
    {
        viewModel->setParameter("someKey", newValue);
    }
    void updateView(const QString &key, const QVariant &value) 
    {
        // 更新界面显示
    }
};

双向绑定-结合 QDataWidgetMapper 和 自定义绑定工具类

  • 简化代码:QDataWidgetMapper 可以直接将模型数据与多个UI控件绑定,减少手动编写信号与槽的代码,特别适合表单、列表等复杂界面的绑定。
  • 代码清晰度高:自定义绑定工具类封装了信号与槽连接,使得代码逻辑更加清晰,不会被繁杂的信号槽连接逻辑所干扰。
  • 易维护和扩展:这两种方法相结合,既能简化日常的数据绑定,又保持了Qt信号槽的灵活性,易于维护和调试。
  • 高效响应式:通过QDataWidgetMapper直接绑定模型和控件的属性,能够让UI实时响应数据变化,模拟接近于QML的绑定体验。

推荐实现方式

  • 使用QDataWidgetMapper进行基本数据的绑定,特别是用于表单或表格形式的数据输入输出。
  • 对于更灵活的UI交互和绑定,可以使用自定义绑定工具类来处理控件和ViewModel之间的交互。

ViewModel(使用QStandardItemModel)

#include <QStandardItemModel>

class ViewModel : public QStandardItemModel 
{
    Q_OBJECT

public:
    enum Roles { ValueRole = Qt::UserRole + 1 };

    ViewModel(QObject *parent = nullptr) : QStandardItemModel(parent) 
    {
        QStandardItem *item = new QStandardItem();
        item->setData(0, ValueRole);
        appendRow(item);
    }

    int value() const { return item(0, 0)->data(ValueRole).toInt(); }

    void setValue(int newValue)
     {
        if (value() != newValue) 
        {
            item(0, 0)->setData(newValue, ValueRole);
            emit valueChanged(newValue);
        }
    }

signals:
    void valueChanged(int newValue);
};

自定义绑定工具类

#include <QObject>
#include <QSlider>
#include <QLabel>

class BindingUtils
 {
public:
    static void bindValue(QSlider *slider, ViewModel *viewModel) 
    {
        QObject::connect(slider, &QSlider::valueChanged, viewModel, &ViewModel::setValue);
        QObject::connect(viewModel, &ViewModel::valueChanged, slider, &QSlider::setValue);
    }

    static void bindValue(QLabel *label, ViewModel *viewModel)
     {
        QObject::connect(viewModel, &ViewModel::valueChanged, label, [label](int newValue)
         {
            label->setText(QString("Current Value: %1").arg(newValue));
        });
    }
};

主程序 main.cpp

#include <QApplication>
#include <QDataWidgetMapper>
#include <QLabel>
#include <QSlider>
#include <QVBoxLayout>
#include <QWidget>
#include "ViewModel.h"
#include "BindingUtils.h"

int main(int argc, char *argv[]) 
{
    QApplication app(argc, argv);

    ViewModel viewModel;
    QWidget window;
    QSlider *slider = new QSlider(Qt::Horizontal);
    QLabel *label = new QLabel("Current Value: 0");
    slider->setRange(0, 100);

    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(slider);
    layout->addWidget(label);
    window.setLayout(layout);

    QDataWidgetMapper mapper;
    mapper.setModel(&viewModel);
    mapper.addMapping(slider, 0, "value");
    mapper.toFirst();

    BindingUtils::bindValue(slider, &viewModel);
    BindingUtils::bindValue(label, &viewModel);

    window.show();
    return app.exec();
}

3. 页面导航模块的更新

  • 场景: 需要在多个页面之间进行导航,并保持页面的状态一致性。使用路由系统可以更灵活地管理页面跳转。
  • 改进: 页面导航(Router): 增强 Router 模块,通过路由路径映射页面和参数,支持复杂的页面跳转和状态管理。

导航控制器(NavigationController)

页面导航管理: 导航控制器管理页面堆栈和页面跳转逻辑,确保应用程序的页面导航流畅。
页面注册与切换: 通过注册页面,导航控制器可以在需要时切换到相应页面,并管理页面的显示、隐藏和堆栈操作。

class NavigationController : public QObject 
{
    Q_OBJECT

public:
    NavigationController(QStackedWidget *stack, QObject *parent = nullptr)
        : QObject(parent), stackedWidget(stack) {}

    void navigateTo(const QString &pageName) 
    {
        if (!pageRegistry.contains(pageName))
         {
            qWarning() << "Page not registered:" << pageName;
            return;
        }

        QWidget *page = pageRegistry[pageName];
        stackedWidget->setCurrentWidget(page);
    }

    void registerPage(const QString &pageName, QWidget *page) 
    {
        if (!pageRegistry.contains(pageName)) 
        {
            pageRegistry[pageName] = page;
            stackedWidget->addWidget(page);
        }
    }

    void goBack()
     {
        if (stackedWidget->currentIndex() > 0)
        {
            stackedWidget->setCurrentIndex(stackedWidget->currentIndex() - 1);
        }
    }

private:
    QStackedWidget *stackedWidget;
    QHash<QString, QWidget*> pageRegistry;
};

路由系统(Router)

路径与页面的映射: 路由系统根据路径将请求映射到对应的页面,支持路径解析和参数传递。
导航请求的统一处理: 所有的页面跳转请求通过路由系统处理,使得页面跳转逻辑集中化、易于管理。

class Router : public QObject 
{
    Q_OBJECT

public:
    Router(NavigationController *navController, QObject *parent = nullptr)
        : QObject(parent), navController(navController) {}

    void navigate(const QString &path, const QVariantMap &params = {})
     {
        QString pageName = resolvePageNameFromPath(path);
        navController->navigateTo(pageName);

        // 在页面跳转时,可以通过 ViewModel 传递参数
        if (viewModels.contains(pageName))
         {
            viewModels[pageName]->updateWithParams(params);
        }
    }

    void registerViewModel(const QString &pageName, ViewModel *viewModel)
     {
        viewModels[pageName] = viewModel;
    }

private:
    NavigationController *navController;
    QHash<QString, ViewModel*> viewModels;

    QString resolvePageNameFromPath(const QString &path)
     {
        return path.section('/', 1, 1);
    }
};

当某些耗时操作本身需要开几个子线程来完成,通常意味着这些操作非常复杂,可能涉及多步计算或并行处理。在这种情况下,需要更加精细地管理线程,以确保操作的并发性和主线程的响应性。以下是处理这种情况的推荐方法和架构设计。

4. 综合架构更新

综合考虑上述的改进,软件架构将包括以下模块:

  • 命令模式:用于封装按键操作,并通过线程池处理耗时操作。
  • MVVM 模式: 通过 ViewModel 管理界面的参数和业务逻辑,保持界面和逻辑的分离。
  • 页面导航模块: 路由系统用于管理页面之间的导航,并支持参数传递和页面状态管理。
  • 事件总线:负责模块间的事件传递,确保系统的实时响应和松耦合。

5. 更新后的主程序集成

int main(int argc, char *argv[])
 {
    QApplication app(argc, argv);

    // 初始化堆叠界面
    QStackedWidget stackedWidget;

    // 初始化模块
    ParameterManager paramManager;
    HardwareControlModule hardwareControlModule;
    ButtonHandlerModule buttonHandlerModule(&hardwareControlModule);
    UIManager uiManager(&stackedWidget);
    Router router(&uiManager);

    // 初始化页面和 ViewModel
    Page1ViewModel *page1ViewModel = new Page1ViewModel(&paramManager);
    Page1View *page1View = new Page1View(page1ViewModel);

    // 注册页面和 ViewModel
    uiManager.registerPage("page1", page1View);
    router.registerViewModel("page1", page1ViewModel);

    // 显示堆叠窗口
    stackedWidget.show();
    return app.exec();
}

6. 总结

这个更新后的架构通过引入命令模式和 MVVM 模式来增强代码的结构和可维护性,确保耗时操作不会阻塞主线程,同时保持界面与业务逻辑的清晰分离。页面导航模块通过路由系统管理页面跳转和参数传递,确保页面之间的状态一致性和灵活性。这种设计可以满足复杂硬件系统的实时性需求,并在应用程序中提供良好的用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值