命令模式(Command)

别名

  • 动作(Action)
  • 事务(Transaction)

定义

命令是一种行为设计模式它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作

前言

1. 问题

假如你正在开发一款新的文字编辑器,当前的任务是创建一个包含多个按钮的工具栏,并让每个按钮对应编译器的不同操作。你创建了一个「Button」类。它不仅用于生成工具栏上的按钮,还可用于生成各种对话框的通用按钮

尽管所有按钮看上去都很相似,但它们可以完成不同的操作(打开、保存、打印和应用等)。你会在哪里放置这些按钮的点击处理代码呢?最简单的解决方案是在使用按钮的每个地方都创建大量的子类。这些子类中包含按钮点击后必须执行的代码

你很快就意识到这种方式有严重缺陷。首先,你创建了大量的子类,当每次修改基类 按钮 时,你都有可能需要修改所有子类的代码。简单来说,GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。

还有另外一个问题最难办。复制/粘贴文字等操作可能会在多个地方被调用。例如用户可以点击工具栏上的“复制”按钮,或者通过右键菜单复制一些文字,又或者直接使用键盘上的Ctrl+C。此时你要么需要将操作代码复制进许多个类中,要么需要让右键菜单依赖于按钮,而后者是更糟糕的选择。

2. 解决方案

优秀的软件设计通常会将关注点进行分离,而这往往会导致软件的分层。最常见的例子:一层负责用户图像界面;另一层负责业务逻辑。GUI 层负责在屏幕上渲染美观的图形,捕获所有输入并显示用户和程序工作的结果。当需要完成一些重要内容时(比如计算月球轨道或撰写年度报告),GUI 层则会将工作委派给业务逻辑底层。

这在代码中看上去就像这样:一个 GUI 对象传递一些参数来调用一个业务逻辑对象。这个过程通常被描述为一个对象发送请求给另一个对象。

命令模式建议将请求的所有细节(例如调用的对象、方法名称和参数列表)抽取出来组成命令类,该类中仅包含一个触发请求的方法。

命令对象负责连接不同的 GUI 和业务逻辑对象。此后,GUI对象无需了解业务逻辑对象是否获得了请求,也无需了解其对请求进行处理的方式。GUI 对象触发命令即可,命令对象会自行处理所有细节工作。

下一步是让所有命令实现相同的接口。该接口通常只有一个没有任何参数的执行方法,让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。此外还有额外的好处,现在你能在运行时切换连接至发送者的命令对象,以此改变发送者的行为。

你可能会注意到遗漏的一块拼图——请求的参数。GUI 对象可以给业务层对象提供一些参数。但执行命令方法没有任何参数,所以我们如何将请求的详情发送给接收者呢?答案是:使用数据对命令进行预先配置,或者让其能够自行获取数据。

让我们回到文本编辑器。应用命令模式后,我们不再需要任何按钮子类来实现点击行为。我们只需在「Button」基类中添加一个成员变量来存储对于命令对象的引用,并在点击后执行该命令即可。

你需要为每个可能的操作实现一系列命令类,并且根据按钮所需行为将命令和按钮连接起来。其他菜单、快捷方式或整个对话框等 GUI 元素都可以通过 同方式来实现。当用户与 GUI 元素交互时,与其连接的命令将会被执行。现在你很可能已经猜到了,与相同操作相关的元素将会被连接到相同的命令,从而避免了重复代码。

最后,命令成为了减少 GUI 和业务逻辑层之间耦合的中间层。而这仅仅是命令模式所提供的一小部分好处!

结构

  1. 发送者(Sender)——亦称“触发者(Invoker)”——类负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的引用。发送者触发命令,而不向接收者直接发送请求。注意,发送者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令。
  2. 命令(Command)接口通常仅声明一个执行命令的方法。
  3. 具体命令(Concrete Commands)会实现各种类型的请求。具体命令自身并不完成工作,而是会将调用委派给一个业务逻辑对象。但为了简化代码,这些类可以进行合并。接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变,仅允许通过构造函数对这些成员变量进行初始化。
  4. 接收者(Receiver)类包含部分业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作。
  5. 客户端(Client)会创建并配置具体命令对象。客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后,生成的命令就可以与一个或多个发送者相关联了。

适用场景

  • 如果你需要通过操作来参数化对象,可使用命令模式。

命令模式可将特定的方法调用转化为独立对象。这一改变也带来了许多有趣的应用:你可以将命令作为方法的参数进行传递、将命令保存在其他对象中,或者在运行时切换已连接的命令等。

举个例子: 你正在开发一个 GUI 组件(例如上下文菜单),你希望用户能够配置菜单项,并在点击菜单项时触发操作。

  • 如果你想要将操作放入队列中、操作的执行或者远程执行操作,可使用命令模式。

