01 意图
命令是一种行为设计模式,它将请求转换为包含有关请求的所有信息的独立对象。此转换允许您将请求作为方法参数传递、延迟或排队请求的执行,并支持可撤消的操作。
02 问题
想象一下,您正在开发一个新的文本编辑器应用程序。您当前的任务是为编辑器的各种操作创建一个带有一堆按钮的工具栏。您创建了一个非常简洁的Button
类,可用于工具栏上的按钮,以及各种对话框中的通用按钮。
虽然所有这些按钮看起来都很相似,但它们都应该做不同的事情。您会将这些按钮的各种单击处理程序的代码放在哪里?最简单的解决方案是为每个使用按钮的地方创建大量子类。这些子类将包含必须在单击按钮时执行的代码。
不久之后,您就会意识到这种方法存在严重缺陷。首先,您有大量的子类,如果您每次修改基Button
类时不冒破坏这些子类中的代码的风险,那就没问题了。简而言之,您的 GUI 代码已经笨拙地依赖于业务逻辑的易变代码。
这是最丑陋的部分。某些操作,例如复制/粘贴文本,需要从多个位置调用。例如,用户可以单击工具栏上的“复制”小按钮,或通过上下文菜单复制某些内容,或者只是敲击Ctrl+C
键盘。
最初,当我们的应用程序只有工具栏时,可以将各种操作的实现放在按钮子类中。换句话说,在CopyButton
子类中复制文本的代码就可以了。但是,当您实现上下文菜单、快捷方式和其他内容时,您必须在许多类中复制操作代码或使菜单依赖于按钮,这是一个更糟糕的选择。
03 解决方案
好的软件设计通常基于关注点分离的原则,这通常会导致应用程序被分成多个层。最常见的例子:一个用于图形用户界面的层和另一个用于业务逻辑的层。GUI 层负责在屏幕上渲染漂亮的图片,捕获任何输入并显示用户和应用程序正在执行的操作的结果。然而,当涉及到做一些重要的事情时,比如计算月球的轨迹或撰写年度报告,GUI 层会将工作委托给底层的业务逻辑层。
在代码中它可能看起来像这样:GUI 对象调用业务逻辑对象的方法,并传递一些参数。这个过程通常被描述为一个对象向另一个对象发送请求。
命令模式建议 GUI 对象不应该直接发送这些请求。相反,您应该将所有请求的详细信息(例如被调用的对象、方法的名称和参数列表)提取到一个单独的命令类中,并使用一个触发该请求的方法。
命令对象充当各种 GUI 和业务逻辑对象之间的链接。从现在开始,GUI 对象不需要知道哪个业务逻辑对象将接收请求以及如何处理它。GUI 对象只是触发处理所有细节的命令。
下一步是让您的命令实现相同的接口。通常它只有一个不带参数的执行方法。此接口允许您对同一请求发送者使用各种命令,而无需将其耦合到具体的命令类。作为奖励,现在您可以切换链接到发送者的命令对象,从而有效地改变发送者在运行时的行为。
您可能已经注意到这个难题中缺少的一块,那就是请求参数。GUI 对象可能已经为业务层对象提供了一些参数。由于命令执行方法没有任何参数,我们如何将请求详细信息传递给接收者?事实证明,命令应该预先配置这些数据,或者能够自行获取。
让我们回到我们的文本编辑器。应用命令模式后,我们不再需要所有这些按钮子类来实现各种点击行为。将单个字段放入Button
存储对命令对象的引用的基类中并让按钮在单击时执行该命令就足够了。
您将为每个可能的操作实现一堆命令类,并将它们与特定按钮链接,具体取决于按钮的预期行为。
其他 GUI 元素,例如菜单、快捷方式或整个对话框,可以以相同的方式实现。它们将链接到当用户与 GUI 元素交互时执行的命令。正如您现在可能已经猜到的那样,与相同操作相关的元素将链接到相同的命令,从而防止任何代码重复。
因此,命令成为一个方便的中间层,减少了 GUI 和业务逻辑层之间的耦合。而这只是命令模式所能提供的一小部分好处!
04 举个栗子
在城市里走了很长一段路后,你到了一家不错的餐厅,坐在靠窗的桌子旁。一位友好的服务员走近您并迅速接受您的订单,并将其写在一张纸上。服务员走到厨房,把点单贴在墙上。过了一会儿,订单送到了厨师那里,厨师阅读并相应地做饭。厨师将餐点与订单一起放在托盘上。服务员发现托盘,检查订单以确保一切都如您所愿,然后将所有东西带到您的餐桌上。
纸张订单用作命令。它一直排在队列中,直到厨师准备好上菜。该订单包含烹饪餐点所需的所有相关信息。它允许厨师立即开始烹饪,而不是直接向您澄清订单详细信息。
05 结构实现
在此示例中,命令模式有助于跟踪已执行操作的历史记录,并可以在需要时恢复操作。
导致更改编辑器状态(例如,剪切和粘贴)的命令在执行与该命令相关联的操作之前制作编辑器状态的备份副本。执行命令后,它与编辑器当时状态的备份副本一起放入命令历史记录(命令对象堆栈)。稍后,如果用户需要恢复操作,应用程序可以从历史记录中获取最近的命令,读取编辑器状态的关联备份,并恢复它。
客户端代码(GUI 元素、命令历史记录等)不与具体的命令类耦合,因为它通过命令接口处理命令。这种方法让您可以在不破坏任何现有代码的情况下将新命令引入应用程序。
// The base command class defines the common interface for all
// concrete commands.
abstract class Command is
protected field app: Application
protected field editor: Editor
protected field backup: text
constructor Command(app: Application, editor: Editor) is
this.app = app
this.editor = editor
// Make a backup of the editor's state.
method saveBackup() is
backup = editor.text
// Restore the editor's state.
method undo() is
editor.text = backup
// The execution method is declared abstract to force all
// concrete commands to provide their own implementations.
// The method must return true or false depending on whether
// the command changes the editor's state.
abstract method execute()
// The concrete commands go here.
class CopyCommand extends Command is
// The copy command isn't saved to the history since it
// doesn't change the editor's state.
method execute() is
app.clipboard = editor.getSelection()
return false
class CutCommand extends Command is
// The cut command does change the editor's state, therefore
// it must be saved to the history. And it'll be saved as
// long as the method returns true.
method execute() is
saveBackup()
app.clipboard = editor.getSelection()
editor.deleteSelection()
return true
class PasteCommand extends Command is
method execute() is
saveBackup()
editor.replaceSelection(app.clipboard)
return true
// The undo operation is also a command.
class UndoCommand extends Command is
method execute() is
app.undo()
return false
// The global command history is just a stack.
class CommandHistory is
private field history: array of Command
// Last in...
method push(c: Command) is
// Push the command to the end of the history array.
// ...first out
method pop():Command is
// Get the most recent command from the history.
// The editor class has actual text editing operations. It plays
// the role of a receiver: all commands end up delegating
// execution to the editor's methods.
class Editor is
field text: string
method getSelection() is
// Return selected text.
method deleteSelection() is
// Delete selected text.
method replaceSelection(text) is
// Insert the clipboard's contents at the current
// position.
// The application class sets up object relations. It acts as a
// sender: when something needs to be done, it creates a command
// object and executes it.
class Application is
field clipboard: string
field editors: array of Editors
field activeEditor: Editor
field history: CommandHistory
// The code which assigns commands to UI objects may look
// like this.
method createUI() is
// ...
copy = function() { executeCommand(
new CopyCommand(this, activeEditor)) }
copyButton.setCommand(copy)
shortcuts.onKeyPress("Ctrl+C", copy)
cut = function() { executeCommand(
new CutCommand(this, activeEditor)) }
cutButton.setCommand(cut)
shortcuts.onKeyPress("Ctrl+X", cut)
paste = function() { executeCommand(
new PasteCommand(this, activeEditor)) }
pasteButton.setCommand(paste)
shortcuts.onKeyPress("Ctrl+V", paste)
undo = function() { executeCommand(
new UndoCommand(this, activeEditor)) }
undoButton.setCommand(undo)
shortcuts.onKeyPress("Ctrl+Z", undo)
// Execute a command and check whether it has to be added to
// the history.
method executeCommand(command) is
if (command.execute)
history.push(command)
// Take the most recent command from the history and run its
// undo method. Note that we don't know the class of that
// command. But we don't have to, since the command knows
// how to undo its own action.
method undo() is
command = history.pop()
if (command != null)
command.undo()
06 适用场景
- 当您想要使用操作参数化对象时,请使用命令模式。
命令模式可以将特定的方法调用变成一个独立的对象。这种变化开辟了许多有趣的用途:您可以将命令作为方法参数传递,将它们存储在其他对象中,在运行时切换链接的命令等。
下面是一个示例:您正在开发一个 GUI 组件,例如上下文菜单,并且您希望您的用户能够配置菜单项,以在最终用户单击某个项目时触发操作。
- 当您想要对操作进行排队、安排它们的执行或远程执行它们时,请使用命令模式。
与任何其他对象一样,命令可以序列化,这意味着将其转换为可以轻松写入文件或数据库的字符串。稍后,该字符串可以恢复为初始命令对象。因此,您可以延迟和安排命令执行。但还有更多!同样,您可以通过网络排队、记录或发送命令。
- 当您想要实现可逆操作时,请使用命令模式。
尽管有很多方法可以实现撤消/重做,但命令模式可能是最流行的。
为了能够还原操作,您需要实现已执行操作的历史记录。命令历史是一个堆栈,其中包含所有已执行的命令对象以及应用程序状态的相关备份。
这种方法有两个缺点。首先,保存应用程序的状态并不容易,因为其中一些状态可能是私有的。这个问题可以通过Memento模式得到缓解。
其次,状态备份可能会消耗大量 RAM。因此,有时您可以求助于替代实现:该命令不是恢复过去的状态,而是执行相反的操作。反向操作也有代价:它可能很难甚至不可能实施。
07 如何实施
使用单个执行方法声明命令接口。
开始将请求提取到实现命令接口的具体命令类中。每个类都必须有一组字段用于存储请求参数以及对实际接收器对象的引用。所有这些值都必须通过命令的构造函数进行初始化。
确定将充当发件人的类。将用于存储命令的字段添加到这些类中。发送者应该只通过命令接口与他们的命令通信。发送者通常不会自己创建命令对象,而是从客户端代码中获取它们。
更改发送者,以便他们执行命令,而不是直接向接收者发送请求。
客户端应按以下顺序初始化对象:
创建接收器。
创建命令,并在需要时将它们与接收器相关联。
创建发件人,并将它们与特定命令相关联。
08 优缺点
欢迎阅读公众号内容: