代码改变世界

读书笔记之 - javascript 设计模式 - 命令模式

2014-09-04 13:36 by sai.zhao, ... 阅读, ... 评论, 收藏, 编辑

本章研究的是一种封装方法调用的方式。命令模式与普通函数有所不同。它可以用来对方法调用进行参数化处理和传送,经过这样处理过的方法调用可以在任何需要的时候执行。

它也可以用来消除调用操作的对象和实现操作的对象之间的耦合。这为各种具体的类的更换带来了极大的灵活性。这种模式可以用在许多不同的场合,不过它在创建用户界面这一方面非常有用,特别是在需要不受限的取消操作的时候。它还可以用来替代回调函数,因为它能够提高在对象之间传递的操作的模块化程度。

命令的结构:

  • 最简形式的命令对象是一个操作和用以调用这个操作的对象的结合体。
  • 所有的命令对象都有一个执行操作。其用途就是调用命令对象所绑定的操作。
  • 在大多数命令对象中,这个操作是一个名为execute或者run方法。
  • 使用同样接口的所有命令对象都可以被同等对待,并且互换。

假设:

  • 你有一个广告公司。想设计一个网页。
  • 客户可以在上面执行一些与自己账户相关的操作,比如启用或者停止某些广告
  • 因为不知具体广告数量。你想设计一个尽可能灵活的UI
  • 你打算用命令模式来弱化按钮之类的用户界面元素与其操作之间的耦合
var Interface = function () {}
var adCommand = new Interface('adCommand',['execute']);

接下来需要定义俩个类,分别用来封装广告的start方法和stop方法

var StopAd = function (adObject) {
    this.ad = adObject;
}
StopAd.prototype.execute = function () {
    this.ad.stop();
}

var StartAd = function (adObject) {
    this.ad = adObject;
}
StartAd.prototype.execute = function () {
    this.ad.start();
}

这是俩个非常典型的命令类,他们的构造函数以另一个对象为参数,而他们实现的execute方法则要调用该对象的某个方法。现在有了俩个可用在用户界面中的类,他们具有相同的接口。你不知道也不关心adObject的具体实现细节,只要它实现了start和stop方法就行。借助于命令模式,可以实现用户界面对象与广告对象的隔离.

下面的代码创建的用户界面中,用户名下的每个广告都有俩个按钮,分别用于启动和停止广告的轮播。

var ads = getAds();
for (var i = 0,len = ads.length; i < len; i++) {
    var startCommand = new StartAd(ads[i]);
    var stopCommand = new StopAd(ads[i]);

    new UiButton('Start' + ads[i].name, startCommand);
    new UiButton('Stop' + ads[i].name, stopCommand);
}

UiButton 类的构造函数有俩个参数:一个是按钮上的文字,另一个是命令对象。它会在网页上生成一个按钮,按钮被点击的时候会执行那个目录对象都实现了excute方法,所以把任何一种命令对象提供给UiButtom,后者应该知道如何跟他打交道。这有助于创建高度模块化和低耦合的用户界面。

用闭包创建命令对象。

还有另外一种方法用来封装函数,这种办法不需要创建一个具有execute方法的对象。而是把想要的执行的方法包装在闭包中。如果想要创建的目录对象像前例中那样只有一个方法,那么这种办法尤其方便。现在你不再调用execute方法。因为那个命令可以作为函数直接执行,这样还可以省却作用域和this关键字绑定这方面的烦恼。

function makeStart(addObject) {
    return function () {
        addObject.start();
    }
}
function makeStop(addObject) {
    return function () {
        addObject.stop();
    }
}

var startCommand = makeStart(ads[0]);
var stopCommand = makeStop(ads[0]);
startCommand();
stopCommand();

这些命令函数可以像命令对象一样四处传递,并且在需要的时候执行。他们是正式的命令对象类的简单替代品,但是这个并不适于需要多个命令方法的场合,比如后面的那个实现取消功能的那个实例。

客户、调用者和接收者。

