设计模式24-命令模式

news/2024/9/14 9:42:27/ 标签: 设计模式, 命令模式

设计模式24-命令模式

  • 写在前面
    • 行为变化模式
  • 命令模式的动机
  • 定义与结构
    • 定义
    • 结构
  • C++ 代码推导
  • 优缺点
  • 应用场景
  • 总结
  • 补充
    • 函数对象(Functors)
      • 定义
      • 具体例子
        • 示例:使用函数对象进行自定义排序
        • 代码说明
        • 输出结果
        • 具体应用
      • 优缺点
      • 应用场景
    • 命令模式(Command Pattern)
      • 定义
      • 实现
      • 优缺点
      • 应用场景
    • 对比
    • 选择

写在前面

行为变化模式

  • 在组件的构建过程中,组件行为的变化经常导致组件本身剧烈的变化。行为变化模式,将组件的行为和组件本身进行解构。从而支持组建行为的变化。实现两者之间的松耦合。
  • 行为变化模式通常指的是一类设计模式,它们允许对象在运行时根据状态或环境的变化动态地改变行为。这类模式通过将算法、职责或行为的变化封装起来,使得系统更具灵活性和可扩展性。
  • 行为变化模式通过封装行为、状态或算法的变化,使得系统更加灵活和可扩展。这类模式在解决动态变化需求、减少代码复杂性、提高系统的可维护性等方面具有重要作用。然而,在选择具体模式时,应根据系统的实际需求和复杂度进行权衡,以避免过度设计和不必要的类增加。

典型模式
命令模式
访问器模式

命令模式的动机

  • 在软件构建过程中,行为请求者与行为实现者通常呈现一种紧耦合。但在某些场合比如需要对行为进行,记录,撤销,重做等处理。这种无法抵御变化的解耦合是不合适的。
  • 那么在这种情况下,如何将行为请求者与行为实现者进行解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
  • 在许多应用中,程序需要向某个对象发送请求,但发送者并不知道请求的接收者是谁,也不知道请求的执行方式。为了实现请求的解耦,命令模式应运而生。命令模式的动机是将“请求”封装为对象,使得可以用不同的请求、队列或日志来参数化对象。命令模式允许请求的发送者与执行者解耦,并且提供了对请求排队、撤销/重做等功能的支持。

定义与结构

定义

命令模式(Command Pattern)是一种行为设计模式,它将一个请求封装为一个对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

结构

在这里插入图片描述
这张UML类图描述的是软件设计模式中的命令模式(Command Pattern)。命令模式是一种行为设计模式,它允许将一个请求封装为一个对象,从而使可用不同的请求、队列、日志来参数化其他对象。命令模式也支持可撤销的操作。根据这张图来详细解释命令模式的主要组成部分和它们之间的关系。

  1. 客户端(Client)

    • 客户端是命令模式的发起者。它创建具体的命令对象,并设置命令的接收者。然后,它通过调用者(Invoker)来执行这个命令。
  2. 调用者(Invoker)

    • 调用者对象负责执行命令。它不直接了解命令的接收者(Receiver)和命令的具体实现(即具体命令),而只是持有对命令对象的引用。调用者有一个执行命令的接口,如Execute(),它接受命令对象作为参数并调用命令的execute()方法。
  3. 命令(Command)

    • 命令是一个接口或抽象类,它定义了执行命令的接口execute()。所有的具体命令类都实现这个接口,并在execute()方法中实现具体的执行逻辑。
  4. 接收器(Receiver)

    • 接收器是命令的实际执行者。它知道如何执行与请求相关的操作。在命令模式中,接收器通常会有一些方法(如Action()),这些方法会在命令执行时被调用。
  5. 具体命令(ConcreteCommand)

    • 具体命令是命令接口的实现类。它持有对接收者的引用,并在其execute()方法中调用接收者的方法(如Action())。这样,具体命令就封装了接收者和调用的具体操作。
  6. 状态(State)(在图中为隐式,通过receiverstate的连接表示):

    • 状态通常不是命令模式的核心部分,但在这张图中通过receiverstate的连接暗示了接收器可能与状态有关。在命令模式的实际应用中,接收器可能会维护一些状态信息,这些状态信息会在执行命令时被读取或修改。
  7. 执行(Execute)(在图中以方法形式出现):

    • Execute()方法是调用者用于执行命令的方法。它接受一个命令对象作为参数,并调用该命令对象的execute()方法。Execute()方法和execute()方法名称上的差异表示了它们是不同类中的方法,但它们共同构成了命令模式的核心执行逻辑。

