意图

命令模式是一种行为型模式,它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。
简单来说,命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。

问题

假如你正在开发一款新的文字编辑器,当前的任务是创建一个包含多个按钮的工具栏,并让每个按钮对应编辑器的不同操作。你创建了一个非常简洁的按钮类,它不仅可用于生成工具栏上的按钮,还可用于生成各种对话框的通用按钮。
应用中的所有按钮都可以继承相同的类

尽管所有按钮看上去都很相似,但它们可以完成不同的操作(打开、保存、打印和应用等)。你会在哪里放置这些按钮的点击处理代码呢?最简单的解决方案是在使用按钮的每个地方都创建大量的子类。这些子类中包含按钮点击后必须执行的代码。
大量的按钮子类

你很快就意识到这种方式有严重缺陷。首先,你创建了大量的子类,当每次修改基类按钮时,你都有可能需要修改所有子类的代码。简单来说,GUI代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。
多个类实现同一功能

还有一个部分最难办。复制/粘贴文字等操作可能会在多个地方被调用。例如用户可以点击工具栏上小小的 “复制” 按钮,或者通过上下文菜单复制一些内容,又或者直接使用键盘上的Ctrl+C

我们的程序最初只有工具栏,因此可以使用按钮子类来实现各种不同操作。换句话来说,​复制按钮Copy­Button子类包含复制文字的代码是可行的。在实现了上下文菜单、快捷方式和其他功能后,你要么需要将操作代码复制进许多个类中,要么需要让菜单依赖于按钮,而后者是更糟糕的选择。

解决方案

将实际的执行代码包装起来,封装成命令,使得调用者不用关心具体的实现细节,他只需要将这些命令组合起来使用,或者撤销某个命令。一般采用这种设计模式,接口都要求实现撤销操作。

优秀的软件设计通常会将关注点进行分离,而这往往会导致软件的分层。最常见的例子:一层负责用户图像界面;另一层负责业务逻辑。GUI 层负责在屏幕上渲染美观的图形,捕获所有输入并显示用户和程序工作的结果。当需要完成一些重要内容时(比如计算月球轨道或撰写年度报告),GUI 层则会将工作委派给业务逻辑底层。

这在代码中看上去就像这样:一个 GUI 对象传递一些参数来调用一个业务逻辑对象。这个过程通常被描述为一个对象发送请求给另一个对象。
GUI 层可以直接访问业务逻辑层

命令模式建议 GUI 对象不直接提交这些请求。你应该将请求的所有细节(例如调用的对象、方法名称和参数列表)抽取出来组成命令类,该类中仅包含一个用于触发请求的方法。

命令对象负责连接不同的 GUI 和业务逻辑对象。此后,GUI 对象无需了解业务逻辑对象是否获得了请求,也无需了解其对请求进行处理的方式。GUI 对象触发命令即可,命令对象会自行处理所有细节工作。
通过命令访问业务逻辑层

下一步是让所有命令实现相同的接口。该接口通常只有一个没有任何参数的执行方法,让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。此外还有额外的好处,现在你能在运行时切换连接至发送者的命令对象,以此改变发送者的行为。

你可能会注意到遗漏的一块拼图——请求的参数。GUI 对象可以给业务层对象提供一些参数。但执行命令方法没有任何参数,所以我们如何将请求的详情发送给接收者呢? 答案是:使用数据对命令进行预先配置,或者让其能够自行获取数据。
GUI对象将命令委派给命令对象

让我们回到文本编辑器。应用命令模式后,我们不再需要任何按钮子类来实现点击行为。我们只需在按钮Button基类中添加一个成员变量来存储对于命令对象的引用,并在点击后执行该命令即可。

你需要为每个可能的操作实现一系列命令类,并且根据按钮所需行为将命令和按钮连接起来。

其他菜单、快捷方式或整个对话框等 GUI 元素都可以通过相同方式来实现。当用户与 GUI 元素交互时,与其连接的命令将会被执行。现在你很可能已经猜到了,与相同操作相关的元素将会被连接到相同的命令,从而避免了重复代码。

最后,命令成为了减少 GUI 和业务逻辑层之间耦合的中间层。而这仅仅是命令模式所提供的一小部分好处!

