4

精读《设计模式 - Command 命令模式》

 3 years ago
source link: https://zhuanlan.zhihu.com/p/343351271
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

精读《设计模式 - Command 命令模式》

前端开发话题下的优秀答主

Command(命令模式)

Command(命令模式)属于行为型模式。

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

举例子

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

点菜是命令模式

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

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

大型软件系统的操作菜单

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

浏览器请求排队

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

意图解释

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

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

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

改为生成一个对象,序列化这个请求:

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

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

结构图

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

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

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

代码例子

下面例子使用 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():

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

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK