设计模式中的每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。
一个设计模式,它的服务对象是高层模块,在设计模式中称为客户端,因此在描述设计模式的时候都是以客户端作为使用方来进行描述的。设计模式在类间关系这个粒度上给出常见问题的解决方案。属于软件工程中逻辑架构设计中相当重要的一环。快速查阅各类设计模式使用场景可参考此文:。
一、命令模式
定义: 把一个请求看做一个对象。这样用户就可以参数化客户端请求,实现请求的队列操作和日志管理,并且支持对操作进行撤销回退。(Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations.)
概述: 在软件开发过程中,我们可能会遇到这样的问题,在程序运行前,不知道命令(本文中请求和命令都指代同一个东西,即客户端发起的一个需要处理的操作)的执行者是谁,也不知道需要执行哪个命令。这时,我们可以使用命令模式将命令的发起者和命令的执行者解耦。把命令化作一个相对独立的对象,在发送者和执行者之间传输。程序运行时,再确定需要执行的命令和其执行者,并将命令移交给命令管理器处理。如果对命令模式稍微做一点改动,让每个命令提供执行的同时提供一个撤销的方法,就能够实现对命令的无限次撤销和重做,这在工程中具有非常广泛的应用。
1、核心思想
其实定义就已经讲出了命令模式的核心思想:把一个命令看做一个对象。这样做就能让命令拥有所有对象所拥有的优势。模式中的其他参与者都是围绕它来运作的。
2、类图
客户端|Client: 命令的发起者。确定接下来要执行什么命令。
调用者|Invoker: 命令的管理者,不关心每个命令具体是做什么内容,根据客户端的指示按序执行命令。命令接口|ICommand:命令接口协议,确定每个命令需要提供的功能,这里要求每个命令类都提供执行方法。具体命令|ConcreteCommand:包含执行一个命令所需的所有上下文信息,例如执行接收者的哪个方法,以及方法所需要的参数,甚至命令作为GUI 显示时的相关信息,例如应该显示的图标路径。具体命令类是命令模式中的核心节点,需要重点理解。接收者|Receiver:命令所对应任务的实际执行者,位于调用链条的末端。
注意:客户端发起的每一个命令都是一个对象,即使是两条相同的命令,也是两个对象,而不是一个对象使用两次。
1)隐喻
下面通过一个隐喻来直观地展现命令模式的各个组成部分。
我们假设这样一个场景,公元 6011 年,人类在银河系的上千的星球建立了殖民地,其中大唐和盖亚是两个敌对的国家,大唐准备发动一场战争,冷月是一个大型特种部队的指挥官,拥有护卫舰、运输舰和战列舰等单位的使用权,该部队具有侦查、暗杀、破坏等多种职能。冷月所在星球只有一个小型港口提供舰船的发射与停靠,有一个指挥塔负责舰船的调度。
某天,冷月派遣一个破坏小队使用战列舰前往盖亚一个冷僻星球进行破坏占领,派遣一个侦查小队使用护卫舰先行侦查。同时派遣一个侦查小队使用运输舰前往友军三十四军团处待命。冷月将舰船出港许可发送给指挥塔,让指挥塔安排舰船出港。
在上面这个场景中,冷月是命令的发起人,是一个 Client,他根据实际的战斗需要安排舰船和作战人员。每一艘舰船都相当于是一个具体命令对象,舰长知道应该把舰船运送到什么地方(类比命令的执行环境),也知道舰船的乘坐人员(命令的接收者),指挥塔是 Invoker,它不关心出港舰船的类型以及舰船的运载人员,只是接受冷月的舰船的出港请求,然后安排对应舰船出港。当舰船到达目的地之后,舰船运载的作战人员会去执行具体的作战任务。
(笔者构思了好几种场景,发现命令对象化在现实中比较难以找到对应物,一个是因为命令对象具有接收者的引用,具有主动性,如果是人引用人会比较奇怪,相对来说具有主动性的机器容器引用人会比较合适,另一方面则是命令对象在 Client 眼中和 Invoker 眼中有不一样的抽象层次,也比较难以找到现实的对应物。科幻战争的这个场景虽然能够阐述命令模式的几个关键要素,但是场景本身有较多的干扰元素,有点过于复杂了,在实际中指挥塔不可能不关心舰船的类型,舰船作为命令的基本单位也有一点牵强。不过场景重在意会,加深理解,笔者认为该场景还是能够达成这一点,就放上来了,各位看官如果有更好的隐喻,欢迎在文后回复。)
3、应用场景以及优缺点
1)应用场景
适用于各种需要对命令进行调度管理以及传输的场景,也就是各种需要把命令包装成命令对象的场景。
1. 数据库的事务
每个事务作为一个命令对象。
2. 线程池
每个线程作为一个命令对象,客户端负责提供接收者
2)优点
- 解开命令调用和命令执行之间的耦合
- 方便命令的管理,包括命令组合、命令添加、命令记录和命令延时执行等
- 容易扩展实现命令的撤销和重做
3)缺点
每个命令都需要一个具体命令类,如果系统的命令繁多,可能会影响开发效率。
二、游戏应用
1、适用场景
命令模式在工程中的许多应用是非常基础性的,在游戏与非游戏项目中都能使用。下面单以游戏应用为例进行介绍:
1) 玩家输入控制 & 菜单项
在控制玩家输入的时候,使用命令模式可以使得按键和按键对应的命令解绑,从而支持用户自定义,还可以把指令的触发和执行时间解绑,实现延时执行。对于多角色游戏,还能为玩家和AI之间提供一套通用的命令接口
2) 宏记录 & GM 指令
所谓宏,就是将一些命令组织在一起,作为一个单独命令完成一个特定任务。而宏记录是指将用户的一系列命令记录下来,将记录下来的一组命令作为一个宏。通过宏记录能够实现对玩家所有操作的录像。
GM 指令一般来说能够模拟玩家操作来快速获得游戏的一些资源或者推进游戏进度。如果为玩家操作的命令类型实现一个 toScript() 方法,将玩家的一个操作直接转换成一个可执行的 GM 指令,那么就可以直接通过宏记录执行 GM 指令回放玩家的操作。关于这个 toScript() 方法稍微再解释一下,一般游戏里面会嵌入脚本引擎,例如 lua ,因此可以方便地将一个操作转化成可执行的 lua 脚本。3) 网络应用
命令对象可以方便地在网络中进行传输,这样一个一个指令就可以同时在多个机器上运行。例如在移动游戏的PVE战斗中,客户端会先将玩家在战斗中的所有操作记录下来,在战斗结束后将所有操作上传给服务器再重新计算一遍,确保客户端没有作弊。
4) 进度条
将每个需要加载的操作作为命令对象,并给命令对象实现一个加载时间预估的方法,可能使得进度条能够较为精准得反映加载进度。
5) 新手引导
使用命令模式可以实现某些类型新手引导的无缝切入,例如某个新手引导只需要玩家一路点击 next 直到点击确认进入实际的游戏环境。那么可以把这个新手引导作为一个命令对象,在需要展示该引导的时候新建命令对象,让该命令对象处理 next 的交互逻辑与每个引导页的展示逻辑,等到完成的时候,命令对象直接调用传入接收者的执行方法完成转换。
2、具体案例
笔者实现了一个简单的 TicTacToe 游戏来介绍命令模式以及其撤销重做的变体。 TicTacToe 的基础游戏逻辑参考了 Andrew Arnott 的 Java 实现,在第三节中有相关引用。
1)伪代码
笔者将案例上传在这里:
README 中有对每个代码文件的基本说明。2)点评
命令模式在游戏开发中真的十分常用,可以说是不可避免会遇到的一个模式。其撤销、重做变体更是应用广泛,随处可见该模式的身影。希望各位看官能够掌握命令模式。
另一方面该模式的专用性也比较强,在一个游戏项目中,能够抽象成命令的概念并不多,但是一旦遇到了就能比较容易的联想到,是个比较容易识别的模式。3)评级
游戏中使用频度评级:★★★★☆
三、参考
[via Robert Nystrom] (貌似是本很不错的讲游戏中设计模式的书,美中不足没有中文版,Unity主程大大的日常(阿斌) 正在翻译。在命令模式一节中,作者详细介绍了该模式在游戏角色控制中的应用,并且介绍了撤销重做功能作为命令模式的一个变体该如何实现。)
[via wolf96 in CSDN] (博主大概是根据 Game Programming Patterns 写了一个 Unity上的实现,写的没那么详细,但是浅显易懂,也不错。)
[via Andrew Arnott](使用 Java 实现了井字棋游戏的撤销和重做功能,虽然也是用命令模式,但是实现的方式与 Game Programming Patterns 有一点点不一样,这里使用了一个 Command Manager 来管理命令,内部分别保存 undo 和 redo 的命令栈,而不是只使用一个栈。)
[](作者提到使用命令模式需要解决的两个实际问题,但是似乎并没有人给出合适的解决方案)
- [via eriknordeus](同样是利用命令模式来控制用户输入,实现撤销重做的功能。在这本书的例子里,作者把重做作为一条命令来进行执行,并且让命令来管理命令列表。笔者觉得这样的实现方式不大合适,一来耦合度提高了,二来概念上undo、redo与其他命令其实并不是平级关系。)
[via 仙道菜](游戏编程模式- 再探Command模式 的一个翻译)
[via java_my_life](里面提到一个录音系统的示例,并不是很贴合这个模式,但是里面提到了一点,使用命令模式可以方便得实现复合命令,即宏命令)
[via me115](对命令模式相对介绍的比较详细,示例代码用 C++ 给出)
(对命令模式的概念以及应用领域做了十分详细的说明,本文的应用部分参考了其中的内容)