同其他对象一样,命令也可以实现序列化(序列化的意思是转化为字符串),从而能方便地写入文件或数据库中。一段时间后,该字符串可被恢复成为最初的命令对象。因此,你可以延迟或计划命令的执行。但其功能远不止如此!使用同样的方式,你还可以将命令放入队列、记录命令或者通过网络发送命令。

  • 如果你想要实现操作回滚功能,可使用命令模式。

尽管有很多方法可以实现撤销和恢复功能,但命令模式可能是其中最常用的一种。

为了能够回滚操作,你需要实现已执行操作的历史记录功能。命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。

这种方法有两个缺点。首先,程序状态的保存功能并不容易实现,因为部分状态可能是私有的。你可以使用备忘录模式来在一定程度上解决这个问题。其次,备份状态可能会占用大量内存。因此,有时你需要借助另一种实现方式:命令无需恢复原始状态,而是执行反向操作。反向操作也有代价:它可能会很难甚至是无法实现。

实现方式

  1. 声明仅有一个执行方法的命令接口。
  2. 抽取请求并使之成为实现命令接口的具体命令类。每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。所有这些变量的数值都必须通过命令构造函数进行初始化。
  3. 找到担任发送者职责的类。在这些类中添加保存命令的成员变量。发送者只能通过命令接口与其命令进行交互。发送者自身通常并不创建命令对象,而是通过客户端代码获取。
  4. 修改发送者使其执行命令,而非直接将请求发送给接收者。
  5. 客户端必须按照以下顺序来初始化对象:
    1. 创建接收者。

    2. 创建命令,如有需要可将其关联至接收者。

    3. 创建发送者并将其与特定命令关联。

优点

  • 单一职责原则。你可以解耦触发和执行操作的类。
  • 开闭原则。你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
  • 你可以实现撤销和恢复功能。
  • 你可以实现操作的延迟执行。
  • 你可以将一组简单命令组合成一个复杂命令。

缺点

代码可能会变得更加复杂,因为你在发送者和接收者之间增 加了一个全新的层次。

Invoker.hpp

#ifndef B71D1E18_43C5_4E09_9AB9_161093534782
#define B71D1E18_43C5_4E09_9AB9_161093534782#include <memory>
#include "Command.hpp"// 触发者: 遥控器
class Controller{public:Controller() {}// 设置命令void setCommand(std::shared_ptr<Command> cmd) {cmd_ = cmd;}// 执行命令void executeCommand() {cmd_->execute();}private:std::shared_ptr<Command> cmd_;
};#endif /* B71D1E18_43C5_4E09_9AB9_161093534782 */

Command.hpp

#ifndef EF09C100_BEAE_48B1_8E2C_137F0535C084
#define EF09C100_BEAE_48B1_8E2C_137F0535C084class Command {public:virtual void execute() = 0;
};
#endif /* EF09C100_BEAE_48B1_8E2C_137F0535C084 */

ConcreteCommand.hpp

#ifndef E726585F_0377_4DBA_B7B4_F6EE62F19BA2
#define E726585F_0377_4DBA_B7B4_F6EE62F19BA2
#include <memory>
#include "Command.hpp"
#include "Receiver.hpp"// 具体命令类: 打开电视
class TVOpenCommand : public Command{public:explicit TVOpenCommand(std::shared_ptr<Television> tv) : tv_(tv) {}void execute() {tv_->open();}private:std::shared_ptr<Television> tv_;
};// 具体命令类: 关闭电视
class TVCloseCommand : public Command{public:explicit TVCloseCommand(std::shared_ptr<Television> tv) : tv_(tv) {}void execute() {tv_->close();}private:std::shared_ptr<Television> tv_;
};// 具体命令类: 切换频道
class TVChangeCommand : public Command{public:explicit TVChangeCommand(std::shared_ptr<Television> tv) : tv_(tv) {}void execute() {tv_->changeChannel();}private:std::shared_ptr<Television> tv_;
};#endif /* E726585F_0377_4DBA_B7B4_F6EE62F19BA2 */

Receiver.hpp

#ifndef C52A5845_A281_4382_9FCA_613DC0950A3E
#define C52A5845_A281_4382_9FCA_613DC0950A3E#include <iostream>// 接受者: 电视
class Television{public:void open() {std::cout << "打开电视机!" << std::endl;}void close() {std::cout << "关闭电视机!" << std::endl;}void changeChannel(){std::cout << "切换电视频道!" << std::endl;}
};#endif /* C52A5845_A281_4382_9FCA_613DC0950A3E */

main.cpp

#include "Invoker.hpp"
#include "ConcreteCommand.hpp"int main() {// 接收者: 电视机std::shared_ptr<Television> tv = std::make_shared<Television>();// 命令std::shared_ptr<Command> openCommand = std::make_shared<TVOpenCommand>(tv);std::shared_ptr<Command> closeCommand = std::make_shared<TVCloseCommand>(tv);std::shared_ptr<Command> changeCommand = std::make_shared<TVChangeCommand>(tv);// 调用者: 遥控器std::shared_ptr<Controller> controller = std::make_shared<Controller>();// 测试controller->setCommand(openCommand);controller->executeCommand();controller->setCommand(closeCommand);controller->executeCommand();controller->setCommand(changeCommand);controller->executeCommand();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/4833.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【PCB专题】Allegro如何设置电源电压属性,将飞线隐藏?

在PCB设计过程中,布局完成之后的布线的顺序一般是先走信号线,然后进行电源的处理、分割。因为电源往往在整个板子上是都有的,所以电源的飞线是非常多,在布线时特别影响其他信号线的布线,界面看着比较杂乱。 如下所示GND和1.8V都存在各种飞线,比较杂乱。 Allegro中有设置…

学习机组过程中的疑难问题与解决 -----(1)

本文章是在学习计算机组成原理过程中个人感觉需要理解与记忆的问题&#xff0c;还有一些在学习过程中自己产生的疑问以及解答,本文章可能排版不良&#xff0c;精力有限&#xff0c;还请见谅 第一章&#xff1a; &#xff08;1&#xff09;MAR的位数对应着存储单元的个数&#…

JVM学习整理(一)

一、JVM的基本介绍 JVM 是 Java Virtual Machine 的缩写&#xff0c;它是一个虚构出来的计算机&#xff0c;一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现 好&#xff0c;其实抛开这么专业的句子不说&#xff0c;就知道JVM其实就类似于一台小电脑运行在windows或…

JSP网上订餐管理系统用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 JSP 网上订餐管理系统是一套完善的web设计系统&#xff0c;对理解JSP java SERLVET mvc编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&a…

Java微服务金融项目智牛股 项目简介与金融知识介绍及技术特点

项目简介 金融交易平台服务于金融衍生产品&#xff0c; 包含外汇、贵金属、期货、股票。 各产品具有不同属性与交易规则&#xff0c; 本项目对标MT4等大型交易平台&#xff0c; 遵循FIX全球最广泛的金融市场通用协议。 实现从证券注册开户、行情订阅与呈现&#xff0c; 股票撮合…

【GPT】如何拥有离线版本的GPT以及部署过程中的问题

【背景】 目前很多公司由于数据安全的问题&#xff0c;不能使用OpenAI的GPT&#xff0c;同时也没有必要非得使用如此泛用化的GPT。很多公司因此有训练自己的离线GPT的需求&#xff0c;这样的GPT只需要具备专业知识即可。 要使这个成为可能&#xff0c;首先就需要能够让GPT的Mo…

SAP ABAP 如果某字段没有参数ID,如自开发程序使用的自建表 新建参数ID

1&#xff09;新建参数ID sm30 TPARA 维护 输入ID和描述 2&#xff09; 参数ID和Se11数据元素 绑定

【MySQL学习笔记】(六)MySQL基本查询

表的增删查改 1 create1.1 单行数据全列插入1.2 多行数据 指定列插入1.3 插入否则更新1.4 替换 2 retrieve2.1 select列2.1.1 全列查询2.2.2 指定列查询2.2.3 查询字段为表达式2.2.4 为查询结果指定别名2.2.5 结果去重 2.2 where 条件2.2.1 null的查询 2.3 结果排序2.4 筛选分…

Leetcode 75算法题( 上)(使用C++实现)

数组 / 字符串 1768.交替合并字符串 两个字符串 word1 和 word2 。请你从 word1 开始&#xff0c;通过交替添加字母来合并字符串。如果一个字符串比另一个字符串长&#xff0c;就将多出来的字母追加到合并后字符串的末尾。 返回 合并后的字符串 。 ​ 代码如下&#xff1a…

初识SpringMVC

HelloSpringMVC 1.新建一个moudle&#xff0c;添加web的支持 2.导入了SpringMVC的依赖 <!-- 依赖--><dependencies> <!-- Junit --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version&…

如何找到更多音视频开发学习资料和资源?

如果你对学习音视频开发感兴趣&#xff0c;以下是一些建议&#xff0c;可以帮助你获取更多相关的资料和资源&#xff1a; 在线学习平台&#xff1a;参考一些知名的在线学习平台&#xff0c;如Coursera、Udemy、edX等&#xff0c;搜索他们的课程目录&#xff0c;看是否有与音视频…

基于深度学习的高精度条形码二维码检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度条形码二维码检测识别系统可用于日常生活中或野外来检测与定位条形码二维码目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的条形码二维码目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系…