《Head First设计模式》读书笔记 —— 命令模式

server/2025/2/27 0:02:02/

文章目录

    • 本节用例
    • 餐厅类比
    • 命令模式
      • 第一个命令对象
        • 实现命令接口
        • 实现一个命令
      • 使用命令对象
      • NoCommand与空对象
    • 定义命令模式
    • 支持撤销功能
      • 使用状态实现撤销
      • 多层次撤销
    • One One One …… more things
      • 宏命令
        • 使用宏命令
      • 队列请求
      • 日志请求
    • 总结

《Head First设计模式》读书笔记
相关代码:Vks-Feng/HeadFirstDesignPatternNotes: Head First设计模式读书笔记及相关代码

将封装带到一个全新的境界:把方法调用(method invoke)封装起来

本节用例

设计一个家电自动化遥控器的API。

  • 遥控器有7个可编程的插槽
  • 每个插槽都有对应的开关按钮
  • 具备一个整体的撤销按钮

希望创建一组控制遥控器的API,让每个插槽都能控制一个或一组装置。且需要能够控制目前的装置和任何未来可能出现的装置

家电设计:种类众多,接口不一,以后还会有更多厂商,每个类还会有其他各种方法
家电类设计

所以分离的关注点:遥控器应该知道如何解读按钮被按下的动作,然后发出正确的请求,但是遥控器不需要知道这些家电自动化的细节

命令模式将“动作的请求者”从“动作的执行者”对象中解耦

  • 此例中:遥控器是请求者、厂商类是执行者

采用“命令对象”,把请求封装成一个特定对象,请求发出时就可以让命令对象做相关工作

  • 此例中:每个按钮都存储一个命令对象,当按钮按下时,命令对象做相关的工作。遥控器不需要知道工作内容是什么,只要命令对象能和正确的对象沟通并完成任务即可。实现了遥控器和具体家电的解耦

餐厅类比

点餐流程

  1. 顾客将订单交给招待员createOrder()
  2. 招待员拿走订单takeOrder(),将订单传递给订单柜台并发出通知orderUp()
  3. 厨师根据订单备餐cook()

角色与职责

订单:封装了准备餐点的请求

  • 订单可以被传递
  • 订单只包含一个方法orderUp(),封装了备餐动作
  • 订单内有一个到“需要进行准备工作的对象”的引用(即厨师)
    招待:接受订单,调用订单的orderUp()方法
  • 招待接收不同用户的不同订单,其takeOrder()被传入不同参数
  • 招待知道订单包含orderUp()方法,在需要备餐时调用即可
  • 招待无需知道订单内容、谁来备餐,只需知道并调用orderUp()
    厨师:具备准备餐点的只是
  • 真正知道如何备餐
  • orderUp()被调用时,厨师接手,实现需要创建餐点的所有方法
  • 厨师与招待彻底解耦

从餐厅到命令模式

  1. 客户创建一个命令对象
  2. 客户利用setCommand()将命令对象存储在调用者中
  3. 客户要求调用者执行命令
    <a class=命令模式流程" />
  • 客户(Client)负责创建命令对象createCommandObject()
  • 命令对象包含了接收者上的一组动作,它提供了一个execute()方法,封装了这些动作。
    • 动作和接收者在命令对象中被绑在一起receiver.action()
    • 调用execute()会调用接收者的这些动作
  • 客户在调用者对象(Invoker)上调用setCommand()方法,并把它传入命令对象。命令对象被存储其中并在之后被使用
  • 某个时刻,调用者将调用命令对象

命令模式

第一个命令对象

实现命令接口
public interface Command {public void execute();
}
实现一个命令

以打开电灯命令为例

public class Light {  public void on() {  System.out.println("Light is On");  }  
}
public class LightOnCommand implements Command{  Light light;  public LightOnCommand(Light light) {  this.light = light;  }  public void execute() {  light.on();  }  
}

使用命令对象

public class SimpleRemoteControl {  Command slot;  public SimpleRemoteControl() { }  public void setCommand(Command command) {  slot = command;  }  public void buttonWasPressed() {  slot.execute();  }  
}

NoCommand与空对象

NoCommand对象是一个空对象(null object)的例子。

  • 当你不想返回一个有意义的对象时,空对象就很有用
  • 客户可以将处理null的责任转移给空对象
  • 本例中:遥控器不可能出场时就设置了有意义的命令对象,所以提供了NoCommand对象作为代用品,当调用execute方法时,这种对象什么事情都不做

许多设计模式中都会看到空对象,甚至有时空对象也被视为一种设计模式

定义命令模式

#HeadFirst设计模式7-命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