到此你对命令模式已经有了一个大概了解。我们现在做点正式说明。这个系统中有三个参与者:客户-client,调用者-invoking 和接收者-receiving。客户负责实例化命令并将其交给调用者。在前面的例子中,for循环中的代码就是客户。它通常被包装为一个对象,但是这不是必然的。调用者接过命令并将其保存下来。它会在某个时候调用该命令对象的execute方法,或者将其交给另一个潜在的调用者。前例中的调用者就是UiButton类创建的按钮,调用者进行commandObject.execute这种调用时,它所调用的方法将转而以receiver.action()这种形式调用恰当的方法。前例中的接收者就是广告对象,它所执行的操作,要么就是start方法,要么就是stop方法。

什么参与者执行什么任务有时不太好记。这里再重复一遍:客户创建命令;调用者执行该命令;接收者在命令执行时执行相应操作。除客户端外的其它俩个参与者的名称在一定程度上揭示了其作用,这有助于记忆。

所有使用命令模式的系统都有客户和调用者,但不一定有接收者。有些复杂(但是模块化程度较低)的命令并不调用接收者的方法,而是执行一些复杂查询或命令,我们将在后面讨论这种类型的命令。

在命令模式中使用接口

命令模式需要用到某种类型的接口,接口的作用在于确保接收者实现了所需要的操作,以及命令对象实现了正确的操作(他们有各种各样的名称,不过通常叫execute、run、undo)。不进行这种检查的代码比较脆弱,容易在运行期间出现难以排查的错误。你可以在自己的代码中统一定义一个Command接口,但凡使用命令对象的地方,都验检查它是否实现了这个接口,这样一来,其中所有命令对象的执行操作都具有相同的名称,因此无需修改即可交换使用。这个接口大体形如:

var Command = new Interface('Command',['execute']);

有了这个接口,你就可以用类似于下面的代码检查命令对象是否实现了正确的执行操作:

Interface.ensureImplements(someCommand,Command);
someCommand.execute();

如果用闭包来创建命令函数,这种检查更简单,只需检查该命令是否为函数即可。

if(typeof someCommand != 'function'){
    throw new Error('Command is not a function');
}

所有类型的命令对象执行的都是同样的任务:隔离调用操作的对象与实际实施操作的对象。这个定义所涵盖的区间有俩种极端情况,前面创建的那种命令对象属于区间一端,这种情况下的命令对象所起的作用只不过是吧现有接收者的操作(广告对象的start和stop方法)与调用者按钮绑定在一起。这类命令对象最简单,其模块化程度也最高,他们与客户,接收者和调用者之间只是松散的耦合在一起。

var SimpleCommand = function (receving) {
    this.receving = receving;
};
SimpleCommand.prototype.execute = function () {
    this.receving.action();
};

位于区间的另一端的则是那种封装着一套复杂指令的命令对象,这种对象实际上没有接收者,因为他们自己提供了操作的具体实现。它并不把操作委托给接收者实现,所有用于实现相关操作的代码都包含在其内部:

var ComplexCommand = function () {
    this.logger = new Logger();
    this.xhrHandler = XhrManager.createXhrHandler();
    this.parameters = {};
}
ComplexCommand.prototype = {
    setParameter: function (key,value) {
        this.parameters[key] = value;
    },
    execute: function () {
        this.logger.log('Executing command');
        var postArray = [];
        for(var key in this.parameters){
            postArray.push(key + '=' + this.parameters[key]);
        }
        var postString = postArray.join('&');
        this.xhrHandler.request(
            'POST',
            'script.php',
            function(){},
            postString
        );
    }
};

这俩种极端之间存在一个灰色地带。有些命令对象不但封装了接收者的操作,而且其execute方法中也具有一些实现代码,这类命令对象位于定义区间的中间地段:

var GeryAreaCommand = function (receiver) {
    this.logger = new Logger();
    this.receiver = receiver;
}

GeryAreaCommand.prototype.execute = function () {
    this.logger.log('Executing command');
    this.receiver.prepareAction();
    this.receiver.action();
}

这些类型的命令对象各有各的用处,它们都能在项目中找到自己的位置。简单命令对象一般用来消除俩个对象(接收者和调用者)之间的耦合,而复杂命令对象一般用来封装不可分的或事务性的指令。

示例:菜单项

这个示例演示了如何用最简单类型的命令对象构建模块化的用户界面。我们设计一个用来生成桌面应用程序风格的菜单栏的类,并通过使用命令对象,让这些菜单执行各种操作。