图中的箭头和依赖关系

  • Client指向Invoker:表示客户端创建并设置调用者。
  • Invoker指向Command:表示调用者持有对命令对象的引用。
  • Command指向Execute()(方法):这是命令接口中定义的方法。
  • Execute()(在Invoker中)指向Receiver(通过具体命令):表示调用者通过命令对象间接与接收器交互。
  • ReceiverConcreteCommand之间的关系是隐式的,因为具体命令持有对接收器的引用。
  • ConcreteCommand指向Execute()(在ConcreteCommand中):这是具体命令实现execute()方法的地方。
  • receiver->Action();表示在命令执行时,接收器的Action()方法被调用。

命令模式的主要优点是解耦了调用者和接收者,增加了命令的灵活性,支持可撤销操作和宏命令等高级功能。

C++ 代码推导

下面是一个简单的命令模式实现例子,用于遥控器控制灯光的开关。

#include <iostream>
#include <memory>
#include <vector>// 命令接口
class Command {
public:virtual ~Command() = default;virtual void execute() = 0;virtual void undo() = 0;
};// 接收者类
class Light {
public:void on() {std::cout << "Light is On" << std::endl;}void off() {std::cout << "Light is Off" << std::endl;}
};// 具体命令类:开灯命令
class LightOnCommand : public Command {
public:LightOnCommand(Light& light) : light_(light) {}void execute() override {light_.on();}void undo() override {light_.off();}private:Light& light_;
};// 具体命令类:关灯命令
class LightOffCommand : public Command {
public:LightOffCommand(Light& light) : light_(light) {}void execute() override {light_.off();}void undo() override {light_.on();}private:Light& light_;
};// 调用者类:遥控器
class RemoteControl {
public:void setCommand(std::shared_ptr<Command> command) {command_ = command;}void pressButton() {if (command_) {command_->execute();history_.push_back(command_);}}void pressUndo() {if (!history_.empty()) {history_.back()->undo();history_.pop_back();}}private:std::shared_ptr<Command> command_;std::vector<std::shared_ptr<Command>> history_;
};int main() {Light light;std::shared_ptr<Command> lightOn = std::make_shared<LightOnCommand>(light);std::shared_ptr<Command> lightOff = std::make_shared<LightOffCommand>(light);RemoteControl remote;remote.setCommand(lightOn);remote.pressButton();  // 打开灯remote.setCommand(lightOff);remote.pressButton();  // 关闭灯remote.pressUndo();  // 撤销关闭灯,重新打开灯return 0;
}

优缺点

优点

  1. 解耦发送者与接收者命令模式将请求的发送者与实际执行者解耦,使得可以在不修改发送者代码的情况下更改或扩展接收者。
  2. 增加灵活性:可以容易地将新命令加入系统,支持撤销/重做、日志记录、事务等功能。
  3. 组合命令:可以将多个命令组合成一个复合命令,从而实现更复杂的功能。
  4. 支持宏命令:可以方便地实现批处理,多个命令组合成宏命令一起执行。

缺点

  1. 命令类数量可能增加:对于每个不同的操作,都需要设计一个具体命令类,这可能导致类的数量增多。
  2. 过度设计:对于简单的操作,命令模式可能显得过于复杂。

