命令模式(Command)

news2024/11/10 12:57:21

别名

  • 动作(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_137F0535C084

class 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.coloradmin.cn/o/698378.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(1)深度学习学习笔记-数据操作和处理

文章目录 前言一、张量操作二、csv文件数据操作数据预处理(读入csv文件作为pytorch能处理的) 来源 前言 张量的一些处理和操作 csv文件数据操作 一、张量操作 [&#xff1a;&#xff0c;1]表示全部行 第二列 [&#xff1a;]是全取 [1:3,1&#xff1a;]&#xff1a;1:3表示1~3的…

Node.js搭建Https服务

要搭建一个接收HTTPS请求的Node.js服务器,可以按以下步骤操作: 1. 初始化项目和安装依赖: bashnpm init -y npm install expresslatest npm install httpslatest 2. 生成证书文件: 这里按照自己的需求 去买ssl证书 或者获取免费证书 直接百度 阿里云获取免费证书等关键词即可…

Prefix-Tuning 阅读笔记

《Prefix-Tuning: Optimizing Continuous Prompts for Generation》 核心思想&#xff1a; 微调的时候&#xff0c;把预训练好的大transformer固定住不训练&#xff0c;在大transformer前面拼接几个token的参数&#xff0c;只训练前面这几个token的参数。 为什么有效 因为虽…

金融科技领先者Broadridge选择CloudBees CI来加速软件交付

Broadridge公司是全球金融科技领先者&#xff0c;通过丰富客户参与度、控制风险、优化效率和创造收入增长的解决方案为客户推动业务转型。 借助CloudBees CI&#xff0c;Broadridge为所有使用Jenkins的开发团队提供了集中管理和自助服务的体验。Broadridge能够不断为客户提供新…

vue 中node-sass和sass版本问题解决

先开始使用 “node-sass”: “^7.0.1” “sass-loader”: “^10.0.5” 然后使用npm install报错 后面百度&#xff0c;搜到了是因为 node-sass和sass-loader版本不对应造成。 然后修改版本&#xff0c;增加sass引用 “node-sass”: “^7.0.1” “sass-loader”: “^7.3.1”, “…

Mysql实现按月份查询挂号统计数据(聚合函数的使用)

最近公司要实现一个通过年月来查询每个月的挂号统计&#xff08;当日挂号&#xff0c;预约挂号&#xff09;&#xff0c;因为医院每个月都要来问我们这边一次&#xff0c;每次都去数据库查询太麻烦了&#xff0c;所以就需要开发一个查询挂号统计的接口。 前面用了很多种方法,要…

计算机中的逻辑运算(与、或、非、异或、同或、与非、或非)

计算机中的逻辑运算又被称作为“布尔运算”&#xff0c;分别为&#xff1a;与、或&#xff0c;非&#xff0c;异或。此外在门电路中还有&#xff1a;同或、与非、或非。共七种。 这七种逻辑运算中&#xff0c;只有 逻辑非运算 是一元逻辑运算&#xff08;一个运算操作数&#…

微信公众号接收事件推送XML数据包解析成实体对象

1.从request获取推送xml包 String callBackXml testNoticeService.formatNoticeParams(request); public static String formatNoticeParams(HttpServletRequest request){try(ByteArrayOutputStream output new ByteArrayOutputStream();InputStream input request.getIn…

微服务 springcloud 11 sleuth 链路跟踪,sleuth + zipkin 链路分析

随着系统规模越来越大&#xff0c;微服务之间调用关系变得错综复杂&#xff0c;一条调用链路中可能调用多个微服务&#xff0c;任何一个微服务不可用都可能造整个调用过程失败 spring cloud sleuth 可以跟踪调用链路&#xff0c;分析链路中每个节点的执行情况 01.微服务中添加 …

如何使用UpdraftPlus迁移备份网站到新网站?

这个问题是关于将您的站点克隆或迁移到新的网站URL的问题。如果您想克隆一个站点进行测试&#xff0c;或者在新版本上线之前移动到新主机并进行测试&#xff0c;以及许多其他类似情况&#xff0c;迁移尤其有用。 在下面的示例中&#xff0c;我们迁移到一个完全新的 WordPress …

搭建Hadoop高可用框架分布式集群

搭建Hadoop高可用框架分布式集群 一.基础配置 1.创建虚拟机&#xff0c;修改虚拟机的主机名 2.修改网络配置 master:192.168.6.200 slave1:192.168.6.201 slave2:192.168.6.202 3.互ping测试 4.sudo授权 5.安装vim编辑器 6.配置网络映射 master配置映射 master向slave1传递映…

MATLAB 之 Simulink系统的仿真与分析

这里写目录标题 一、Simulink 系统的仿真与分析1. 设置仿真参数1.1 Solver 参数设置1.2 Data lmport/Export 参数设置 2. 运行仿真与仿真结果分析2.1 运行仿真2.2 仿真结果分析 一、Simulink 系统的仿真与分析 系统的模型建立之后&#xff0c;选择仿真参数和数值算法&#xff…

LabVIEW自适应屏幕分辨率的两种方法

文章目录 前言一、方案1&#xff1a;组合缩放1、举例2、验证 二、方案2&#xff1a;分隔栏匹配窗格1、举例2、验证 前言 前阵子做的一个项目是在显示器分辨率为 2560*1600&#xff0c;缩放选项为 150% 的笔记本上开发的&#xff0c;但是当 vi 文件在另一台显示器分辨率为 1920…

ThingsBoard IoT Gateway 专栏 分享 前言

最近有朋友询问tb-gateway相关的问题&#xff0c;于是抽业余时间查看了一下官方文档&#xff0c;并做了一些尝试和案例。接下来我会将这些分享给大家。这也是一个系列&#xff0c;将会和我的ThingsBoard专栏放在一起。 ThingsBoard IoT Gateway 能够帮助你将连接到传统和第三方…

一次Android APK打包的报错

打包的时候报了一个错 没有记下来 但是解决方法是 打开Window-> Package Manager 移除这个&#xff08;这个是Unity的广告组件

杰西·利弗莫尔股票大作手操盘术策略

文章目录 上升趋势演变多次回撤回升&#xff0c;区间震荡向上突破&#xff0c;恢复上升趋势前期回升高点下挫&#xff0c;趋势逆转警告信号向下突破&#xff0c;确认下降趋势 下降趋势演变多次回升回撤&#xff0c;区间震荡向下突破&#xff0c;恢复下降趋势前期回撤低点反弹&a…

mysql-大数据的上传(load data)

文章目录 1. 大批量数据的上传如果使用insert语句就会很慢&#xff0c;可以使用load data的方式 1. 大批量数据的上传如果使用insert语句就会很慢&#xff0c;可以使用load data的方式 如下&#xff0c;我建立了一个这样的数据表&#xff0c; 2. 我要将以下数据插入数据表&am…

基于FPGA的RC滤波器设计实现

目录 简介&#xff1a; 传递函数 FPGA代码实现 总结 简介&#xff1a; RC滤波器的特性基本情况介绍 RC一阶低通滤波介绍&#xff1b;RC滤波器电路简单&#xff0c;抗干扰性强&#xff0c;有较好的低频性能&#xff0c;并且选用标准的阻容元件易得&#xff0c;所以在工程测…

【滤波】平滑

%matplotlib inline#format the book import book_format book_format.set_style()简介 当你考虑未来的数据时&#xff0c;卡尔曼滤波器的性能并不是最优的。例如&#xff0c;假设我们在跟踪飞行器&#xff0c;最新的观测值突然偏离的很离谱&#xff0c;就像这样&#xff08;我…

PHP的pack/unpack

前言&#xff1a;直接参照官网。 PHP: pack - Manual PHP中文手册 PHP中国镜像 php 国内镜像 PHP官方网站 PHP: unpack - Manual PHP中文手册 PHP中国镜像 php 国内镜像 PHP官方网站 1、作用 &#xff08;1&#xff09;pack&#xff1a;将数据打包成二进制字符串。将输入数据…