借助于命令模式,我们可以把调用者(菜单项—)和接收者(实际执行操作的对象)隔离开。那些菜单项不必了解接收者的方法,它们只需要知道所有命令对象都实现了一个execute方法就行。这意味着同样的命令对象也可以被工具栏图标等其他用户界面元素使用,而且并不需要修改。

这里没有给出接收者类的实现代码,其出发点在于你只需要知道接收者有些什么操作可供调用即可。

FileActions 
    -open()
    -close()
    -save()
    -saveAS()
    
EditActions
    -cut()
    -copy()
    -parste()
    -delete()

InsertACtions
    -textBlock()
    
HelpActions
    -showHelp    

前面说过,接口在命令模式中起着非常重要的作用。这种作用在本例中尤其突出,这是因为我们还要为菜单使用组合模式,而组合对象又严重依赖接口,本例定义三个接口:

var Command = new Interface('Command',['execute']);
var Composite = new Interface('Composite',['add','remove','getChild','getElement']);
var MenuObject = new Interface('MenuObject',['show']);

1.菜单组合对象

接下来要实现的是 MenuBar,Menu,MenuItem 类。作为一个整体,他们要能显示所有可用操作,并且根据要求调用这些操作。

MenuBar 和 Menu都是组合类对象,而MenuItem则是叶类,MenuBar类保存着所有Menu 实例:

var MenuBar = function () {
    this.menus = {};
    this.element = document.createElement('ul');
    this.element.style.display = 'none';
}
MenuBar.prototype = {
    add: function (menuObject) {
        Interface.ensureImplements(menuObject, Composite, MenuObject);
        this.menus[menuObject.name] = menuObject;
        this.element.appendChild(this.menus[menuObject.name].getElement());
    },
    remove: function (name) {
        delete this.menus[name];
    },
    getChild: function (name) {
        return this.menus[name];
    },
    getElement: function () {
        return this.element;
    },
    show: function () {
        this.element.style.display = "block";
        for (var name in this.menus) {
            this.menus[name].show();
        }
    }
}

MenuBar 是一个很简单的组合对象类。它会生成一个无序列表标签,并且提供了想这个列表中添加菜单对象的方法。Menu类与此类似,不过它管理的是MenuItem实例:

var Menu = function (name) {
    this.name = name;
    this.items = {};
    this.element = document.createElement('li');
    this.element.innerHTML = this.name;
    this.element.style.display='none';
    this.container = document.createElement('ul');
    this.element.appendChild(this.container);
}
MenuBar.prototype = {
    add: function (menuObject) {
        Interface.ensureImplements(menuObject, Composite, MenuObject);
        this.items[menuObject.name] = menuObject;
        this.element.appendChild(this.items[menuObject.name].getElement());
    },
    remove: function (name) {
        delete this.items[name];
    },
    getChild: function (name) {
        return this.items[name];
    },
    getElement: function () {
        return this.element;
    },
    show: function () {
        this.element.style.display = "block";
        for (var name in this.items) {
            this.items[name].show();
        }
    }
}

值得一提的是,Menu类的item的属性只起着一个查找表的作用,他不会保存菜单项的次序信息。菜单项的次序由DOM负责保持。每一条新添加的菜单项,都被添加在已有菜单项之后,如果要求能对菜单项的次序进行重排,那么可以把items属性实现为数组。

真正让人感兴趣的是MenuItem类。这是系统中的调用者类。MenuItem的实例被用户点击时,会调用与其绑定在一起的命令对象。为此需要先确保传入构造函数的命令对象实现了execute方法,然后再为MenuItem对象对应的锚标签注册click事件处理器中加入调用他们的语句。

var MenuItem = function (name,command) {
    Interface.ensureImplements(command, Command);
    this.name = name;
    this.element = document.createElement('li');
    this.element.style.display = 'none';
    this.auchor = document.createElement('a');
    this.auchor.href = '#';
    this.element.appendChild(this.auchor);
    this.auchor.innerHTML = this.name;

    addEvent(this.auchor,'click', function (e) {
        e.preventDefault();
        command.execute();
    });
}

MenuItem.prototype = {
    add: function () {

    },
    remove: function () {

    },
    getChild: function () {

    },
    getElement: function () {
        return this.element;
    },
    show: function () {
        this.element.style.display = 'block';
    }
};

