中午快到了,点个外卖,下单支付然后等着送餐干饭。 这中间过程其实包括许多步骤:平台处理请求生成订单 → 餐厅前台接收订单,后厨根据订单内容安排厨师做菜 → 平台调配骑手规划路线去餐厅取餐 -> 骑手送餐等等。你只是点击按钮触发了一个命令,不用管整个中间过程,最终真正做辣椒炒肉的是餐厅里某个厨师,当然,也可能是某个炒菜机器人。
这就是 命令模式 的基本概念:将一个请求(如‘做一份青椒炒肉’)包装起来,由另外的对象触发(‘餐厅前台’)再由具体的接收者(‘厨师’)来真正执行。请求者并不直接与接收者交互,就像你不知道最终是哪台机器人来做你的辣椒炒肉。
1. 命令模式定义
《Head First 设计模式》中命令模式的定义:命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
命令模式 是一种行为设计模式,它可将请求转换为一个包含与请求相关的所有信息(一般包括 ①执行命令所需的参数及 ②真正执行命令的对象)的独立 对象,即 命令。
由于命令对象中包含了执行请求所需的要素,相当于将原来的方法调用 receiver.doSomething(parameters)
中的 方法 接收者、方法名、参数 都封装在一个对象中,我们可以实现 方法参数化,然后在合适的时候执行,从而实现 延迟执行请求,同时我们可以将命令对象存储在队列中并实现相应的 undo
方法,就可以达到 撤销操作 的目的。
2. 命令模式类图
下面是 refactoringguru.cn 上的命令模式类图(原文中以文本编辑器为示例,所以包含一个复制命令 CopyCommand,及编辑器 editor 与按钮 ):
Tip: 可以将类图中指向其他节点较多/被指向较少的节点作为开始入口。
从类图中可以看出 客户端 Client
依赖的对象最多,没有指向 Client
的节点。客户端创建了一个具体的命令 CopyCommand
,并配置命令接收者为编辑器 editor
,然后将命令与一个作为发送者的按钮控件关联起来。点击按钮时就会触发命令执行 excute()
方法,进而在方法内通过接收者实现各种具体的请求 receiver.operation(params)
。可以查看原网站 refactoringguru.cn,介绍地更详细。
命令模式中的角色
角色 | 做什么 | 不做什么 |
---|---|---|
1️⃣ 发送者 Sender / 调用者 Invoker | 触发命令的执行 | 不创建命令不直接访问命令接收者发送请求 |
2️⃣ 命令 Command(协议 or 接口) | 一般仅定义执行命令的方法,也会定义所有具体命令需要的公共参数或依赖的对象。 | 不提供具体实现 |
3️⃣ 具体命令 ConcreteCommand | 实现命令方法,执行具体请求: 1. 将请求传递给接收者 2. 维护完成当前请求需要的额外参数及依赖对象 | - |
4️⃣ 接收者 Receiver | 完成实际的具体工作任务细节 | - |
5️⃣ 客户端 Client | 1. 创建接收者 2. 创建具体命令,配置接收者 3. 创建发送者,并配置命令 | - |
如果严格按照 命令模式 的定义,那仔细一想开头提到的 外卖订单 例子其实没有完全遵循 命令模式(一些书中也以外卖或者到餐馆吃饭作为示例)。因为命令模式中的 命令 是要包含 接收者 的,外卖订单信息中应该包含了是下单用户信息(下单时间、价格信息、配送地址)、餐厅信息、菜品信息、配送员信息等等,但一般是没有包含最终的 接收者 - 真正炒菜的厨师。
所以如果较真的话,下单时允许选择指定特定厨师来做,才跟接近 命令模式 的原始定义。
又或者在订外卖这个场景中我们不再关注 ‘完成菜品的制作’ 这个命令,而是简化调整为 ‘配送外卖’ 这个命令,忽略餐厅的制作过程以及平台分配骑手的过程。顾客下单,即 客户端 创建 具体命令:‘去某地取某物送至另外一个地点,并配置 接收者 - 平台分配的骑手’,平台作为 发送者 触发命令,配送工作实际由具体的骑手完成。
3. 命令模式的优缺点
根据类图及各角色的职责描述,可以看出几乎每个角色都只负责一件事情,接收者可能内部细节过程会相对复杂一点,但也可以概括为一个任务。这也是遵循「单一职责原则」的体现。
客户端作为最外层的‘组织者’,在真实业务系统中一般跟发送者都属于 GUI 层,命令对象将请求与相关信息都封装起来,一般与接收者都属于 逻辑层,通过采用命令模式可以将 ‘发送请求的对象’ 与 ‘接收/执行请求的对象*’* 分隔开,实现解耦。不妨反过来想一下,将 GUI 层 中的控件直接与具体命令中的实现方法关联在一起。随着时间的推移,业务逻辑发生变更需要增加调整参数,或者需要被另一个类中的控件复用(实现相同的功能),又或者业务逻辑的具体实现需要调整,都可能导致 UI 层代码需要相应调整。当然,上面这些问题都可以通过别的方式缓解,例如增加调整参数 - 可以使用模型来封装参数;另一个类中的控件复用 - 可以将实现方法移动到一个独立对象中,再通过这个对象来调用方法。但是仔细一想,在这个基础上再进步规范化增加一个命令协议定义命令执行方法,让这个对象遵循协议并实现方法,然后配置执行方法所需的其他对象及参数,这不就是命令模式的一部分么?
另一方面,如果‘点击控件执行指定方法’应用中并没有可预见的大调整或功能扩展,如 refactoringguru 举例的文本编辑器中需要实现许多命令(复制命令、粘贴命令、保存命令、取消命令等),或者《Head First 设计模式》中举例的多功能遥控器需要动态调整每个遥控按钮所绑定的命令,显然没有必要采用命令模式。
概括起来命令模式的优点:
- 遵循「单一职责原则」:减少 GUI 层(触发命令) 与 业务逻辑层(执行具体请求)之间的耦合。
- 遵循「开闭原则」:不修改已有代码就可以扩展添加新的命令。
- 「多用组合少用继承」:可以方便组合多个命令实现复杂功能。
同时也有一些弊端:
- 显然会相对增加代码量,特别是在简单业务中。(采用设计模式相比一般都会增加代码量/复杂度,以换取架构上的整洁,就像算法里以空间换时间)
适用场景(from refactoringgur.cn):
- 需要通过操作来参数化对象,参数化对象以后就可以保存、传递命令。
- 需要将操作放入队列、远程执行/延迟执行操作。
- 需要实现回滚功能。
4. 工作中的命令模式
不那么严肃地讲,我们的日常工作就是命令模式的实践:
① 发送者,也就是领导,负责安排工作,即触发工作的执行。
② 命令,工作协议,定义完成工作方法,工作之余别忘了学习。
③ 具体命令,各种不同的工作内容,需求调研、原型设计、UI 设计、编码、测试等等。
④ 接受者,一线工具人,负责完成具体的工作。
⑤ 客户端,老板,创建命令例如完成项目,配置若干接受者,并与发送者关联。
那么,你扮演什么角色?欢迎评论交流。
事实上,我们点击打开软件本身以及在电子设备上进行的几乎每一项操作就是作为发送者触发了一个命令,我们不用关心系统处理实现响应的细节,只是触发命令而已,真真的无所不在。
相关
参考
- refactoringguru.cn - 命令模式:包含一个支持撤销操作的文本编辑器示例,命令模式的优缺点、实现方式、与其他模式的关系对比等,同时这个网站系统地总结了 22 种设计模式,支持中文版。
- 《Head First 设计模式》6. 命令模式。可以了解使用宏命令。