应用场景

  1. 操作的可撤销性:如文本编辑器中的撤销/重做操作。
  2. 事务性操作:如数据库的事务处理,需要对操作进行记录,以便在出现错误时回滚。
  3. 远程调用:如远程控制系统,需要将操作封装为命令,通过网络发送给远程服务器执行。
  4. 宏命令:如在家居自动化中,一个按键可以执行一系列命令(如同时关闭所有灯光、锁门等)。

命令模式在需要灵活性、扩展性,以及解耦请求和执行者的场景中非常有用。它不仅提高了系统的可维护性,还为功能的拓展提供了良好的支持。

总结

  • 命令模式的根本目的在于将行为请求者与行为实现者进行解耦。在面向对象语言中常见的实现手段是将行为抽象为对象。
  • 实现命令接口的具体命令对象,有时候根据需要可能会保存一些额外的状态信息。通过使用组合模式可以将多个命令封装为一个复合命令也就是宏命令。
  • 命令模式与c++中的函数对象有些类似。但两者定义行为接口的规范有所区别。命令模式以面向对象中的接口实现来定义行为接口规范。更严格,但是具有性能损失的缺点。C++函数对象以函数签名(参数+返回值)来定义行为接口规范。更灵活性能更高。

补充

C++的函数对象(Functors)和命令模式(Command Pattern)都是将操作封装为对象的技术,但它们的实现方式、用途和适用场景有所不同。下面是它们的对比、优缺点以及应用场景的详细说明。

函数对象(Functors)

定义

在C++中,函数对象是指重载了operator()的类对象。通过重载operator(),类的实例能够像函数一样被调用。这种机制允许将行为封装在对象中,并使其可以在需要时调用。

具体例子

C++的函数对象(Functors)通过重载operator()来实现,使得类对象能够像函数一样被调用。这种特性在许多场景下非常有用,比如在标准库算法中传递行为、创建灵活的回调函数等。以下是一个更具体的例子,演示如何使用函数对象来创建一个自定义的排序规则。

示例:使用函数对象进行自定义排序

假设我们有一个包含学生成绩的向量,我们希望根据学生的成绩进行排序,但如果成绩相同,则按学生的名字进行字母排序。我们可以通过定义一个函数对象来实现这个自定义的排序规则。

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>// 定义一个Student结构体,包含姓名和成绩
struct Student {std::string name;int grade;
};// 定义一个函数对象,用于自定义排序规则
class CompareStudents {
public:// 重载operator(),实现自定义排序规则bool operator()(const Student& a, const Student& b) const {if (a.grade != b.grade) {return a.grade > b.grade; // 成绩从高到低排序} else {return a.name < b.name;   // 如果成绩相同,按名字字母顺序排序}}
};int main() {// 创建一个学生列表std::vector<Student> students = {{"Alice", 90},{"Bob", 85},{"Charlie", 90},{"David", 85},{"Eve", 92}};// 使用std::sort和自定义的函数对象进行排序std::sort(students.begin(), students.end(), CompareStudents());// 输出排序后的学生列表for (const auto& student : students) {std::cout << student.name << ": " << student.grade << std::endl;}return 0;
}
代码说明
  1. Student 结构体:包含两个成员变量,name(学生姓名)和grade(学生成绩)。
  2. CompareStudents 函数对象:这个类重载了operator(),用于定义自定义的排序规则。
    • 当两个学生的成绩不相同时,按成绩从高到低排序。
    • 当两个学生的成绩相同时,按名字的字母顺序进行排序。
  3. 排序操作
    • 使用std::sort函数对students向量进行排序。
    • std::sort的第三个参数是排序规则,这里传递的是CompareStudents类的一个实例。
输出结果

程序运行后,将按自定义规则对学生列表进行排序,并输出如下结果:

Eve: 92
Alice: 90
Charlie: 90
Bob: 85
David: 85
具体应用

