定义

命令模式属于对象的行为型模式。命令模式是把一个操作或者行为抽象为一个对象中,通过对命令的抽象化来使得发出命令的责任和执行命令的责任分隔开。命令模式的实现可以提供命令的撤销和恢复功能。

图解

命令模式图解

命令模式的结构图涉及到五个角色:

  • 客户(Client):发出一个具体的命令并确定其接收者。
  • 命令(Command):声明了一个用于给具体命令类实现的抽象接口。
  • 具体命令(ConcreteCommand):定义了一个接收者和行为的弱耦合,负责调用接受者的相应方法。
  • 请求者(Invoker):负责调用命令对象执行命令。
  • 接受者(Receiver):具体行为的执行。

命令模式能用到哪?

想要制作游戏中类似于守望先锋回访系统的“时间回溯”功能,通过卖萌询问群友得知有“命令模式”这么个神奇的存在。

DVA真可爱

利用命令模式这样的程序设计,可以实现诸如撤销,重做,回访,时间倒流之类的功能。

可以参考《Dota2》中的观战系统、《魔兽争霸3》中的录像系统等。

例子

联想到了什么?

我们在介绍InputSystem时提到过,InputSystem想要提供一个平台,统一管理不同外设的输入,通过Action调用,这里面实际上就蕴含了命令模式的思想。

我想出门吃好的

我们举一个例子,来实现一个命令模式:

客户进入餐厅进行点餐,点餐流程为:

  1. 客户进行点餐,填写订单;
  2. 服务员将订单放到订单柜台,并通知厨师;
  3. 厨师根据订单制作餐点。

在这个例子中,一张订单封装了准备餐点的请求,把订单想象成一个用来请求准备餐点的对象,订单对象可以被传递;服务员传递订单到柜台,然后通知厨师订单来了。

具体实现

具体实现是什么样子的呢?

在这个例子中,各角色分别为:

  1. 餐厅是客户(Client)角色,提供点餐功能;
  2. 服务员是请求者(Invoker)角色,用以传递订单;
  3. 没填写的订单就是命令(Command)角色;
  4. 填写好的订单是具体命令(ConcreteCommand)角色;
  5. 厨师是最后的接受者(Receiver)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

public class Chef
{
public void Cook()
{
Printf("做菜!");
}
}

// 命令接口
public interface IOrder
{
void Execute(); // 调用做菜
void Undo(); // 撤销
}

// 做具体的菜,比如我要吃西红柿炒鸡蛋
public class TomatoOmeletteOrder : IOrder
{
private Chef chef;

public TomatoOmeletteOrder(Chef chef)
{
this.chef = chef;
}

public override Execute()
{
chef.Cook();
}
}

// 服务员让厨师做菜
public class Waiter
{
private IOrder order;

public void SetOrder(IOrder order)
{
this.order = order;
}

//下单
public void TakeOrder()
{
order.Execute();
}
}

// 用户使用(客户在客户端中使用)
public class MainClient
{
public static void Main(string[] args)
{
// 餐厅里有服务员和厨师
Waiter waiter = new Waiter();
Chif chif = new Chif();
// 客人具体点了西红柿炒鸡蛋
TomatoOmeletteOrder tOrder = new TomatoOmeletteOrder(chif);

// 服务员给客人下单
waiter.SetOrder(tOrder);
waiter.TakeOrder(); // 最终通过厨师做好了菜
}
}

总结

更多用途

  • 队列请求(命令队列)
  • 日志记录
  • 撤销之前的操作

命令模式的优缺点

优点:

  • 解耦,将发起命令的客户端与具体处理命令的接收者完全解耦,客户端不知道接收者是什么样子。
  • 动态控制,把请求封装,可以动态对请求进行参数化、队列化和日志化等。
  • 很容易的实现复合命令功能。
  • 更好的扩展,很容易添加新命令。

缺点:类的数量会随着命令数量的增长而增长,可能造成类数量过多。