设计模式-行为模式之Command

更多请移步我的博客

意图

Command(命令)是一种行为模式,让你可以把请求转换到单独的对象,可以用来把不同的请求参数化,排队或者记录请求,并且支持撤销操作。

问题

假设你在做一个新的文本编辑器。你创建了一个Button类,可以被用做工具栏的按钮,也可以用作对话框的通用按钮。

这些按钮看起来很像,但是它们做不同的事情。此时我们要把针对不同按钮点击的处理代码放在哪里呢?简单的解决方法是为每个按钮创建一个Button的子类,然后把处理点击事件的代码放到子类中。

但是这么做的缺点很明显。首先,你创建了许多子类。其次,GUI代码依赖了易变的业务逻辑代码。

还有更加讨厌的部分。一些操作,比如拷贝文本,可以在几个不同地方调用。比如,工具栏的按钮,上下文菜单的按钮或者使用Ctrl+C。当应用只有按钮时,拷贝逻辑的代码只会出现在CopyButton子类中。但是,现在你需要拷贝相同的代码到其他两个地方。

解决

Command模式建议封装请求到它们各自的命令(command)对象中。如果这个操作有参数,把它们变成command类的字段。

大多数command作为客户端之间的链接,它们触发请求和接收对象,接收对象通过执行一些操作来处理它们。

现在command只有一个无参方法,让各个command遵循通用接口很容易。通常,一个command接口只有一个类似execute()的方法。一旦通用接口创建,你可以用command替换掉客户端代码之前耦合的特定操作。

在编辑器例子中使用了Command模式之后,你讲不再需要Button子类。基础类将需要增加一个字段来保存command对象的引用。一个按钮对象将把用户发起的点击请求委托给关联的command对象,而不是自己去完成处理。command要么他自己执行一些操作要么把它委托给一个业务逻辑对象。

你可以对上下文按钮和热键代码做类似处理。这些类将把工作委托给在它们之间共享的单个command对象。

因此,command类将使得GUI和业务逻辑类的衔接变得很方便。这只是Command众多优点中的一部分。

真实世界的类比

在餐厅下单

你到一个餐厅中并找了个靠窗的位置坐下。服务员拿走了你写在纸上的订单,把它贴在厨房的墙上,它之前还有其他订单。

正如你猜的那样,这个纸条就是一个command。它排在队列中知道一个厨师准备去做。这个订单包含了做这道菜的所有信息。他允许厨师立马开始做菜而不是跑来跑去来搞清楚订单的细节。

结构

structure.png

  1. Invoker保存Command对象的引用,并且当一个操作需要被执行的时候使用它。Invoker通过通用接口和command协作,这个接口通常只有一个execute()方法。Invoker不负责创建command对象。他们通常通过构造函数从客户端获取预创建的command。

  2. 为具体的Command声明接口。接口最少需要一个方法来执行实际操作。

  3. 具体的Command实现实际操作。一些command是自足的,不可变的。它们通过构造方法一次性接收所有必要的数据。其他的需要一个Receiver,作为外部的上下文对象。

  4. Receiver包含特定命令所必需的业务逻辑或数据。命令可以查询这些对象的其他信息或整个操作委托给他们。在某些情况下,为了简单起见,可以将接收者的代码合并到命令类中。

  5. Client创建并配置具体的Command对象。然后把他们传给适当的Invoker。

伪代码

在这个例子中,Command模式用来记录操作历史,还可以还原它。不想之前的例子,这个应用每次用户操作都创建一个新的command。之后在帮助列表中会展示这些个性的命令。

在执行操作前,command会创建编辑器当前状态的备份。执行后,command把自己放到历史栈中。

客户端的代码,比如UI元素,command历史和其他类将不会和具体的command类耦合,因为他们通过command接口来协作。这就允许在不改变已存在代码的情况下新增command。

// Abstract command 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 in order to force all concrete
    // commands to provide their own implementations. The method must return
    // true or false depending on whether or not the command changes the
    // editor's state.
    abstract method execute()


// Concrete commands.
class CopyCommand extends EditorCommand is
    // The copy command is not saved to the history since it does not change
    // editor's state.
    method execute() is
        app.clipboard = editor.getSelection()
        return false

class CutCommand extends EditorCommand is
    // The cut command does change the editor's state, therefore it must be
    // saved to the history. And it will be as long as the method returns true.
    method execute() is
        saveBackup()
        app.clipboard = editor.getSelection()
        editor.deleteSelection()
        return true

class PasteCommand implements Command is
    method execute() is
        saveBackup()
        editor.replaceSelection(app.clipboard)
        return true

// The undo operation is also a command.
class UndoCommand implements 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 command to the end of history array.

    // ...first out
    method pop():Command is
        Get the most recent command from history.


