命令模式

在面向对象的世界里,命令模式(the Command Pattern)是一只奇怪的野兽。和大多数对象不同,一个命令对象相当于一个动词(verb)而不是一个名词(noun),但是命令模式又不同于函数。

什么是命令模式

就像我说的,一个命令对象相当于一个动词。还有一种说法是,命令模式是一种对象方法的封装方式。 简单来说,它会作为一个方法实现对象和一个方法调用对象中间的抽象层。这对用户界面来说非常有用。像往常一样,直接给出一个代码示例更直观。

假设我们要做一个闹钟的应用,就和你手机上的那种非常相似。你可以有一个闹钟列表,数量可以是从0到无穷大,不像我的手机上最多只能设置4个闹钟。为了这个应用我需要一个Alarm对象,它包含闹钟的状态和设置选项。现在,我们只关心几个特定方法的实现:enabledisableresetset

我们为第一个方法创建一个命令对象来封闭它:

var EnableAlarm = function(alarm) {
    this.alarm = alarm;
}
EnableAlarm.prototype.execute = function () {
    this.alarm.enable();
}

var DisableAlarm = function(alarm) {
    this.alarm = alarm;
}
DisableAlarm.prototype.execute = function () {
    this.alarm.disable();
}

var ResetAlarm = function(alarm) {
    this.alarm = alarm;
}
ResetAlarm.prototype.execute = function () {
    this.alarm.reset();
}

var SetAlarm = function(alarm) {
    this.alarm = alarm;
}
SetAlarm.prototype.execute = function () {
    this.alarm.set();
}

注意每个命令对象只关注一个接口。在这个例子里,每个接口只定义一个方法,每个方法本身只调用一个函数。在这种情况下,你可能会忽略正在做的事情,只是使用这些回调函数,本质上其实就是使用命令对象本身。在这个例子中你当然还是用的命令模式,只是你自己不知道,因为它们全都被当做回调函数来用。

现我们要使用这些命令对象了。我们把这些命令对象传进一个UI对象里,作为一个按钮添加到屏幕上,当按钮被点击时就执行刚才传入的命令对象上的execute方法。按钮点击时当然知道调用哪个方法,因为所有的命令对象都实现相同的接口。

var alarms = [/* 闹钟列表 */],
    i = 0, len = alarms.length;

for (; i < len; i++) {
    var enable_alarm = new EnableAlarm(alarms[i]),
        disable_alarm = new DisableAlarm(alarms[i]),
        reset_alarm = new ResetAlarm(alarms[i]),
        set_alarm = new SetAlarm(alarms[i]);

    new Button('enable', enable_alarm);
    new Button('disable', disable_alarm);
    new Button('reset', reset_alarm);
    new Button('set', set_alarm);
}

命令模式的4个部分

命令模式由四个主要部分组成。第一个也是最明显的一个就是命令对象。从前面讲的你应该知道它是什么了。另外三个部分分别是:委托者(client),调用者(invoker)和接受者(receiver)。委托者是指创建命令对象并把它传递给调用者的那段代码。那么上面的例子里也就是for循环里的代码片段。调用者是使用这个命令对象并调用它上面(那些)方法的那个对象。最后,接受者是被调用方法所在的对象,也就是上例中的alarms数组元素。

没有这4部分就不能称作命令模式。认识到这一点,你可能会认为我说的命令模式不同于回调函数似乎有些偏颇,对吗?我并不认为。因为我相信JavaScript足够灵活,我们可以在操作一个函数时就像在操作命令对象。我们上面发生在命令对象里的4件事(译注:指enabledisableresetset),都是包含在接受者里的。再没有其他的抽象层。委托者现在需要知道接受者的函数的名字,然而之前委托者并不需要知道这些,此外它需要知道是命令对象代替了它们。你损失了抽象层和模块化,但是你却获得了更容易理解和运行更快的代码。

如果你想在命令对象和回调函数之间妥协,那么请看下面这个例子:

var makeEnableCommand = function (alarm) {
    return function() {
        alarm.enable();
    }
}

var makeDisableCommand = function (alarm) {
    return function() {
        alarm.disable();
    }
}

var makeResetCommand = function (alarm) {
    return function() {
        alarm.reset();
    }
}

var makeSetCommand = function (alarm) {
    return function() {
        alarm.set();
    }
}

因为不用创建对象再去调用方法,所以代码没有那么多。换成直接创建方法,再把它当回调函数来用。这其实没什么用,除非你打算用它来做比现在更多的事。它可以作为一种增加代码安全性的手段,假设调用者的代码是第三方的,它可能添加,修改或替换接受者的属性。虽然这种可能性比较小。