一文读懂命令模式

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 如何实施

  1. 使用单个执行方法声明命令接口。

  2. 开始将请求提取到实现命令接口的具体命令类中。每个类都必须有一组字段用于存储请求参数以及对实际接收器对象的引用。所有这些值都必须通过命令的构造函数进行初始化。

  3. 确定将充当发件人的类。将用于存储命令的字段添加到这些类中。发送者应该只通过命令接口与他们的命令通信。发送者通常不会自己创建命令对象,而是从客户端代码中获取它们。

  4. 更改发送者,以便他们执行命令,而不是直接向接收者发送请求。

  5. 客户端应按以下顺序初始化对象:

  • 创建接收器。

  • 创建命令,并在需要时将它们与接收器相关联。

  • 创建发件人,并将它们与特定命令相关联。

08 优缺点

图片

欢迎阅读公众号内容:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容

  • 命令模式 将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化; 对请求排队或记录日志,以及支持可撤销的操...
    yiyiyuan阅读 611评论 0 0
  • 【学习难度:★★★☆☆,使用频率:★★★★☆】直接出处:命令模式梳理和学习:https://github.com/...
    BruceOuyang阅读 835评论 0 3
  • 设计模式概述 在学习面向对象七大设计原则时需要注意以下几点:a) 高内聚、低耦合和单一职能的“冲突”实际上,这两者...
    彦帧阅读 3,740评论 0 14
  • 目录 本文的结构如下: 什么是命令模式 为什么要用该模式 模式的结构 代码示例 优点和缺点 适用环境 模式应用 总...
    w1992wishes阅读 1,108评论 2 9
  • 命令模式(行为型) 一、相关概述 1). 案例引入 Sunny软件公司开发人员为公司内部OA系统开发了一个桌面版应...
    哈哈大圣阅读 275评论 0 0