概念

命令模式(Command Pattern)是一种行为型设计模式,它将请求封装成一个对象,从而允许用不同的请求对客户进行参数化,队列或记录请求日志,并支持可撤销的操作


命令模式通常涉及四个核心角色:命令(Command)、接收者(Receiver)、调用者(Invoker)和客户端(Client)

  • 命令(Command)

    定义了执行操作的接口

    通常包含一个执行操作的方法,以及可能包含一些其他方法用于支持命令的撤销、重做等操作

  • 接收者(Receiver)

    实际执行命令操作的对象

    接收者包含了具体的业务逻辑,负责实现命令接口定义的操作

  • 调用者(Invoker)

    负责调用命令对象来执行请求

    调用者通常不直接与接收者交互,而是通过调用命令对象的方法来执行具体的操作

  • 客户端(Client)

    创建命令对象,并将命令对象传递给调用者来执行请求

    客户端通常不需要知道命令对象的具体实现,只需要知道如何创建命令对象,并将其传递给调用者即可


命令模式的核心思想是将请求封装成一个对象,使得请求的发送者和接收者之间解耦,从而可以灵活地添加、修改和重用命令对象,同时也提供了一种统一的方式来处理请求的执行、撤销和重做等操作

命令模式适用于以下情况:

  • 当需要将请求的发送者和接收者之间解耦,并且希望在不同的请求之间进行参数化时,可以使用命令模式
  • 当希望支持命令的撤销、重做等操作,并且希望将这些操作封装到命令对象中时,命令模式也是一个很好的选择


举个简单的例子,考虑一个遥控器系统

遥控器可以控制不同的家电设备(如电视、音响等),而命令模式可以将每个控制命令(如打开、关闭、调高音量等)封装成一个命令对象,并将命令对象传递给遥控器来执行具体的控制操作

这样,可以实现遥控器与家电设备之间的解耦,同时也提供了一种统一的方式来处理控制命令的执行、撤销和重做等操作


实现条件

  1. 存在命令对象

    命令模式适用于存在一组需要被执行的命令,并且希望将这些命令封装成独立的对象的情况

  2. 需要将请求者和接收者解耦

    命令模式适用于需要将请求者和接收者解耦的情况,即请求者不需要知道接收者的具体实现细节,而是通过命令对象来与接收者进行通信

  3. 需要支持撤销和重做操作

    命令模式适用于需要支持撤销和重做操作的情况,因为命令对象可以保存执行命令的历史记录,并且可以根据需要进行撤销和重做操作

  4. 需要支持命令队列或者日志

    命令模式适用于需要支持命令队列或者日志的情况,因为命令对象可以将所有的命令保存在一个队列中,并且可以将执行命令的日志保存下来


优点

  1. 解耦请求者和接收者

    命令模式将请求封装成命令对象,使得请求者和接收者之间解耦,请求者不需要知道接收者的具体实现细节

  2. 容易扩展新的命令

    由于命令模式将每个命令封装成独立的对象,因此容易扩展新的命令,只需要创建新的命令对象并实现对应的执行逻辑即可

  3. 支持撤销和重做操作

    命令模式可以轻松地支持撤销和重做操作,因为每个命令对象都可以保存执行命令的历史记录,并且可以根据需要进行撤销和重做操作

  4. 支持命令队列和日志

    命令模式可以支持命令队列和日志功能,因为命令对象可以将所有的命令保存在一个队列中,并且可以将执行命令的日志保存下来

  5. 降低系统的耦合度

    命令模式降低了系统的耦合度,使得请求者和接收者之间的关系更加灵活,易于维护和扩展


缺点

  1. 可能导致类爆炸

    命令模式可能会导致类爆炸,因为每个命令都需要定义一个独立的命令类,如果命令较多,可能会导致类的数量增加

  2. 增加代码复杂度

    命令模式可能会增加代码的复杂度,因为需要定义大量的命令类,并且需要正确地组织和管理这些命令类

  3. 可能降低性能

    命令模式可能会降低系统的性能,因为需要创建和管理大量的命令对象,并且需要保存命令的历史记录,可能会导致内存占用和执行时间增加


实现方式

控制器基类

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
import { Command } from './SubClass/Command'

/**
* 远程控制类,用于管理和执行命令
*/
class RemoteControl {
/**
* 构造函数,初始化命令列表和历史记录
*/
constructor() {
this.commands = [] // 储存待执行的命令
this.history = [] // 用于存储执行过的命令
}

/**
* 设置命令
* @param {Command} command - 需要设置的命令对象,必须是Command的实例
* @throws {Error} 会抛出错误如果传入的参数不是Command的实例
*/
setCommand(command) {
if (!(command instanceof Command)) {
throw new Error("传递的参数必须是Command的实例")
}
this.commands.push(command)
}

/**
* 按下按钮执行命令
* 如果有可执行的命令,则执行队列中的第一个命令,并将其移出队列,存入历史记录
*/
pressButton() {
if (this.commands.length > 0) {
const command = this.commands.shift() // 执行队列中的第一个命令
if ('execute' in command) {
command.execute()
this.history.push(command) // 将执行的命令存入历史记录
} else {
console.log("无法执行命令:缺少execute方法")
}
} else {
console.log("没有可执行的命令")
}
}

/**
* 按下撤销按钮,执行撤销操作
* 如果历史记录中有命令,则执行历史记录中的最后一个命令的undo方法
*/
pressUndo() {
if (this.history.length > 0) {
const command = this.history.pop() // 取出历史记录中的最后一个命令
if ('undo' in command) {
command.undo()
} else {
console.log("无法撤销命令:缺少undo方法")
}
} else {
console.log("没有可撤销的命令")
}
}
}

export default RemoteControl