在实际应用中,函数对象可以用于实现任何需要灵活行为的场景。例如:

  • 排序规则:如上例,函数对象可以用于自定义排序规则。
  • 回调函数:函数对象可以作为回调函数传递给其他函数或类,用于事件处理、数据处理等。
  • 算法参数化:在标准库算法如std::for_eachstd::transform等中,函数对象可以作为参数,用于定义具体的操作。

函数对象通过类的机制实现了行为的封装和状态的管理,同时保持了类似函数的调用方式,非常适合需要灵活性和状态保持的场景。

优缺点

优点

  1. 简洁:函数对象通常不需要额外的基础设施(如接口、抽象类),实现较为简洁。
  2. 内联性:由于函数对象通常是小型类,编译器能够更容易地进行内联优化,提升性能。
  3. 状态保持:函数对象可以在对象内部保存状态,且可以多次使用状态。

缺点

  1. 缺乏结构化:对于复杂的行为或系统,函数对象的设计可能会变得不够结构化和灵活。
  2. 扩展性较差:函数对象一般用于实现简单操作,难以扩展到更复杂的行为控制。

应用场景

  1. 标准库算法:如std::sortstd::for_each等标准库算法,常常接受函数对象作为参数。
  2. 简单的回调:在不需要完整命令模式的地方,可以使用函数对象来代替回调函数。

命令模式(Command Pattern)

定义

命令模式是一种行为设计模式,它将请求封装为对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

实现

以下是命令模式的一个简化实现:

#include <iostream>
#include <memory>// 命令接口
class Command {
public:virtual ~Command() = default;virtual void execute() = 0;
};// 具体命令类
class LightOnCommand : public Command {
public:void execute() override {std::cout << "Light is On" << std::endl;}
};// 调用者类
class RemoteControl {
public:void setCommand(std::shared_ptr<Command> command) {command_ = command;}void pressButton() {if (command_) {command_->execute();}}private:std::shared_ptr<Command> command_;
};int main() {RemoteControl remote;std::shared_ptr<Command> lightOn = std::make_shared<LightOnCommand>();remote.setCommand(lightOn);remote.pressButton();  // 输出 "Light is On"return 0;
}

优缺点

优点

  1. 解耦性命令模式将请求的发送者与接收者解耦,使得可以轻松地交换、增加或删除命令。
  2. 可扩展性:可以很容易地添加新的命令,不影响其他命令的实现。
  3. 支持复杂功能:如命令的排队、撤销、重做、日志记录等。

缺点

  1. 复杂性:实现命令模式需要额外的命令类,这会增加系统的复杂性和代码量,尤其是在简单场景下。
  2. 开销较大:创建命令对象和维护这些对象的生命周期需要额外的资源开销。

应用场景

  1. 远程操作和请求:例如远程控制设备、网络请求处理。
  2. 撤销/重做操作:如文本编辑器、图像处理软件中的撤销/重做功能。
  3. 事务管理:如数据库的事务管理,将一系列操作封装为命令对象。

对比

  • 复杂性:函数对象适用于较为简单的操作,易于实现且不需要复杂的设计模式命令模式适用于复杂的场景,需要明确的结构和解耦要求。
  • 灵活性命令模式比函数对象更灵活,适用于需要多种操作的场景,而函数对象更适合单一功能的封装。
  • 状态管理:函数对象天然支持内部状态的管理;命令模式需要在设计时明确状态的存储和操作。

选择

  • 当操作简单且不需要太多结构化时,使用函数对象是更简洁的选择。
  • 当操作复杂、需要解耦或者需要管理操作的生命周期(如撤销、重做)时,命令模式是更好的选择。

http://www.ppmy.cn/news/1516720.html

相关文章

鸿蒙位置服务

