命令模式的概述
命令模式的定义是:把请求封装成为对象,以便用于不同的请求,队列或者日志请求来参数化其他对象,并支持可撤销操作。
那么命令模式主要用来解决哪些问题?这也是为什么需要命令模式,主要是以下四种
1. 请求者与接收者之间的强耦合
- 问题:如果调用者直接调用接收者的方法,两者就绑死了。如果接收者变了,调用者也要改。
- 解决:引入一个“命令对象”作为中间人。调用者只需要知道“执行命令”,根本不需要知道背后是谁在干活、怎么干。
2. 操作的“撤销(Undo)”与“重做(Redo)” - 问题:普通的函数调用执行完就结束了,想反悔很难。
- 解决:命令对象可以保存执行前的状态(或反向操作逻辑)。在命令接口中定义
execute()和undo()方法,就能轻松实现撤销和重做(比如文本编辑器的 Ctrl+Z)。
3. 操作的排队、延迟执行与日志记录 - 问题:有些操作需要排队等待,或者在系统崩溃后需要恢复。
- 解决:因为请求被封装成了对象,所以它可以像普通数据一样被放进队列、写入文件(日志),甚至通过网络传输。系统重启后,读取日志重新执行命令即可恢复状态。
4. 动态组合与扩展操作(支持宏命令) - 问题:想要把多个操作组合成一个“一键执行”的功能很麻烦。
- 解决:可以设计“宏命令(Macro Command)”,里面包含多个子命令。执行宏命令就等于依次执行所有子命令,新增功能时只需增加新命令类,符合开闭原则。
下面我们一步步来在理论建立和真实的情景中拆解理解。
命令模式理论建立
这里先介绍命令模式在一些游戏功能的方面的应用,拿比较常见的沙盒建造系统开始,现在让你设计编写一个沙盒系统该怎么做呢?
策划案
1. 需求概述
在沙盒建造玩法中,玩家可以在网格地图上放置或拆除建筑方块。为了提升容错率与操作体验,需要提供“撤销(Undo)”与“重做(Redo)”功能,支持玩家回退或恢复最近的建造操作。
2. 核心功能点(需求分析)
- 建造与拆除:玩家通过鼠标左键放置方块,右键拆除方块。
- 操作撤销:玩家按下
Z键,可回退上一步操作(放置变拆除,拆除变放置)。 - 操作重做:玩家按下
Y键,可恢复刚刚被撤销的操作1。 - 状态联动:当执行新操作时,必须清空重做栈(因为历史线已经改变)
UML类图解析
为什么使用命令模式?当你得到需求时可能想出的最简单的方案是把代码都写到一个文件里,比如直接使用if判断输入if内使用API执行建筑,这满足了最基本的需求,但是当你想加入撤销操作时会发现无从下手,从最基本的代码开始我们可以重新思考,可以发现命令模式解决的四种问题中就包含了我们遇到的问题。
PS使用命令模式的优点:第一方便实现撤销和重做的操作,第二方便后续扩展,如果我想加入别的命令只需要继承Command接口并在需要的地方创建出所需的命令即可。
现在开始根据需求构筑我们的UML类图。
PS这里使用的单一职责原则,开闭原则,依赖倒置原则,体现的适合会提到,后续会更新相关的文章。
我们回头看一下对需求的分析,想一下我们可以怎么做?面对具体的功能我们可以将它们合理的分离开来(具体实现的脚本为底层组件),然后我们可以再用一个脚本来面向客户(创建客户端脚本)让它管理底层组件(高层组件)。(单一职责原则)
让我们来看命令模式是怎么分离功能和组织代码的。
我们需要客户端(BuildController)接受玩家的信号,然后让客户端去发号命令(客户端内部new出新命令传递给对象),并让对应的脚本执行具体逻辑。
命令接口规范命令必须包含的函数,并根据里氏替换原则,为记录命令提供统一接口。
命令脚本(BulidCommand)的作用是作为一个中间人(把数据传给Command,然后再把数据传给Builder,这里Builder就是接受者),负责封装请求(封装请求:把谁来做和用什么做一起打包),当客户端发出建筑请求时,不会直接调用Builder。(注意这里只发送和接受并不执行请求)。
这里再讲一下接受者:接受者同时被客户端和命令组合,不同的是在客端中接受者只是作为一个数据(让Command知道接受者是谁,也叫绑定(关联)接收者),在命令中接受者是已经被关联好的接受者,命令就负责去调用接受者的具体方法。
命令管理脚本这时候对传入的命令进行管理和记录,执行和撤销命令,被成为调用者。
我们这里再来梳理一下发生了什么。
客户端:负责接受信息,根据信息发送对应的请求。
接受者:最终接受到命令的组件,执行具体逻辑。
执行者:对传入的信息管理和记录(管理就包括了执行和撤销)
其层级关系应该为
客户端 -> 调用者 -> 命令 -> 接受者。
PS:层级关系体现出依赖倒置原则,想要理解倒置,就要知道什么是正的,传统的思维里是高层组件依赖(组合)底层组件(高层组件->底层组件),倒置中多出了一个抽象类,使高层组件和底层组件都依赖这个抽象类(高层组件->抽象类<-底层组件),这里倒置就是说的底层组件依赖关系的倒置。
这里高层组件Contorller通过Manager依赖接口Command,而底层组件BuildCommand这是直接依赖(继承)Command

这里通过上面的示例我们就可以提炼出命令模式的UML类图

命令模式示例代码
定义命令接口 (ICommand)
1 | public interface ICommand { |
接收者:建造者系统 (Builder)
1 | using UnityEngine; |
具体命令:建造命令 (BuildCommand)
1 | using UnityEngine; |
调用者:命令管理器 (CommandManager)
1 | using UnityEngine; |
客户端:输入控制器 (BuildController)
1 | using UnityEngine; |
参考引用
命令模式 · Design Patterns Revisited · 游戏设计模式
HeadFirst 设计模式
命令模式 | 菜鸟教程
说些什么吧!