// The editor class has an 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 clipboard contents at 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 last command from the history and run its undo method. Note that
    // we do not 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()

适用性

  • 当你想要把行为参数化成对象。举个例子,你在开发一个用户接口组件,比如一个菜单,你想你的用户可以配置菜单元素被点击之后的行为。

    Command模式将操作转换为可以从各种UI元素链接的对象。每个元素把工作委托给command对象而不是自己去做。command可以自己执行操作或者调用时昂的业务逻辑对象。

  • 当你需要排队,规划,或者执行远程操作。

    和其他任何对象以牙膏,command是可以序列化的,意味着它可以转换成一个字符串。这个字符串可以背保存到一个文件或者数据库并且稍后可以再把它转换成一个commadn对象。甚至,你可以通过网络发送一个序列化的command,然后在远程服务上恢复并执行它。

  • 当你需要撤销操作。

    想要支持撤销操作的第一件事就是保存历史。尽管有很多实现方式,Command模式或许是最流行的。

    command历史栈由已执行的command对象组成。每个command在执行操作前线创建当前应用状态的快照。在操作完成后,command把自己压入历史栈中。注意,它始终保持应用程序状态的备份。当需要撤销时,程序从历史栈中拿到栈顶command并且恢复它存储的快照。

    这个方法有两个缺点。首先,保存应用的状态不容易,因为一些是私有的。这个问题可以使用Memento模式缓解。

    其次,状态的保存需要消耗大量RAM。因此,有时候你可以修改实现,不是恢复过去的状态,而是执行命令的逆操作。这个选择是昂贵的。反转操作通常很难以实现甚至不能实现。

如何实现

  1. 声明只有一个execute方法的Command接口。

  2. 在遵循通用Command接口情况下抽离操作到具体的Command实现中。把操作的参数转换成具体command类的字段。他们应当通过command的构造方法来初始化。

  3. 确保command中有字段来持有需要和它协作Receiver对象的引用。这个字段也应该通过构造方法来初始化。

  4. 识别Invoker类并为其提供用于存储command对象的字段。Invoker应当只通过Command的接口和command对象交互。他们通常不是自己创建command对象,而是从客户端获取。

  5. 应用的主要代码,看作Client,应该创建并配置具体的command并且传递给适当的Invoker对象。有时,多个Invoker可以使用相同的command对象。

优点

  • 解耦操作调用和处理类。

  • 允许撤销操作。

  • 允许延迟操作。

  • 允许简单命令组装成更大的命令。

  • 符合开闭原则。

缺点

  • 创建了多个额外类导致代码复杂度上升。

和其他模式的关系

  • Chain Of Responsibility,Command,Mediator和Observer处理连接请求的发送者和接收者的各种方式:

    • 责任链沿着潜在接收者的动态链顺序传递一个请求,直到其中一个处理这个请求。

    • 命令模式建立从发送者到接收者的单向连接。

    • 调解模式持有发送者和接收者间接引用。

    • 观察者会在同一时间把一个请求发送给所有关心的接受者,但是允许它们动态的确定是否继续订阅和取消订阅后面的请求。

  • 责任链中的处理者可以表示为命令(Command)。在这种情况下,许多不同的操作可以在由请求表示的相同上下文中执行。

    但还有另外一种方式,请求本身就是一个Command对象,沿着对象链传递。这种情况下,相同的操作可以在由链条对象表示的不同上下文中执行。

  • Command和Memento可以一起使用。他们可以充当魔法token,被延迟传递和调用。在Command中,token代表一个请求;在Memento中,它代表了某个特定时间的物体的内部状态。多态性对Command而言是重要的,但对于Memento来说却是非常重要的,因为它的接口太狭隘所以Memento只能作为一个值来传递。

  • Command和Strategy很像,因为他们都用于参数化一些行为的上下文。Command被用来转化任意操作到一个对象。操作的参数变成对象的字段。转换允许延迟或远程执行命令,存储命令历史等。

    另一方面,Strategy模式通常来描述做相同事情的不同方式。它可以帮助在单个上下文类中交换这些算法。

  • 在我们需要保存Command拷贝到历史中时Prototype可以提供帮助。

  • Visitor模式就像增强版的Command模式,可以对任何类型的对象执行操作。

小结

Command是行为模式的一种,可以将请求或者简单的操作转换到对象中。这种转换允许延迟或者远程执行命令,存储命令历史等。

在Java中这种模式很常见。大多数情况下,它被用作参数化UI元素的动作回调。也被用来任务排队,追踪操作历史等。

Java核心库中的一些例子:

  • java.lang.Runnable接口的所有实现

  • javax.swing.Action接口的所有实现

参考

翻译整理自:https://refactoring.guru/design-patterns/command

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值