5.2 COMMAND(命令) — 对象行为型模式

1 意图

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

2 别名

动作( Action ),事务( Transaction )

3 动机

有时必须向某对象提交请求,但并不知道关于被请求的操作或请求的接受者的任何信息。例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。但工具箱,不能显式的在按钮或菜单实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作。而工具箱的设计者无法知道请求的接受者或执行的操作。

命令模式通过将请求本身变成一个对象使工具箱对象可向未指定的应用对象提出请求。这个对象可被存储并像其他的对象一样被传递。这一模式的关键是一个抽象的Command类,它定义了一个执行操作的接口。其最简单的形式是一个抽象的Excute操作。具体的Command子类将接受者作为其一个实例变量,并实现Execute操作,指定接收站采取的动作。而接收者有执行该请求所需的具体信息。


image.png

用Command对象可以可很容易的实现菜单(Menu),每一菜单中的选项都是一个菜单项(MenuItem)类的实例。一个Application类创建这些菜单和它们的菜单项以及其余的用户界面。该Application类还跟踪用户已打开的Document对象。

该应用为每一个菜单项配置一个具体的Command子类的实例。当用户选择了一个菜单项
时,该 MenuItem对象调用它的Command对象的Execute方法,而Execute执行相应操作。
MenuItem对象并不知道它们使用的是Command的哪一个子类。Command子类里存放着请求的接收者,而Execute操作将调用该接收者的一个或多个操作。

例如,PasteCommand支持从剪贴板向一个文档 ( Document )粘贴正文。PasteCommand的接收者是一个文档对象,该对象是实例化时提供的。Execute操作将调用该 Document的Paste操作。


image.png

而OpenCommand的Execute操作却有所不同:它提示用户输入一个文档名,创建一个相应的文档对象,将其作为接受者的应用对象中,并打开文档。


image.png

有时一个MenuItem需要执行一系列命令。例如,使一个页面按正常大小居中的MenuItem可由一个CenterDocumentCommand对象和一个NormalSizeCommand对象构建。因为这种需将多条命令串接起来的情况很常见。我们定义一个 MacroCommand类来让一个MenuItem执行任意数目的命令。 MacroCommand 是一个具体的Command 子类,它执行一个命令序列。MacroCommand没有明确的接收者,而序列中的命令各自定义其接收者。
image.png

请注意这些例子中Command模式是怎样解耦调用操作的对象和具有执行该操作所需信息的那个对象的。这使我们在设计用户界面时拥有很大的灵活性。一个应用如果想让一个菜单与一个按钮代表同一项功能,只需让它们共享相应具体Command子类的同一个实例即可。我们还可以动态地替换Command对象,这可用于实现上下文有关的菜单。我们也可通过将几个命令组成更大的命令的形式来支持命令脚本 (command scripting)。所有这些之所以成为可能乃是因为提交一个请求的对象仅需知道如何提交它,而不需知道该请求将会被如何执行。

4 适用性

当你有如下需求时,可使用 Command模式:

  • 1 像上面讨论的MenuItem对象那样,抽象出待执行的动作以参数化某对象。你可用过程语言中的回调(callback)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Command模式是回调机制的一个面向对象的替代品。

  • 2 在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接受者可用一种与地址空间无关的方式表达。那么就可以将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。

  • 3 支持取消操作。Command的Excute操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command接口必须添加一个 UnExecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用UnExcute和Execute来实现重数不限的“取消”和“重做”。

  • 4 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Execute操作重新执行它们。

  • 5 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( transaction )的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。

5 结构
image.png
6 参与者
  • Command
    ——声明执行操作的接口
  • ConcreteCommand(PasteCommand,OpenCommand)
    ——将一个接收者对象绑定于一个动作
    ——调用接收者相应的操作,以实现Execute
  • Client(Application)
    ——创建一个具体命令对象并设定它的接收者。
  • Invoke(MenuItem)
    ——要求该命令执行这个请求。
  • Receiver(Document,Application)
    ——知道如何实施与执行一个请求相关的操作,任何类都可能作为一个接收者;