命令模式的作用在此开始显现出来。你可以创建一个包含有许多菜单的非常复杂的菜单栏,而每个菜单栏都包含着一些菜单项。这些菜单项对如何执行自己所绑定的操作一无所知,它们也不需要知道哪些细节,它们唯一需要知道的就是命令对象有一个execute方法。

每个MenuItem都与一个命令对象绑定在一起。这个命令对象不能再改变,因为它被封装在一个事件监听器的闭包中。如果想改变菜单项所绑定的命令,必须另外创建一个新的MenuItem对象。

2.命令类:

MenuCommand这个命令类非常简单。其构造函数的参数就是将被作为操作而调用的方法。因为javascript可以把对方法的引用作为参数传递 ,所以命令类只要把这个引用保存下来,然后在execute方法的执行过程中调用就可以了,这实际上是一个函数包装对象。

var MenuCommand = function (action) {
    this.action = action;
}
MenuCommand.prototype.execute = function () {
    this.action();
};

如果action内部方法用到this关键字,那么它必须包装在一个匿名函数中,如下所示:

ar someCommand = new MenuCommand(function () {
    myObject.someMethod();
})

汇合起来:

这个复杂结构的最终结果中的代码实现很容易理解,其各个部分之间的耦合也比较松散,你需要做的就是创建MenuBar类的一个实例,然后为他添加一些Menu和MenuItem对象。其中每个MenuItem对象都绑定了一个命令对象。

var fileActions = new FileActions();
var EditActions = new EditActions();
var InsertACtions = new InsertACtions();
var HelpActions = new HelpActions();

var appMenuBar = new MenuBar();
//-----------
var fileMenu = new Menu('File');
var openCommand = new MenuCommand(fileActions.open);
var closeCommand = new MenuCommand(fileActions.close);
var saveCommand = new MenuCommand(fileActions.save);
var saveAsCommand = new MenuCommand(fileActions.saveAs);

fileMenu.add(new MenuItem('open', openCommand));
fileMenu.add(new MenuItem('Close', closeCommand));
fileMenu.add(new MenuItem('Save', saveCommand));
fileMenu.add(new MenuItem('Close', saveAsCommand));

appMenuBar.add(fileMenu);
//--------------
var editMenu = new Menu('Edit');
var cutCommand = new MenuCommand(EditActions.cut);
var copyCommand = new MenuCommand(EditActions.copy);
var pasteCommand = new MenuCommand(EditActions.paste);
var deleteCommand = new MenuCommand(EditActions.delete);

editMenu.add(new MenuItem('Cut', cutCommand));
editMenu.add(new MenuItem('Copy', copyCommand));
editMenu.add(new MenuItem('Paste', pasteCommand));
editMenu.add(new MenuItem('Delete', deleteCommand));

appMenuBar.add(editMenu);

//------------
var insertMenu = new Menu('Insert');
var textBlockCommand = new MenuCommand(InsertACtions.textBlock);
insertMenu.add(new MenuItem('Text  Block', textBlockCommand));
appMenuBar.add(insertMenu);

//------------
var helpMenu = new Menu('Help');
var showHelpCommand = new MenuCommand(HelpActions.showHelp());
helpMenu.add(new MenuItem('Show Help', showHelpCommand));
appMenuBar.add(helpMenu);

document.getElementsByTagName('body')[0].appendChild(appMenuBar.getElement());
appMenuBar.show();

要是想为菜单再增添一些菜单项,很容易实现,例如下面俩行代码就能在Insert菜单中添加一个图像命令。

var imageCommand = new MenuCommand(InsertACtions.image);
insertMenu.add(new MenuItem('Image', imageCommand));

这个菜单系统实现了接受用户请求的对象与实现相关操作的对象的隔离。命令模式非常适合用来构建用户界面,这是因为这种模式可以把执行具体工作的类与生成用户界面的类隔离开来。在这种模式中,甚至可以让多个用户界面元素共用一个接收者或命令对象。既然命令可以作为一等对象进行传递和重用,那么它自然应该能够反复执行。甚至被不同的调用者反复执行。

示例:取消操作和命令日志