子类

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
126
127
128
129
130
131
132
/**
* Command 类定义了一个命令模式的基本结构。
* 该类是抽象的,不应该直接实例化,它的目的是为了被子类继承。
* @constructor
* @param {Object} device - 与命令相关联的设备对象。
* @throws 会抛出错误如果尝试直接实例化 Command 类。
*/
class Command {
constructor(device) {
if (new.target === Command) {
throw new Error("本类不能实例化")
}
this.device = device
}

/**
* 执行命令的方法。该方法在子类中被重写,以提供具体的命令执行逻辑。
* @throws {Error} 会抛出错误如果该方法在子类中没有被重写。
*/
execute() {
throw new Error("execute 方法必须被重写")
}

/**
* 撤销命令的方法。该方法在子类中被重写,以提供具体的命令撤销逻辑。
* @throws {Error} 会抛出错误如果该方法在子类中没有被重写。
*/
undo() {
throw new Error("undo 方法必须被重写")
}
}

/**
* TurnOnCommand 类继承自 Command 类,用于执行打开设备的操作。
*/
class TurnOnCommand extends Command {
/**
* 执行打开设备的操作。
*/
execute() {
this.device.turnOn()
}

/**
* 撤销打开设备的操作,即关闭设备。
*/
undo() {
this.device.turnOff()
}
}

/**
* TurnOffCommand 类继承自 Command 类,用于执行关闭设备的操作。
*/
class TurnOffCommand extends Command {
/**
* 执行关闭设备的操作。
*/
execute() {
this.device.turnOff()
}

/**
* 撤销关闭设备的操作,即打开设备。
*/
undo() {
this.device.turnOn()
}
}

// 导出 Command, TurnOnCommand, TurnOffCommand 供其他模块使用。
export {
Command,
TurnOnCommand,
TurnOffCommand
}



/**
* Stereo类代表一个音响系统。
*/
class Stereo {
/**
* 打开音响。
* 无参数。
* 无返回值。
*/
turnOn() {
console.log("音响已打开")
}

/**
* 关闭音响。
* 无参数。
* 无返回值。
*/
turnOff() {
console.log("音响已关闭")
}
}

// 导出Stereo类作为默认模块
export default Stereo



/**
* 电视类
*/
class Television {
/**
* 打开电视
* @无参数
* @无返回值
*/
turnOn() {
console.log("电视已打开")
}

/**
* 关闭电视
* @无参数
* @无返回值
*/
turnOff() {
console.log("电视已关闭")
}
}

// 导出电视类作为默认模块
export default Television


怎么使用

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
/**
* 这段代码展示了使用命令模式来控制家电设备(电视和音响)的示例。
* 它通过将具体的打开和关闭命令分配给遥控器的不同按键来实现对家电设备的控制,
* 并且提供了撤销操作的功能。
*/

// 导入远程控制、电视和音响类以及开关命令类
import RemoteControl from '../CommandPattern'
import Television from '../SubClass/Television'
import Stereo from '../SubClass/Stereo'
import { TurnOffCommand, TurnOnCommand } from '../SubClass/Command'

// 创建电视和音响实例
const tv = new Television()
const stereo = new Stereo()

// 创建电视和音响的开关命令实例
const turnOnTvCommand = new TurnOnCommand(tv)
const turnOffTvCommand = new TurnOffCommand(tv)

const turnOnStereoCommand = new TurnOnCommand(stereo)
const turnOffStereoCommand = new TurnOffCommand(stereo)

// 创建远程控制实例,并设置命令
const remoteControl = new RemoteControl()

remoteControl.setCommand(turnOnTvCommand)
remoteControl.setCommand(turnOffTvCommand)
remoteControl.setCommand(turnOnStereoCommand)
remoteControl.setCommand(turnOffStereoCommand)

// 使用遥控器控制电视和音响的开关,并演示撤销操作
remoteControl.pressButton() // 输出:电视已打开
remoteControl.pressButton() // 输出:电视已关闭
remoteControl.pressButton() // 输出:音响已打开
remoteControl.pressButton() // 输出:音响已关闭

// 演示撤销操作,每次按下undo按钮将撤销上一步的操作
remoteControl.pressUndo() // 输出:音响已打开
remoteControl.pressUndo() // 输出:音响已关闭
remoteControl.pressUndo() // 输出:电视已打开
remoteControl.pressUndo() // 输出:电视已关闭

// 当没有更多的命令可以撤销时,给出相应提示
remoteControl.pressUndo() // 输出:没有可撤销的命令


场景

  1. GUI应用程序

    在GUI应用程序中,命令模式常用于实现菜单和工具栏按钮的操作

    每个菜单项或按钮都可以关联一个命令对象,当用户点击时,执行与该命令对象关联的操作

  2. 撤销和重做功能

    命令模式非常适合实现撤销和重做功能

    通过记录执行的命令历史,可以轻松地撤销上一步操作,并且可以再次执行已经撤销的操作

  3. 多线程请求处理

    在多线程环境下,命令模式可以用于将请求封装成独立的命令对象,这样可以安全地将请求发送给不同的线程进行处理

  4. 日程安排

    在日程安排应用程序中,用户可以添加、编辑和删除日程事件

    每个操作可以表示为一个命令对象,以便能够轻松地撤销或重做这些操作

  5. 数据库事务

    在数据库操作中,命令模式可以用于实现事务管理

    每个数据库操作可以表示为一个命令对象,并且可以将多个命令组合成一个事务,以便能够一次性地执行或回滚一系列操作

  6. 远程控制

    类似于你提到的遥控器系统,远程控制设备也是命令模式的典型应用场景

    通过将每个控制命令封装成一个命令对象,并将命令对象传递给远程设备执行具体的控制操作,可以实现设备之间的解耦


源代码