一个命令对象通过在特定接收者上绑定一组动作来封装一个请求

  • 命令对象将动作和接收者包进对象中。这个对象只暴露出一个execute()方法,当此方法被调用时,接收者会进行这些动作,
  • 从外面看,其他对象不知道接收者做了哪些具体动作,只知道调用execute()就能达成目标
    <a class=命令模式类图" />

Q:接收者一定有必要存在吗?为何命令对象不直接实现execute()方法的细节?
A:一般来说,我们尽量设计“傻瓜”命令对象。它只懂得调用一个接收者的一个行为。然而,有许多“聪明”命令对象会实现许多逻辑,直接完成一个请求。但是“聪明”命令对象的调用者和接收者之间的解耦程度不如“傻瓜”命令对象,而且你不能把接收者当成参数传给命名

支持撤销功能

  1. 当命令支持撤销时,该命令就必须提供和execute()方法相反的undo()方法。
    • 不管execute()刚才做什么,undo()都会倒转过来
public interface Command {public void execute();public void undo();
}
  1. LightOnCommand为例,如果LightOnCommandexecute()方法被调用,那么最后被调用的是on()方法,所以undo()需要执行的即为相反的off()
public class LightOnCommand implements Command {  private Light light;  public LightOnCommand(Light light) {  this.light = light;  }  public void execute() {  light.on();  }  public void undo() {  light.off();  }  
}
  1. 遥控器类中加入一个新的实例变量用来追踪最后被调用的命令。这样当撤销按钮被按下后,我们都可以取出这个命令并调用它的undo()方法
package remotecontrol;  import command.Command;  
import command.NoCommand;  public class RemoteControlWithUndo {  Command[] onCommands;  Command[] offCommands;  Command undoCommand;  public RemoteControlWithUndo() {  onCommands = new Command[7];  offCommands = new Command[7];  Command noCommand = new NoCommand();  for (int i = 0; i < 7; i++) {  onCommands[i] = noCommand;  offCommands[i] = noCommand;  }  undoCommand = noCommand;  }  public void setCommand(int slot, Command onCommand, Command offCommand) {  onCommands[slot] = onCommand;  offCommands[slot] = offCommand;  }  public void onButtonWasPushed(int slot) {  onCommands[slot].execute();  undoCommand = onCommands[slot];  }  public void offButtonWasPushed(int slot) {  offCommands[slot].execute();  undoCommand = offCommands[slot];  }  public void undoButtonWasPushed() {  undoCommand.undo();  }  public String toString() {  StringBuilder stringBuilder = new StringBuilder();  stringBuilder.append("\n------ Remote Control ------\n");  for (int i = 0; i < onCommands.length; i++) {  stringBuilder.append("[slot " + i + "]" + onCommands[i].getClass().getName()  + "    " + offCommands[i].getClass().getName() + "\n");  }  return stringBuilder.toString();  }  
}

使用状态实现撤销

例如,风扇具有多种转速,当撤销时,我们就需要考虑如何恢复它的上一个转速。

方法:加入prevSpeed变量对历史状态进行记录

多层次撤销

使用一个堆栈记录操作过程的每一个命令
当撤销时,从堆栈中取出最上层的命令,然后调用其undo()方法

One One One …… more things

宏命令

按下一个按钮,同时完成多个任务

制造一个新的命令,用来执行其他一堆命令

public class MacroCommand implements Command{Command[] commands;  public MacroCommand(Command[] commands) {  this.commands = commands;  }  public void execute() {  for (int i = 0; i < commands.length; i++) {  commands[i].execute();;  }  }  
}

Q:为什么不创建一个PartyCommand(),在其中调用其他的命令
A:这相当于把Party模式“硬编码”到PartyCommand中。而利用宏命令,你可以动态地决定PartyCommand是由哪些命令组成,所以宏命令在使用上更灵活。一般来说,宏命令的做法更优雅,也需要较少的新代码。

使用宏命令
  1. 创建想要进入宏的命令集合
  2. 创建两个数组,其中一个用来记录开启命令,另一个用来记录关闭命令,并在数组内放入对应命令
  3. 将宏命令指定给我们希望的按钮

队列请求

命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去,就像是一般的对象一样。
现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,他甚至可以在不同的线程中被调用。
利用这样的特性可以衍生出一些应用:日程安排(Scheduler)、线程池、工作队列等

想象有一个工作队列:一端添加命令,另一端则是线程

  • 线程进行下面的动作,从队列中取出一个命令,调用它的execute()方法,调用完成后将命令对象丢弃,再取出下一个命令
    命令队列

注意:工作队列类和进行计算的对象之间完全是解耦的。此刻线程可能在进行财务运算,下一刻却在读取网络数据。工作队列对象不在乎到底做些生命,它们只知道取出命令对象,然后调用execute()方法。
类似的,只要是实现命令模式的对象,就可以放入队列里,当线程可用时就调用此对象的execute()方法。

日志请求

某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的装填。

命令模式能够支持这一点:新增两个方法store()load()

  • java中,利用对象的序列化(Serialization)实现这些方法,但是一般认为序列化最好还是只用在对象的持久化上(persistence)

执行命令时,将历史记录存储在磁盘中,一旦系统司机,就可以将命令对象重新加载,并成批次地依次调用这些对象的execute()方法

许多大型数据结构的动作的应用无法在每次改变发生时被快速地存储。通过使用记录日志,我们可以将上次检查点(checkpoint)之后的所有操作记录下来,如果系统出状况,从检查点开始应用这些操作

对更高级的应用而言,这些技巧可以被扩展应用到事务(transaction)处理中。

总结

OO基础

  • 抽象
  • 封装
  • 多态
  • 继承

OO原则

  • 封装变化
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程
  • 为交互对象之间的松耦合设计而努力
  • 对扩展开放,对修改关闭
  • 依赖抽象,不要依赖具体类

OO模式

  • 命令模式——将请求封装成对象,这可以让你使用不同的请求、队列或者日志请求来参数化其他对象。命令模式也可以支持撤销操作。

要点

  • 命令模式将发出请求的对象和执行请求的对象解耦
  • 被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作
  • 调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用
  • 调用者可以接受命令当做参数,甚至在运行时动态地进行
  • 命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态
  • 宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销
  • 实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求,而不是将工作委托给接收者
  • 命令也可以用来实现日志和事务系统

http://www.ppmy.cn/server/170885.html

相关文章

网络安全防御:蓝队重保备战与应急溯源深度解析

课程目标 本课程旨在培养专业的网络安全蓝队成员&#xff0c;通过系统化的学习和实战演练&#xff0c;使学员能够掌握网络安全防御的核心技能&#xff0c;包括资产测绘、应急响应、系统安全应急溯源分析、网络层溯源分析以及综合攻防演练等。学员将能够熟练运用各种工具和技术…

20250224-代码笔记02-class CVRPTrainer

文章目录 前言一、__init__(self, env_params, model_params, optimizer_params, trainer_params)函数功能函数代码 二、run(self)函数功能函数代码 三、_train_one_epoch(self, epoch)函数功能函数代码问题批次与回合 四、_train_one_batch(self, batch_size)函数功能函数代码…

nginx 部署前端vue项目

?? 主页&#xff1a; ?? 感谢各位大佬 点赞?? 收藏 留言?? 加关注! ?? 收录于专栏&#xff1a;前端工程师 文章目录 一、??什么是nginx&#xff1f;二、??nginx 部署前端vue项目步骤 2.1 ??安装nginx 2.1.1 ??windows环境安装2.1.2 ??linux环境安装 2.2 …

基于YOLO11深度学习的苹果叶片病害检测识别系统【python源码+Pyqt5界面+数据集+训练代码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…

Linux设备驱动开发-UART驱动

UART 有三条线&#xff0c;分别是 Rx&#xff0c;Tx 和 GND 数据发送接收步骤&#xff1a; 1.双方约定波特率 2.拉低&#xff08;从高电平&#xff09; Tx 引脚维持 1bit 时间 3.接收端在低电平开始处计时 4.发送端根据数据驱动 Tx 引脚电平 5.接收端 1.5bit 时间后读取引…

Linux环境基础开发工具的使用(apt、vim、gcc、g++、gdb、make/Makefile)

Linux环境基础开发工具的使用&#xff08;apt、vim、gcc、g、gdb、make/Makefile&#xff09; 文章目录 Linux软件包管理器 - apt Linux下安装软件的方式认识apt查找软件包安装软件如何实现本地机器和云服务器之间的文件互传卸载软件 Linux编辑器 - vim vim的基本概念vim下各…

安装 Milvus Java SDK

本主题介绍如何为 Milvus 安装 Milvus Java SDK。 当前版本的 Milvus 支持 Python、Node.js、GO 和 Java SDK。 要求 Java&#xff08;8 或更高版本&#xff09;Apache Maven 或 Gradle/Grails 安装 Milvus Java SDK 运行以下命令安装 Milvus Java SDK。 Apache Maven &…

Node.js 中 fs 模块的高级用法

目录 1. 流式文件处理 示例&#xff1a;大文件复制 2. 文件监控 示例&#xff1a;使用 fs.watch 监控文件变化 3. 异步递归操作 示例&#xff1a;异步递归遍历目录 4. 文件权限管理 示例&#xff1a;修改文件权限 5. 原子操作 示例&#xff1a;原子重命名文件 在 Nod…