结构

  1. 发送者(Sender)——亦称 “触发者(Invoker)”——类负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的引用。发送者触发命令,而不向接收者直接发送请求。注意,发送者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令。
  2. 命令(Command)接口通常仅声明一个执行命令的方法。
  3. 具体命令(Concrete Commands)会实现各种类型的请求。具体命令自身并不完成工作,而是会将调用委派给一个业务逻辑对象。但为了简化代码,这些类可以进行合并。
    接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变,仅允许通过构造函数对这些成员变量进行初始化。
  4. 接收者(Receiver)类包含部分业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作。
  5. 客户端(Client)会创建并配置具体命令对象。客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后,生成的命令就可以与一个或多个发送者相关联了。

实现方式

  1. 声明仅有一个执行方法的命令接口。
  2. 抽取请求并使之成为实现命令接口的具体命令类。每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。所有这些变量的数值都必须通过命令构造函数进行初始化。
  3. 找到担任发送者职责的类。在这些类中添加保存命令的成员变量。发送者只能通过命令接口与其命令进行交互。发送者自身通常并不创建命令对象,而是通过客户端代码获取。
  4. 修改发送者使其执行命令,而非直接将请求发送给接收者。
  5. 客户端必须按照以下顺序来初始化对象:
    • 创建接收者。
    • 创建命令,如有需要可将其关联至接收者。
    • 创建发送者并将其与特定命令关联。

代码演示

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
using System;

namespace RefactoringGuru.DesignPatterns.Command.Conceptual
{
// The Command interface declares a method for executing a command.
public interface ICommand
{
void Execute();
}

// Some commands can implement simple operations on their own.
class SimpleCommand : ICommand
{
private string _payload = string.Empty;

public SimpleCommand(string payload)
{
this._payload = payload;
}

public void Execute()
{
Console.WriteLine($"SimpleCommand: See, I can do simple things like printing ({this._payload})");
}
}

// However, some commands can delegate more complex operations to other
// objects, called "receivers."
class ComplexCommand : ICommand
{
private Receiver _receiver;

// Context data, required for launching the receiver's methods.
private string _a;

private string _b;

// Complex commands can accept one or several receiver objects along
// with any context data via the constructor.
public ComplexCommand(Receiver receiver, string a, string b)
{
this._receiver = receiver;
this._a = a;
this._b = b;
}

// Commands can delegate to any methods of a receiver.
public void Execute()
{
Console.WriteLine("ComplexCommand: Complex stuff should be done by a receiver object.");
this._receiver.DoSomething(this._a);
this._receiver.DoSomethingElse(this._b);
}
}

// The Receiver classes contain some important business logic. They know how
// to perform all kinds of operations, associated with carrying out a
// request. In fact, any class may serve as a Receiver.
class Receiver
{
public void DoSomething(string a)
{
Console.WriteLine($"Receiver: Working on ({a}.)");
}

public void DoSomethingElse(string b)
{
Console.WriteLine($"Receiver: Also working on ({b}.)");
}
}

// The Invoker is associated with one or several commands. It sends a
// request to the command.
class Invoker
{
private ICommand _onStart;

private ICommand _onFinish;

// Initialize commands.
public void SetOnStart(ICommand command)
{
this._onStart = command;
}

public void SetOnFinish(ICommand command)
{
this._onFinish = command;
}

// The Invoker does not depend on concrete command or receiver classes.
// The Invoker passes a request to a receiver indirectly, by executing a
// command.
public void DoSomethingImportant()
{
Console.WriteLine("Invoker: Does anybody want something done before I begin?");
if (this._onStart is ICommand)
{
this._onStart.Execute();
}

Console.WriteLine("Invoker: ...doing something really important...");

Console.WriteLine("Invoker: Does anybody want something done after I finish?");
if (this._onFinish is ICommand)
{
this._onFinish.Execute();
}
}
}

class Program
{
static void Main(string[] args)
{
// The client code can parameterize an invoker with any commands.
Invoker invoker = new Invoker();
invoker.SetOnStart(new SimpleCommand("Say Hi!"));
Receiver receiver = new Receiver();
invoker.SetOnFinish(new ComplexCommand(receiver, "Send email", "Save report"));

invoker.DoSomethingImportant();
}
}
}

执行结果:

1
2
3
4
5
6
7
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

参考原文:命令设计模式