# Command（命令模式）

Command（命令模式）属于行为型模式。

**意图：将一个请求封装为一个对象，从而使你可用不同的请求对客户进行参数化，对请求排队或记录请求日志，以及支持可撤销的操作。**

## 举例子

如果看不懂上面的意图介绍，没有关系，设计模式需要在日常工作里用起来，结合例子可以加深你的理解，下面我准备了三个例子，让你体会什么场景下会用到这种设计模式。

### 点菜是命令模式

为什么顾客会找服务员点菜，而不是直接冲到后厨盯着厨师做菜？因为做菜比较慢，肯定会出现排队的现象，而且有些菜可能是一起做效率更高，所以将点菜和做菜分离比较容易控制整体效率。

其实这个社会现象就对应编程领域的命令模式：点菜就是一个个请求，点菜员记录的菜单就是将请求生成的对象，点菜员不需要关心怎么做菜、谁来做，他只要把菜单传到后厨即可，由后厨统一调度。

### 大型软件系统的操作菜单

大型软件操作系统都有一个特点，即软件非常复杂，菜单按钮非常多。但由于菜单按钮本身并没有业务逻辑，所以通过菜单按钮点击后触发的业务行为不适合由菜单按钮完成，此时可利用命令模式生成一个或一系列指令，由软件系统的实现部分来真正执行。

### 浏览器请求排队

浏览器的请求不仅会排队，还会取消、重试，因此是个典型的命令模式场景。如果不能将 `window.fetch` 序列化为一个个指令放入到队列中，是无法实现请求排队、取消、重试的。

## 意图解释

**意图：将一个请求封装为一个对象，从而使你可用不同的请求对客户进行参数化，对请求排队或记录请求日志，以及支持可撤销的操作。**

一个请求指的是来自客户端的一个操作，比如菜单按钮点击。重点在点击后并不直接实现，而是将请求封装为一个对象，可以理解为从直接实现：

```typescript
function onClick() {
  // ... balabala 实现逻辑
}
```

改为生成一个对象，序列化这个请求：

```typescript
function onClick() {
  concreteCommand.push({
    // ... 描述这个请求
  })
  // 执行所有命令队列
  concreteCommand.executeAll()
}
```

看上去繁琐了一些，但得到了后面所说的好处：“从而使你可用不同的请求对客户进行参数化”，**也就是可以对任何请求进行参数化存储，我们可以在任意时刻调用。** 这相当于掌握了执行时机，可以在任意时刻调用，以实现排队或记录日志，如果再记录下反向操作信息，就可以实现撤销重做了。

## 结构图

<img width=600 src="https://img.alicdn.com/imgextra/i2/O1CN01preTih1iRMuH3oQYY_!!6000000004409-2-tps-1846-620.png">

Command 是命令的接口，一般固定有一个 `execute` 方法。

ConcreteCommand 是命令接口的实现，它会注入具体执行者 `Receiver`，它实现的 `execute` 方法会调用 `receiver.execute` 来具体执行。

`Invoker` 是执行请求的命令，其实上面都在推入命令，并没有真正执行，如果排队结束或点击撤销重做时，就触发了 Invoker 实际，就该调用对应的 Command 执行啦。

## 代码例子

下面例子使用 typescript 编写。

首先看最终执行态，最终执行需要先添加命令，再执行命令：

```typescript
const command1 = new Command('balabala1')
const command2 = new Command('balabala2')

const invoker = new Invoker()
invoker.push(command1)
invoker.push(command2)
invoker.execute()
```

`Invoker` 内部用一个队列维护，执行的时候其实是 `for` 循环执行了每个 `command.execute()`:

```typescript
class Invoker {
  push(command) {
    // 队列里推入命令
    this.commands.push(command)
  }

  execute() {
    this.commands.forEach(command => command.execute())
    // 别忘了清空 this.commands
  }
}
```

## 弊端

命令模式需要注意序列化大小，一般分为：

1. 仅记录操作。
2. 记录全量快照。
3. 全量快照共享内存。

记录操作是较为精细的管理方式，并且可以延伸出协同编辑功能。记录快照要注意尽量共享内存，防止快照过大，而且协同编辑场景因为快照无法做冲突处理，所以快照模式在协同编辑场景无法应用。

另外要识别没必要使用命令模式的场景，对于没有撤销重做的前端大部分场景来说，都无需改为命令模式。

## 总结

命令模式本质上就是将操作抽象为可序列化的命令，使操作可以在合适的时间执行，这种设计带来了许多额外好处。

利用命令模式可以达到高内聚低耦合的效果，提升代码可维护性，也可以实现撤销重做、协同编辑等功能性需求。

> 讨论地址是：[精读《设计模式 - Command（命令模式）》· Issue #295 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/295)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