位置服务 1、首先申请权限 在module.json5文件下申请位置权限 "requestPermissions": [{"name": "ohos.permission.LOCATION", // 权限名称,为系统已定义的权限"reason": "$string:location_reason", // 申请权限的原因,…

windows 核心编程第五章:演示作业的使用及获取统计信息

演示作业的使用及获取统计信息 演示作业的使用及获取统计信息 文章目录 演示作业的使用及获取统计信息演示作业的使用及获取统计信息 演示作业的使用及获取统计信息 /* 演示作业的使用及获取统计信息 */#include <stdio.h> #include <Windows.h> #include <tc…

HBase原理和操作

目录 一、HBase在Zookeeper中的存储元数据信息集群状态信息 二、HBase的操作Web Console命令行操作 三、HBase中数据的保存过程 一、HBase在Zookeeper中的存储 元数据信息 HBase的元数据信息是HBase集群运行所必需的关键数据&#xff0c;它存储在Zookeeper的"/hbase&quo…

ARM32开发——(七)GD32F4串口引脚_复用功能_查询

1. GD32F4串口引脚查询 TX RX CK CTS RTS USART0 PA9,PA15,PB6 PA10,PB3,PB7 PA8 PA11 PA12 USART1 PA2,PD5 PA3,PD6 PA4,PD7 PA0,PD3 PA1,PD4 USART2 PB10,PC10,PD8 PB11,PC5,PD9 PB12,PC12,PD10 PB13,PD11 PB14,PD12 UART3 PA0,PC10 PA1,PC11 …

kafka 入门

kafka 有分区和副本的概念&#xff0c;partition 3 表示有3个分区&#xff0c;replication 2 表示有2个副本 通过 --describe --topic test命令可以知道 test这个 主题的分区和副本情况&#xff0c;途中的replicas 表示 其他副本分区的情况&#xff0c;如第一条&#xff0c;t…

【运筹学】【数据结构】【经典算法】最小生成树问题及贪心算法设计

1 知识回顾 我们已经讲过最小生成树问题的基础知识&#xff0c;我们现在想要利用贪心算法解决该问题。我们再来回顾一下最小生成树问题和贪心算法的基础知识。 最小生成树问题就是从某个图中找出总权重最小的生成树。 贪心算法是一种算法设计范式&#xff0c;每一步都选…

深度学习学习经验——全连接神经网络(FCNN)

什么是全连接神经网络&#xff1f; 全连接神经网络&#xff08;FCNN&#xff09;是最基础的神经网络结构&#xff0c;它由多个神经元组成&#xff0c;这些神经元按照层级顺序连接在一起。每一层的每个神经元都与前一层的每个神经元连接。 想象你在参加一个盛大的晚会&#xf…

Vue中的this.$emit()方法详解【父子组件传值常用】

​在Vue中&#xff0c;this.$emit()方法用于触发自定义事件。它是Vue实例的一个方法&#xff0c;可以在组件内部使用。 使用this.$emit()方法&#xff0c;你可以向父组件发送自定义事件&#xff0c;并传递数据给父组件。父组件可以通过监听这个自定义事件来执行相应的逻辑。 …

问界M7 Pro这招太狠了,直击理想L6/L7要害

文 | AUTO芯球 作者 | 雷慢 李想的理想估计要失眠了&#xff0c;为什么啊&#xff1f; 前有L6悬架薄如铁片被曝光&#xff0c;被车主们骂了个狗血淋头&#xff0c; 现在又来个问界M7 Pro版&#xff0c; 24.98万的后驱智驾版就上华为ADS主视觉智驾了&#xff0c; 两个后驱&…

TMDOG的微服务之路_07——初入微服务,NestJS微服务快速入门

TMDOG的微服务之路_07——初入微服务&#xff0c;NestJS微服务快速入门 博客地址&#xff1a;TMDOG的博客 在前几篇博客中&#xff0c;我们探讨了如何在 NestJS 中的一些基础功能&#xff0c;并可以使用NestJS实现一个简单的单体架构后端应用。本篇博客&#xff0c;我们将进入…

基于改进YOLOv8的景区行人检测算法

贵向泉, 刘世清, 李立, 秦庆松, 李唐艳. 基于改进YOLOv8的景区行人检测算法[J]. 计算机工程, 2024, 50(7): 342-351. DOI: 10.19678/j.issn.10 原文链接如下&#xff1a;基于改进YOLOv8的景区行人检测算法https://www.ecice06.com/CN/rich_html/10.19678/j.issn.1000-3428.006…

解决Element-plus中Carousel(走马灯)图片无法正常加载的bug

前言&#xff1a; 最近帮助朋友解决了一个使用Element-plus中Carousel&#xff08;走马灯&#xff09;图片无法正常加载的bug&#xff0c;经过笔者的不断努力终于实现了&#xff0c;现在跟大家分享一下&#xff1a; 朋友原来的代码是这样的&#xff1a; <template><…

【计算机网络】电路交换、报文交换、分组交换

电路交换&#xff08;Circuit Switching&#xff09;&#xff1a;通过物理线路的连接&#xff0c;动态地分配传输线路资源 ​​​​

依靠 VPN 生存——探索 VPN 后利用技术

执行摘要 在这篇博文中,Akamai 研究人员强调了被忽视的 VPN 后利用威胁;也就是说,我们讨论了威胁行为者在入侵 VPN 服务器后可以用来进一步升级入侵的技术。 我们的发现包括影响 Ivanti Connect Secure 和 FortiGate VPN 的几个漏洞。 除了漏洞之外,我们还详细介绍了一组…

SpringBoot集成kafka-获取生产者发送的消息(阻塞式和非阻塞式获取)

说明 CompletableFuture对象需要的SpringBoot版本为3.X.X以上&#xff0c;需要的kafka依赖版本为3.X.X以上&#xff0c;需要的jdk版本17以上。 1、阻塞式&#xff08;等待式&#xff09;获取生产者发送的消息 生产者&#xff1a; package com.power.producer;import org.ap…

Linux的进程详解(进程创建函数fork和vfork的区别,资源回收函数wait,进程的状态(孤儿进程,僵尸进程),加载进程函数popen)

目录 什么是进程 Linux下操作进程的相关命令 进程的状态&#xff08;生老病死&#xff09; 创建进程系统api介绍&#xff1a; fork() 父进程和子进程的区别 vfork() 进程的状态补充&#xff1a; 孤儿进程 僵尸进程 回收进程资源api介绍&#xff1a; wait() waitpid…

VastBase——全局性能调优

目录 一、系统资源调优 1.内存和CPU 2.网络 3.I/O 二、查询最耗性能的SQL 三、分析作业是否被阻塞 背景&#xff1a;影响性能的因素 系统资源 数据库性能在很大程度上依赖于磁盘的I/O和内存使用情况。为了准确设置性能指标&#xff0c;用户需要了解Vastbase部署硬件的基本…

深信服研发面试经验分享

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485367&idx1&sn837891059c360ad60db7e9ac980a3321&chksmc0e47eebf793f7fdb8fcd7eed8ce29160cf79ba303b59858ba3a6660c6dac536774afb2a6330#rd 《网安面试指南》http://mp.weixin.qq.com/s?…

在Spring Boot项目中集成Geth(Go Ethereum)

在Spring Boot项目中集成Geth&#xff08;Go Ethereum&#xff09;客户端&#xff0c;通常是为了与以太坊区块链进行交互。以下是一些基本的步骤和考虑因素&#xff0c;帮助你在Spring Boot应用程序中集成Geth。 安装Geth 首先&#xff0c;你需要在你的机器上安装Geth。你可以从…

k8s备份etcd3.5

一、思路 1、创建nfs存储类,用作存储备份数据<略> 2、制作用于备份的镜像文件 3、指定cronjob 二、制作镜像 ## dockerfile文件# cat Dockerfile FROM dhub.kubesre.xyz/centos:7 ADD etcdv359.tar / RUN mkdir /snapshot# docker build -t registry.k8s.io/etcd:3.…