还有一个方法也经常被实现为命令模式,那就是undo。借助这个方法,调用者可以回滚用execute执行的操作。undo方法可以用来实现不受限制的取消功能。只需要把执行过的命令对象压入栈顶即可实现对命令执行历史的记录。如果用户想撤销最近的操作,他们可以点击取消按钮,这会从栈中弹出最近那个命令并调用该命令的undo方法。

下面模仿一个Etch A Sketch,游戏界面有四个按钮,功能为把指针在上下左右四个方向移动10像素,此外还有一个取消按钮,用来撤销操作。首先,我们必须修改一下Command接口,为其添加一个undo方法。

var ReversibleCommand = new Interface('ReversibleCommand',['execute','undo']);

//创建四个命令,分别向上下左右四个方向移动指针:

var MoveUp = function (cursor) {
    this.cursor = cursor;
}
MoveUp.prototype = {
    execute: function () {
        this.cursor.move(0, -10);
    },
    uodo: function () {
        this.cursor.move(0, 10);
    }
}
var MoveDowm = function (cursor) {
    this.cursor = cursor;
}
MoveDowm.prototype = {
    execute: function () {
        this.cursor.move(0, 10);
    },
    uodo: function () {
        this.cursor.move(0, -10);
    }
}
var MoveLeft = function (cursor) {
    this.cursor = cursor;
}
MoveLeft.prototype = {
    execute: function () {
        this.cursor.move(-10, 0);
    },
    uodo: function () {
        this.cursor.move(10, 0);
    }
}

var MoveRight = function (cursor) {
    this.cursor = cursor;
}
MoveRight.prototype = {
    execute: function () {
        this.cursor.move(10, 0);
    },
    uodo: function () {
        this.cursor.move(-10, 0);
    }
}

这些代码很简单。execute 方法向合适的方向移动指针,而undo方法则向相向的方向把指针移回去。最后,我们还需要有用作调用者的按钮和实际负责实现指针移动的接收者。先来看看接收者:

var Cursor = function (width,height,parent) {
    this.width = width;
    this.height = height;
    this.position = {
        x:width/2,
        y:height/2
    }

    this.canvas = document.createElement('canvas');
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    parent.appendChild(this.canvas);

    this.ctx = this.canvas.getContext('2d');
    this.ctx.fillStyle = '#CCC000';
    this.move(0, 0);
}

Cursor.prototype.move = function (x, y) {
    this.position.x +=x;
    this.position.y += y;
    this.ctx.clearRect(0, 0, this.width, this.height);
    this.ctx.fillRect(this.position.x, this.position.y, 3, 3);
};

Cursor类实现了命令类所要求的操作。本例中涉及的操作只不过在特定位置绘制一个方块。调用命令的是网页上的按钮。本例需要的按钮有俩种,分别是用来调用execute方法的命令按钮,和来来调用undo的取消按钮。

在讲按钮之前,先来看看如何用装饰者模式进一步提高命令类的模块化程度。我们需要在系统中的某个地方加入一些用来把执行过的命令压入栈的代码。这些代码可以写在用户界面类(按钮类)中,这样一来就必须在每一个用户界面类中重复这些代码,如果想把这些命令用于快捷键的话,还得再次实现这种入栈代码。最好的办法是用一个实现了这些代码的装饰者来包装每一个命令。这样我们就可以把命令对象传递给任意的用户界面,不用担心它是否实现了入栈代码。

下面这个装饰者的作用就是在执行一个命令之前先将其压入栈。

var UndoDecorator = function (command,undoStack) {
    this.command = command;
    this.undoStack = undoStack;
}
UndoDecorator.prototype = {
    execute: function () {
        this.undoStack.push(this.command);
        this.command.execute();
    },
    undo: function () {
        this.command.undo();
    }
}

这是装饰者模式出色的运用。藉此我们可以在保留原有接口的前提下为命令增添新的特性。在本例中这些装饰者对象可以与所有命令对象互换使用。

现在来看看用户界面类。这些负责生成必要的html元素,并且为其注册click事件监听器,这些监听器要么调用execute方法要么调用 undo 方法:

var CommandButton = function (label,command,parent) {
    Interface.ensureImplements(command,ReversibleCommand);
    this.element = document.createElement('button');
    this.element.innerHTML = label;
    parent.appendChild(this.element);

    addEvent(this.element,'click', function () {
        command.execute();
    });
}