7 协作
  • Client创建一个ConcreteCommand对象并指定它的Receiver对象;
  • 某Invoker对象存储该ConcreteCommand对象;
  • 该Invoker通过调用Command对象的Execute操作来提交一个请求。若该命令是可撤消的,ConcreteCommand就在执行Execute操作之前存储当前状态以用于取消该命令。
  • ConcreteCommand对象对调用它的R e c e i v e r的一些操作以执行该请求。


    image.png

    下图展示了这些对象之间的交互。它说明了Command是如何将调用者和接收者 (以及它执
    行的请求)解耦的。

8 效果

Command模式有以下效果:

  • 1 Command模式将调用操作的对象与知道如何实现该操作的对象解耦;
  • 2 Command是头等的对象。它们可像其他的对象一样被操纵和扩展;
  • 3 你可将多个命令装配成一个复合命令。例如是前面描述的MacroCommand类。一般说来,复合命令是Composite模式的一个实例;
  • 4 增加新的Command很容易,因为这无需改变已有的类;
9 实现

实现Command模式时考虑以下问题:

  • 1 一个命令对象应达到何种智能程度 命令对象的能力可大可小。一个极端是它仅确定
    一个接收者和执行该请求的动作。另一极端是它自己实现所有功能,根本不需要额外的接收
    者对象。当需要定义与已有的类无关的命令,当没有合适的接收者,或当一个命令隐式地知
    道它的接收者时,可以使用后一极端方式。例如,创建另一个应用窗口的命令对象本身可能
    和任何其他的对象一样有能力创建该窗口。在这两个极端间的情况是命令对象有足够的信息
    可以动态的找到它们的接收者。

  • 2 支持取消和重做 如果Command提供方法逆转( reverse )它们操作的执
    行 ( 例如Unexecute 或Undo操作 ) ,就可支持取消和重做功能。为达到这个目的,
    ConcreteCommand类可能需要存储额外的状态信息。这个状态包括:

  • 接收者对象,它真正执行处理该请求的各操作;

  • 接收者上执行操作的参数;

  • 如果处理请求的操作会改变接收者对象中的某些值,那么这些值也必须先存储起来。接收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态。

若应用只支持一次取消操作,那么只需存储最近一次被执行的命令。而若要支持多级的
取消和重做,就需要有一个已被执行命令的历史表列 (history list),该表列的最大长度决定了取消和重做的级数。历史表列存储了已被执行的命令序列。向后遍历该表列并逆向执行( reverse - executing )命令是取消它们的结果;向前遍历并执行命令是重执行它们。

有时可能不得不将一个可撤消的命令在它可以被放入历史列表中之前先拷贝下来。这是因为执行原来的请求的命令对象将在稍后执行其他的请求。如果命令的状态在各次调用之间会发生变化,那就必须进行拷贝以区分相同命令的不同调用。

例如,一个删除选定对象的删除命令 ( DeleteCommand )在它每次被执行时,必须存储不同的对象集合。因此该删除命令对象在执行后必须被拷贝,并且将该拷贝放入历史表列中。如果该命令的状态在执行时从不改变,则不需要拷贝,而仅需将一个对该命令的引用放入历史表列中。在放入历史表列中之前必须被拷贝的那些 Command起着原型(参见 Prototype模式(3 . 4))的作用。

  • 3 避免取消操作过程中的错误积累 在实现一个可靠的、能保持原先语义的取消 /重做机
    制时,可能会遇到滞后影响问题。由于命令重复的执行、取消执行,和重执行的过程可能会
    积累错误,以至一个应用的状态最终偏离初始值。这就有必要在Command中存入更多的信息以保证这些对象可被精确地复原成它们的初始状态。这里可使用 Memento模式(5 . 6)来让该Command访问这些信息而不暴露其他对象的内部信息。

  • 4 使用C + +模板 对( 1 )不能被取消 ( 2 )不需要参数的命令,我们可使用 C + +模板来实现,这样可以避免为每一种动作和接收者都创建一个Command子类。我们将在代码示例一节说明这种做法。

10 代码示例

github地址

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