命令模式

概述

Command 命令模式通过将请求封装为命令对象,因此可以让系统参数化处理请求,包含排队、日志、撤回功能。它又被称为 Action 或 Transaction。与过程化的请求调用不同,命令对象在程序中会有更长的生命周期,我们可以在程序运行的任意时刻访问命令对象,且命令对象可以被四处传递。

命令对象将请求的发送者和请求的接收者与请求本身解耦,因此其设计与实现的焦点在于命令对象。假想有一个包含一堆按钮的用户界面,使用命令模式制作时,点击按钮将构建一个命令对象并发送,按钮并不知道命令的接收者具体会执行什么操作;命令的操作由具体的命令子类执行 execute 实现。这样既便于将请求的发送者和接收者解耦,又便于将用户界面的编写逻辑进行分工。

上图中,应用将具体的菜单项和具体的命令子类关联起来。当用户选择菜单项时,会触发执行具体命令子类的 execute 操作,由该具体的命令子类调用请求接收者的处理方法。使用命令模式可以将界面呈现和具体操作解耦;同时也方便组合多种命令构成新的处理逻辑。命令对象容易被扩展和操纵,也容易增加新的命令对象。

撤销命令意味着要将命令的执行动作以状态的形式存入堆栈中。因为有执行堆栈的存在,撤销的动作也可以被重做。当执行堆栈被持久化(即保存为草稿)后,我们也能基于草稿恢复命令。

命令模式提供对一组事务进行建模的方式。命令有一个公共接口,允许你以相同的方式调用事务操作;基于原子操作封装事务系统。

结构

  • Command:定义了执行一个操作 Action 的接口。
  • ConcreteCommand:绑定操作 Action 和消息接收者 Receiver,并实现接收者的具体操作。
  • Client:创建具体命令对象并其接收者,即上文中的 Application。
  • Invoker:执行命令,即上文中的 MenuItem 菜单项。
  • Receiver:接收者指定对具体命令的处理操作,即上文中的 Document 执行复制操作等。

基于上述组件,单个命令的执行流程如下:

在实现命令模式时,命令对象既可以仅确定一个接收者和执行该请求的动作,又可以实现所有功能,无需外部接收者。

若应用仅需要取消一次,那么仅需存储最近一次被执行的命令。若支持多级取消和重做,那么久需要实现记录已执行命令的历史列表。向后遍历该列表并逆向执行命令即 undo 取消命令;向前遍历并执行命令即 redo 重做命令。

示例

以下示例呈现了上文中用户界面的简要实现:

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
interface Command {
execute: () => void;
}

class OpenCommand implements Command {
application: Application;
constructor(app: Application){
this.application = app;
}
execute(){// 创建新的 Document 对象,并添加 application 中,然后打开
const name = askUser();// 询问用户
if (!name){
Document document = new Document(name);
this.application.add(document);
document.open();
}
}
}

class PasteCommand implements Command {
document: Document;
constructor(document: Document){
this.document = document;
}
execute(){
this.document.paste();
}
}

/**
* 用于创建简单的命令 new SimpleCommand(document, "paste");
*/
class SimpleCommand implements Command {
receiver: Receiver;
actionName: string;
constructor(receiver: Receiver, actionName: string){
this.receiver = receiver;
this.actionName = actionName;
}
execute(){
this.receiver[actionName]();
}
}

/**
* 创建宏命令
*/
class MacroCommand implements Command {
commands: Command[];
add(command: Command){
this.commands.push(command);
}
remove(command: Command){
this.commands = this.commands.filter(cmd => cmd != command);
}
execute(){
this.commands.forEach(cmd => {
cmd.execute();
})
}
}

前端应用

富文本编辑器

前端富文本编辑器的实现一种方式是基于 contenteditable 设置可编辑文档,另一种方式是自建编辑内容。当节点的 contenteditable 属性置为 true 时,就可以调用 document.execCommand(aCommandName, aShowDefaultUI, aValueArgument) 调用浏览器命令。浏览器支持如下命令,使用效果可以看 这里

  • undo:撤销。
  • redo:重做。
  • contentReadOnly:设置内容只读。
  • forwardDelete:删除光标前的内容。
  • indent:设置缩进。
  • outdent:取消缩进。
  • underline:下划线。
  • backColor:背景色。
  • hiliteColor:设置选中区域的背景色,需要 useCss 为真值。
  • fontName:字体。
  • fontSize:字体大小。
  • decreaseFontSize:为选中区域添加 small 包裹(IE 不支持)。
  • increaseFontSize:为选中区域添加 big 包裹(IE 不支持)。
  • foreColor:字体颜色。
  • bold:加粗,使用 strong 或 b。
  • italic:添加斜体。
  • strikeThrough:设置中划线。
  • underline:下划线。
  • subscript:设置下标。
  • superscript:设置上标。
  • formatBlock:加粗选中区域,使用 h1 等。
  • removeFormat:移除选中区域的所有格式。
  • heading:为选中区域添加 h1 等包裹。
  • defaultParagraphSeparator:设置默认分段节点,如 br 或 p 等。
  • enableAbsolutePositionEditor:允许拖动绝对定位元素。
  • enableInlineTableEditing:允许对内置表格进行编辑,默认 false。
  • enableObjectResizing:允许调整图片、表格大小,默认 false。
  • createLink:基于选中区域的 url 生成链接,添加 a 包裹。
  • unlink:移除链接中的 a 标签。
  • selectAll:全选可编辑区域内容。
  • delete:删除选中区域内容。
  • copy:拷贝选中区域内容。
  • cut:剪切选中区域内容。
  • paste:粘贴。
  • justifyCenter:居中布局。
  • justifyFull:平铺布局。
  • justifyLeft:居左布局。
  • justifyRight:居右布局。
  • insertBrOnReturn:插入 br。
  • insertHorizontalRule:插入 hr。
  • insertText:插入 text。
  • insertHTML:插入 html。
  • insertImage:插入图片,需要图片地址作为 img 节点的 src 属性。
  • insertOrderedList:插入 ol 列表。
  • insertUnorderedList:插入 ul 列表。
  • insertParagraph:插入 p 节点。
  • ClearAuthenticationCache:清除 auth 缓存。
  • useCSS:使用样式替换标签操作。
  • styleWithCSS:使用样式替换标签操作。