var UndoButton = function (label,parent,undoStack) {
    this.element = document.createElement('button');
    this.element.innerHTML = label;
    parent.appendChild(this.element);

    addEvent(this.element,'click', function () {
        if(undoStack.length === 0){
            return ;
        }
        var lastCommand = undoStack.pop();
        lastCommand.undo();
    });
}

像UndoDecorator类一样,UndoButton类的构造函数也需要把命令栈作为参数传入。这个栈其实就是一个数组。调用经UndoDecorator对象装饰过的命令对象的execute方法时这个命令对象会被压入栈。为了执行取消操作,取消按钮就会从命令栈中弹出最近的命令并调用其undo方法。这将逆转刚执行的操作。

本例实例代码也比较简单,只需要实例化Cursor和所有命令,创建使用这些命令的按钮,再创建一个空白栈即可:

var body = document.getElementsByTagName('body')[0];
var cursor = new Cursor(400, 400, body);
var undoStack = [];

var upCommand = new UndoDecorator(new MoveUp(cursor), undoStack);
var downCommand = new UndoDecorator(new MoveDowm(cursor), undoStack);
var leftCommand = new UndoDecorator(new MoveLeft(cursor), undoStack);
var rightCommand = new UndoDecorator(new MoveRight(cursor), undoStack);

var upButton = new CommandButton('Up', upCommand, body);
var downButton = new CommandButton('Down', downCommand, body);
var leftButton = new CommandButton('Left', leftCommand, body);
var rightButton = new CommandButton('Right', rightCommand, body);
var undoButton = new UndoButton('Undo', body, undoStack);

这段代码生成的用户界面包含一幅画布 canvas 和 5个按钮,点击4个命令的任何一个按钮都会向相应的方向移动指针,而点击取消按钮则会撤销最后一次移动。

使用命令日志实现不可逆的操作取消:

有些取消是不可逆的,如果我们需要在指针后面留下一串尾迹,那么在画布上画线很容易,不过要取消这条线是不可能的,从A到B划一条线的逆转并不是从B到A画一条线。取消这种操作的唯一办法就是清除状态,然后把之前执行过的操作(不含最近那个)依次重做一遍。为此需要把执行命令都记录在栈中,想取消一个操作,需要做的就是从栈中弹出最近那个命令丢弃,然后清理画布并从头开始重新执行记录下来的所有命令。使用命令日志实现取消操作的系统不要求那些命令都是可逆命令,因此在本例中可以继续使用原来的Command接口。

本例在原先的例子上修改很小,由于大多数代码保持不动,所以我们只讨论那些需要修改的地方。

第一个变化时我们删除了所有命令对象的undo方法,原因是命令对象代表的操作现在不可逆。下面是其中一条命令类,其中undo方法已经删除:

var MoveUp = function (cursor) {
    this.cursor = cursor;
}
MoveUp.prototype = {
    execute: function () {
        this.cursor.move(0, -10);
    }
}

接下来,最大的变化发生在Cursor类代码中。原来用来记录命令的栈undoStack现在成了该类的内部属性,名称也改为commandStack。而UndoDecorator类和所有其他对undoStack的引用被删除。新Cursor如下:

var Cursor = function (width,height,parent) {
    this.width = width;
    this.height = height;
    this.commandStack = [];

    this.canvas = document.createElement('canvas');
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    parent.appendChild(this.canvas);

    this.ctx = this.canvas.getContext('2d');
    this.ctx.fillStyle = '#CCC000';
    this.move(0, 0);
}

Cursor.prototype = {
    move: function (x,y) {
        var _this = this;
        this.commandStack.push(function () {
            _this.lineTo(x,y);
        });
    },
    lineTo: function (x,y) {
        this.position.x +=x;
        this.position.y +=y;
        this.ctx.lineTo(this.position.x, this.position.y);
    },
    executeCommands: function () {
        this.position = {x: this.width / 2, y: this.height / 2};
        this.ctx.clearRect(0, 0, this.width, this.height);
        this.ctx.beginPath();
        this.ctx.moveTo(this.position.x, this.position.y);
        for (var i = 0; i < this.commandStack.length; i++) {
            this.commandStack[i]();
        }
        this.ctx.stroke();
    },
    undo: function () {
        this.commandStack.pop();
        this.executeCommands();
    }
}

