设计模式——2_1 命令(Command)
文章目录
- 定义
- 图纸
- 一个例子:空调和他的遥控器
- 只有控制面板的空调
- 遥控器
- 可以撤销的操作
- 碎碎念
- 命令和Runnable
- 命令和事务
定义
把请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作
在职责链中,我们把不同的动作分支组合在一起,让请求在不同的分支中进行流通,他可以是逻辑上的流通,也可以是封装成一个参数对象在里面流通。而在命令模式中,这种思想进一步升级,他会不分青红皂白的把所有请求封装成命令对象
图纸

各位道友应该知道有个叫枪的东西吧,一种装上子弹就可以开火的设备
和由枪和子弹构成的模式一样,命令模式是由 实际执行操作的【Receiver】 和 命令对象【ConcreteCommand】 构成,如果把 Receiver 看成 枪,那么 ConcreteCommand 就是 子弹
每次要开枪(执行指令)之前,你都要获取子弹(获取对应的 ConcreteCommand 对象),上膛(组合 Receiver 和 ConcreteCommand 对象),最后扣动扳机(调用 Receiver 的 Action 方法)
一个例子:空调和他的遥控器
假定你入职了一家空调公司,接手了和立式空调有关的模块。不久之后,公司决定给这个立式空调增加一个遥控器(原有的立式空调全部都是通过控制面板上的按钮进行操作的)。正当你摩拳擦掌准备大干一番的时候,前辈留给你的紧耦合代码却让你差点当场骂娘。困难面前,你是要说服上司放弃遥控器的企划,还是连夜删库跑路?又或者重构已有的代码……
铺垫有点太长了,总之这次的例子开始了:
只有控制面板的空调
言归正传,前辈留下的代码是这样的:

AirConditioner(空调)
/*** 空调*/
public class AirConditioner {public static final int MAX_TEMPERATURE = 32;//最高温度public static final int MIN_TEMPERATURE = 18;//最低温度private static final String[] MODE_ARRAY = {"制冷模式", "睡眠模式", "送风模式"};/*** 温度*/private Float temperature;/*** 当前模式*/private Integer modePoint;/*** 是否是开启的*/private boolean isOn;/*** 控制面板*/private ControlPanel controlPanel;public AirConditioner() {controlPanel = ControlPanel.createControlPanel(this);}public static String[] getModeArray() {return MODE_ARRAY.clone();}public Float getTemperature() {return temperature;}public void setTemperature(Float temperature) {if (temperature == null || (temperature >= MIN_TEMPERATURE && temperature <= MAX_TEMPERATURE)) {this.temperature = temperature;}}public String getMode() {return MODE_ARRAY[modePoint];}public Integer getModePoint() {return modePoint;}public void setModePoint(Integer modePoint) {this.modePoint = modePoint;}public boolean isOn() {return isOn;}public void setOn(boolean on) {isOn = on;}
}
ControlPanel
/*** 控制面板*/
public class ControlPanel {private final AirConditioner airConditioner;private ControlPanel(AirConditioner airConditioner) {this.airConditioner = airConditioner;}public static ControlPanel createControlPanel(AirConditioner airConditioner) {ControlPanel controlPanel = new ControlPanel(airConditioner);controlPanel.off();return controlPanel;}/*** 开机*/public void on() {if (!airConditioner.isOn()) {//关机模式才可以执行这个动作airConditioner.setOn(true);//设定开机airConditioner.setTemperature(26f);//默认26度airConditioner.setModePoint(0);//默认第一个模式}}/*** 关机*/public void off() {if (airConditioner.isOn()) {//开机状态才可以执行这个动作airConditioner.setOn(false);airConditioner.setTemperature(null);airConditioner.setModePoint(null);}}/*** 温度上升1*/public void addTemperature() {if (airConditioner.isOn()) {airConditioner.setTemperature(airConditioner.getTemperature() + 1);}}/*** 温度下降1*/public void lessenTemperature() {if (airConditioner.isOn()) {airConditioner.setTemperature(airConditioner.getTemperature() - 1);}}/*** 下一模式*/public void nextMode() {Integer modePoint = airConditioner.getModePoint();if (airConditioner.isOn()) {if (modePoint + 1 >= AirConditioner.getModeArray().length) {airConditioner.setModePoint(modePoint + 1);} else {airConditioner.setModePoint(0);}}}
}
在这个只有控制面板的立式空调里面,前辈通过 AirConditioner(空调) 来表示一部空调的底层函数,然后通过 ControlPanel(控制面板) 为 Client 提供操作空调内部属性的接口
虽然它可以如预期一般完成任务,但这种设计绝算不上优雅,他的问题主要体现在 控制面板 和空调底层方法之间的耦合过于紧密,现在只有一种类型的空调,如果有底层方法不相同的第二种类型的空调出现,那么这个 控制面板 是无法与其兼容的
遥控器
遥控器 的引入改变了现态,很显然,遥控器 至少需要拥有和 控制面板 一样的效果,而且一个 遥控器 必须可以同时对应多个 空调 对象。也就是说,你只有在最终调用遥控器上面的任务的时候才会知道到底要调用哪个空调上的方法,做出来的效果应该是这样的:

我们新增了 RemoteController(遥控器) 作为遥控器的实现,而 RemoteController 和 ControlPanel 的实现唯一的区别就在于 ControlPanel 的 AirConditioner 从他诞生的时候就被定义且无法修改,而 RemoteController 的 AirConditioner 在调用命令的时候才会被指定
除此之外,程序中出现了大量的重复代码,这些重复代码分布在 控制面板 和 遥控器 的每一个对应方法中
有没有办法把他们解耦?我的意思是把调用者和他的实现解耦,也就是说做成这样的效果:

放在本例中,他长这样:

Executor & Pool
/*** 空调命令执行器*/
public abstract class ACExecutor implements Cloneable {/*** 执行命令*/public abstract void execute(AirConditioner airConditioner);/*** 克隆方法,覆盖Object中的clone*/public ACExecutor clone() {try {return (ACExecutor) super.clone();} catch (CloneNotSupportedException e) {e.printStackTrace();throw new RuntimeException(e);//异常原样抛出}}
}public class OnExecutor extends ACExecutor {@Overridepublic void execute(AirConditioner airConditioner) {if (!airConditioner.isOn()) {//关机模式才可以执行这个动作airConditioner.setOn(true);//设定开机airConditioner.setTemperature(26f);//默认26度airConditioner.setModePoint(0);//默认第一个模式}}
}public class OffExecutor extends ACExecutor {@Overridepublic void execute(AirConditioner airConditioner) {if (airConditioner.isOn()) {//开机状态才可以执行这个动作airConditioner.setOn(false);airConditioner.setTemperature(null);airConditioner.setModePoint(null);}}
}public class AddTemperatureExecutor extends ACExecutor {@Overridepublic void execute(AirConditioner airConditioner) {if (airConditioner.isOn()) {airConditioner.setTemperature(airConditioner.getTemperature() + 1);}}
}public class LessenTemperatureExecutor extends ACExecutor {@Overridepublic void execute(AirConditioner airConditioner) {if (airConditioner.isOn()) {airConditioner.setTemperature(airConditioner.getTemperature() - 1);}}
}public class NextModeExecutor extends ACExecutor {@Overridepublic void execute(AirConditioner airConditioner) {Integer modePoint = airConditioner.getModePoint();if (airConditioner.isOn()) {if (modePoint + 1 >= AirConditioner.getModeArray().length) {airConditioner.setModePoint(modePoint + 1);} else {airConditioner.setModePoint(0);}}}
}/*** 空调命令执行器的对象池*/
public class ACExecutorPool {//单例相关private static final ACExecutorPool INSTANCE = new ACExecutorPool();public static ACExecutorPool getInstance() {return INSTANCE;}//原型池private final Map<Class<? extends ACExecutor>, ACExecutor> prototypePool = new HashMap<>();public ACExecutor createOnExecutor() {return getNewObjectByPool(OnExecutor.class);}public ACExecutor createOffExecutor() {return getNewObjectByPool(OffExecutor.class);}public ACExecutor createAddTemperatureExecutor() {return getNewObjectByPool(AddTemperatureExecutor.class);}public ACExecutor createLessenTemperatureExecutor() {return getNewObjectByPool(LessenTemperatureExecutor.class);}public ACExecutor createNextModelExecutor() {return getNewObjectByPool(NextModeExecutor.class);}/*** 从原型池中获取对应的新命令对象*/private ACExecutor getNewObjectByPool(Class<? extends ACExecutor> c) {if (prototypePool.containsKey(c)) {return prototypePool.get(c).clone();} else {try {ACExecutor prototype = c.newInstance();prototypePool.put(c, prototype);return prototype.clone();} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();throw new RuntimeException("初始化命令对象原型池的时候出现了异常,具体异常为:" + e.getCause(), e);}}}
}
ControlPanel & RemoteController
/*** 控制面板*/
public class ControlPanel {private final AirConditioner airConditioner;private ControlPanel(AirConditioner airConditioner) {this.airConditioner = airConditioner;}public static ControlPanel createControlPanel(AirConditioner airConditioner) {ControlPanel controlPanel = new ControlPanel(airConditioner);controlPanel.off();return controlPanel;}/*** 开机*/public void on() {ACExecutorPool.getInstance().createOnExecutor().execute(airConditioner);}/*** 关机*/public void off() {ACExecutorPool.getInstance().createOffExecutor().execute(airConditioner);}/*** 温度上升1*/public void addTemperature() {ACExecutorPool.getInstance().createAddTemperatureExecutor().execute(airConditioner);}/*** 温度下降1*/public void lessenTemperature() {ACExecutorPool.getInstance().createLessenTemperatureExecutor().execute(airConditioner);}/*** 下一模式*/public void nextMode() {ACExecutorPool.getInstance().createNextModelExecutor().execute(airConditioner);}
}/*** 遥控器*/
public class RemoteController {/*** 开机*/public void on(AirConditioner airConditioner) {ACExecutorPool.getInstance().createOnExecutor().execute(airConditioner);}/*** 关机*/public void off(AirConditioner airConditioner) {ACExecutorPool.getInstance().createOffExecutor().execute(airConditioner);}/*** 温度上升1*/public void addTemperature(AirConditioner airConditioner) {ACExecutorPool.getInstance().createAddTemperatureExecutor().execute(airConditioner);}/*** 温度下降1*/public void lessenTemperature(AirConditioner airConditioner) {ACExecutorPool.getInstance().createLessenTemperatureExecutor().execute(airConditioner);}/*** 下一模式*/public void nextMode(AirConditioner airConditioner) {ACExecutorPool.getInstance().createNextModelExecutor().execute(airConditioner);}
}
在这种实现方式里,我们把具体执行的操作封装到 ACExecutor(空调执行器) 类簇中,为 所有的操作定义了自己的执行类;然后在 client 点击 控制面板 和 遥控器 上的对应按钮的时候,把让他们获取对应的 执行器对象,并执行对应的操作。而 控制面板 和 遥控器 根本不关心 执行器 的底层做了什么操作,这让他们从命令的执行者变成了命令的调度者
听起来很复杂,但是这种复杂是 颗粒度 细致程度导致的(五个命令导致我们需要五个对应的执行者子类),流程上其实是很简单的。以开机为例,我们的程序其实是这样做的:

这种让程序变得复杂的写法是有价值的,至少可以让你在以下两种情况可以少写很多代码:
- 当出现新的操作面板,比如手机APP控制空调的时候,我就可以通过创建新的控制器类并让他调用已有的执行者来实现,而不需要再重复执行者里面的代码
- 如果出现了接口一致,但行为不一致的空调,那我依然可以继续使用控制面板和遥控器,只需要建立对应的执行器类簇即可
而这正是一个标准的命令实现
可以撤销的操作
一般我讲到 ”这正是xxx的实现“ 的时候,例子就结束了,但这次是例外
请留意一下上例的一个配角类—— ACExecutorPool,这个类的作用是为我们的程序产出可靠的执行类对象。在上例中,这一个类里,用到了两种模式,分别是 单例 和 原型
单例很好理解,为了让全局都用一个对象池
为什么要用原型呢?每个执行器都用单例不是更节省吗?
这种情况下会用原型只有一种可能,那就是执行器应该是带状态的,而且他的状态是有意义的
那你会说了,不对啊,上例的执行器哪有状态。别急,业务来了
某日,接到通知,我们需要在 控制面板 上添加一个 返回(back) 按钮,用于撤销我们刚刚执行的命令,要怎么实现呢?
如果你没有使用命令模式实现这种功能费死劲,但是在命令模式的框架下,你可以这样做:

我们在 ACExecutor 中提供了用于回滚的方法 back,而在 控制面板 中我们通过添加 history 列表的方式存储已经执行过的执行器,以便我们回滚
这时候 ACExecutor 的对象就绝不能用单例了,因为他的属性是一种凭证,用于证明这个执行器对象有没有执行成功。此时原型模式就是你比较合适的选择了,因为执行器对象是会被大量创建的,原型可以有效的降低创建执行器的开销(复制初始属性)
而这也是命令模式所能实现的功能之一
碎碎念
命令和Runnable
Java的多线程模块中用到了很标准的命令模式
我们通过 Thread 来管控线程,但是线程具体如何执行是由 Runnable 来决定的
也就是说 Thread 本质上其实就是 Receiver,Runnable 才是掌握具体内容的执行器
命令和事务
命令模式中的执行器的颗粒度你是可以自己掌握的,你可以只让他执行单体命令,也可以让他执行多个指令捆绑在一起的复合指令
这就是SQL中的事务一样,他是一个具有 原子性 的整体,一荣俱荣,一损俱损
万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容
相关文章:
设计模式——2_1 命令(Command)
文章目录 定义图纸一个例子:空调和他的遥控器只有控制面板的空调遥控器可以撤销的操作 碎碎念命令和Runnable命令和事务 定义 把请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持…...
HP数组面试题
PHP数组面试题 问题: 如何创建一个空数组和一个带有初始值的数组? 答案: 创建空数组:可以使用array()函数或空数组语法[]来创建一个空数组,例如$arr array();或$arr [];。创建带有初始值的数组:可以在创建…...
机器学习5-线性回归之损失函数
在线性回归中,我们通常使用最小二乘法(Ordinary Least Squares, OLS)来求解损失函数。线性回归的目标是找到一条直线,使得预测值与实际值的平方差最小化。 假设有数据集 其中 是输入特征, 是对应的输出。 线性回归的…...
vulhub中Adminer ElasticSearch 和 ClickHouse 错误页面SSRF漏洞复现(CVE-2021-21311)
Adminer是一个PHP编写的开源数据库管理工具,支持MySQL、MariaDB、PostgreSQL、SQLite、MS SQL、Oracle、Elasticsearch、MongoDB等数据库。 在其4.0.0到4.7.9版本之间,连接 ElasticSearch 和 ClickHouse 数据库时存在一处服务端请求伪造漏洞(…...
浅谈Zookeeper及windows下详细安装步骤
1. Zookeeper介绍 1.1 分布式系统面临的问题 分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。 面临的问题:系统每个节点之间信息同步及共享 以一个小团队为例,面临的问题 通过网络进行信息…...
vite, vue3, vue-router, vuex, ES6学习日记
学习使用vitevue3的所遇问题总结(2024年2月1日) 组件中使用<script>标签忘记加 setup 这会导致Navbar 没有暴露出来,导致使用不了,出现以下报错 这是因为,如果不用setup,就得使用 export default…...
25考研|660/880/1000/1800全年带刷计划
作为一个参加过两次研究生考试的老学姐,我觉得考研数学的难度完全取决于你自己 我自己就是一个很好的例子 21年数学题目是公认的简单,那一年考130的很多,但是我那一年只考了87分。但是22年又都说是有史以来最难的一年,和20年的难度…...
Mybatis基础教程及使用细节
本篇主要对Mybatis基础使用进行总结,包括Mybatis的基础操作,使用注解进行增删改查的练习;详细介绍xml映射文件配置过程并且使用xml映射文件进行动态sql语句进行条件查询;为了简化java开发提高效率,介绍一下依赖&#x…...
10 分钟在K8s 中部署轻量级日志系统 Loki
转载至我的博客 https://www.infrastack.cn ,公众号:架构成长指南 Loki 是什么? Loki是由Grafana Labs开源的一个水平可扩展、高可用性,多租户的日志聚合系统的日志聚合系统。它的设计初衷是为了解决在大规模分布式系统中&#x…...
图像处理python基础
array 读取图片 tensor 模型预测 一般过程:读取数据np->tensor->model->result->np->画图 shape确保图像输入输出尺寸正确 读取图片 将在GPU上运行的tensor类型转变成在CPU上运行的np类型 三类计算机视觉任务的输入: 分类࿱…...
基于WordPress开发微信小程序2:决定开发一个wordpress主题
上一篇:基于WordPress开发微信小程序1:搭建Wordpress-CSDN博客 很快发现一个问题,如果使用别人的主题模板,多多少少存在麻烦,所以一咬牙,决定自己开发一个主题模板,并且开源在gitee上ÿ…...
[Python] 什么是网格搜索以及scikit-learn中GridSearch类的介绍和使用案例?
什么是网格搜索? 网格搜索是一种参数调优的方法,它可以帮助找到最佳的模型参数。在网格搜索中,我们先指定参数的候选值范围,然后枚举所有可能的参数组合,计算每个模型的性能指标(比如准确率、精确率等&…...
Linux-正则表达式
1.正则表达式的定义: 正则表达式通常用于判断语句中,使用字符串描述、匹配一系列符合某个规则的字符串。 正则表达式是由普通字符与元字符组成。 普通字符包括小写字母、数字、标点符号及一些其他符号。元字符是指在正则表达式中具有特殊意义的专用字符&…...
Java基础学习:System类和Static方法的实际使用
一、System类 1.在程序开发中,我们需要对这个运行的结果进行检验跟我们预判的结果是否一致,就会用到打印结果在控制台中显示出来使用到了System类。System类定义了一些和系统相关的属性和方法,它的属性和方法都是属于静态的,想使用…...
线性代数------矩阵的运算和逆矩阵
矩阵VS行列式 矩阵是一个数表,而行列式是一个具体的数; 矩阵是使用大写字母表示,行列式是使用类似绝对值的两个竖杠; 矩阵的行数可以不等于列数,但是行列式的行数等于列数; 1.矩阵的数乘就是矩阵的每个…...
Flutter 开发3:创建第一个Flutter应用
Step 1: 安装Flutter 1.1 下载Flutter SDK 首先,你需要访问Flutter官方网站下载最新的Flutter SDK。选择适合你操作系统的安装包。 $ cd ~/development $ unzip ~/Downloads/flutter_macos_2.2.3-stable.zip1.2 更新环境变量 接下来,你需要将Flutter…...
Linux中断下半部分:软中断,tasklet和工作队列
为什么要有下半部分 中断会打断其他程序,为了打断其他程序时间短,就需要中断处理程序快。执行中断处理程序后,相同中断不会触发,甚至所有中断都不能触发(设置IRQF_DISABLED,其他硬件与操作系统无法通信)中…...
Flink CEP实现10秒内连续登录失败用户分析
1、什么是CEP? Flink CEP即 Flink Complex Event Processing,是基于DataStream流式数据提供的一套复杂事件处理编程模型。你可以把他理解为基于无界流的一套正则匹配模型,即对于无界流中的各种数据(称为事件),提供一种组合匹配的…...
QSqlRelationalTableModel 关系表格模型
一、 1.1 QSqlRelationalTableModel继承自QSqlTableModel,并且对其进行了扩展,提供了对外键的支持。一个外键就是一个表中的一个字段 和 其他表中的主键字段之间的一对一的映射。例如,“studInfo”表中的departID字段对应的是“departments…...
JS和CSS实现的原生轮播图
JSCSS实现滑动轮播图 使用JS加CSS来实现的幻灯片,主要使用的是CSS的transform属性中的translate来实现,适合与用户交互的轮播图,展现轮播图的数量,用户可自由进行选择。 <!DOCTYPE html> <html lang"en">&…...
Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
React 第五十五节 Router 中 useAsyncError的使用详解
前言 useAsyncError 是 React Router v6.4 引入的一个钩子,用于处理异步操作(如数据加载)中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误:捕获在 loader 或 action 中发生的异步错误替…...
从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
如何为服务器生成TLS证书
TLS(Transport Layer Security)证书是确保网络通信安全的重要手段,它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书,可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
C++.OpenGL (14/64)多光源(Multiple Lights)
多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
Leetcode33( 搜索旋转排序数组)
题目表述 整数数组 nums 按升序排列,数组中的值 互不相同 。 在传递给函数之前,nums 在预先未知的某个下标 k(0 < k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k1], …, nums[n-1], nums[0], nu…...
Java后端检查空条件查询
通过抛出运行异常:throw new RuntimeException("请输入查询条件!");BranchWarehouseServiceImpl.java // 查询试剂交易(入库/出库)记录Overridepublic List<BranchWarehouseTransactions> queryForReagent(Branch…...