这里新增加了3个新方法,原有的move已被修改,它现在所做的就是把操作压入命令栈。然后调用 executeCommands 方法。实际操作画线的是lineTo方法。executeCommands负责重置canvas元素,然后依次执行命令栈中保存的操作。undo方法则会删除最近的那条命令然后调用 executeCommands 重建系统状态。

所有对undoStack的引用都被删除,UndoButton中相关元素的click事件监听器代码也改了。

var undoButton = function (label,parent,cursor) {
    this.element = document.createElement('button');
    this.element.innerHTML = label;
    parent.appendChild(this.element);

    addEvent(this.element,'click', function () {
        cursor.undo();
    });
}

实现代码与原来相同,唯一变化是删除了undoStack,而传给UndoButton 构造函数的参数也改成了一个Cursor实例。

var body = document.getElementsByTagName('body')[0];
var cursor = new Cursor(400, 400, body);

var upCommand = new Moveup(cursor);
var downCommand = new MoveDown(cursor);
var leftCommand = new MoveLeft(cursor);
var rightCommand = new MoveRight(cursor);

var upButton = new CommandButton('Up', upCommand, body);
var downButton = new CommandButton('Down', downCommand, body);
var leftButton = new CommandButton('Left', leftCommand, body);
var rightButton = new CommandButton('Right', rightCommand, body);
var undoButton = new UndoButton('Undo', body, undoStack);

用于奔溃恢复的命令日志:

命令日志的一个有趣用途是在程序奔溃后恢复其状态。在前面那个示例中,可以用XHR把经过序列号处理的命令记录在服务器上。用户下次访问该网页的时候,可以将画布图案精确还原。

命令模式的适用场合:

命令模式的主要用途是把调用对象(用户界面,API和代理等)与实现操作的对象隔离开。照此而言,凡是俩个对象间的互动方式需要更高的模块化程度的时候都可以用到这种模式。这是一种组织型模式,几乎可以用在任何系统。不过,最能体现其效用的还是那种需要对操作进行规范化处理的场合。有了这种规范化处理,一个类或者一个调用者也能调用多种方法,而不需要事先为此了解哪些方法。

许多页面元素非常符合这样特征,比如前面菜单。命令模式可以彻底消除用户界面元素与负责实际工作类之间的耦合。你可以为某个操作创建一个命令对象,然后用菜单项,工具图标和键盘快捷键来调用这个对象。

可以受益于命令模式的还有其他一些特别场合。这种模式可以用来封装XHR调用或其他延迟性调用场合的回调函数,用一个回调函数命令代替回调函数。可以把多条函数调用封装为一个单位。有了命令对象的帮助,实现取消机制非常容易。实现不受限制的取消机制需要把执行过的命令保存在栈中。这种命令日志甚至可以用来取消本质上不可逆的操作,他还可以在任何应用程序奔溃之后用来恢复其整体状态。

命令模式利弊:

好处:

  1. 如果运用得当,可以提高程序的模块化程度和灵活性
  2. 实现取消和状态恢复等复杂的有用特性非常容易

弊端:

  1. 用的勉强,容易浪费使用。如果你不需要模式给予的额外特性,也不需要接口一致,直接使用方法更恰当。
  2. 调试难度加大。总而言之,命令模式是一种用来封装单个操作的结构型模式。

其封装的操作可能是单个方法调用这么简单,也可能是整个子程序那么复杂。经过封装的对象可以作为一等的对象进行传递。命令对象主要用于消除调用者和接收者之间的耦合。这有助于创建高度模块化的调用者,它们对所调用的操作不需要任何了解,这种模式也给程序员实现了接收者的自由,他们不必担心接收者能否用在某套用户界面中。这些负责的用户特性用命令模式很容易实现,不受限制的取消和程序奔溃之后的状态恢复就是这方面的俩个例子。它们还可以用来实现事务,具体做法就是把命令保存在栈中,隔段时间提交一次。

命令模式的最大优点在于,只要是能够在execute方法中实现的操作,不管他有多复杂或者彼此间的差异多大,都能以与任何别的命令完全相同的方式进行传送和调用。借助这种模式,代码的重用几乎可以达到一种不受限制的程度。