装饰者模式

我今天我会展示另一种JavaScript设计模式:装饰者(the Decorator Pattern),它是一种不通过子类或添加额外属性的方式就可以给对象增加新功能的手段。这篇博客是JavaScript设计模式系列中的一篇,如果你第一次接触这个系列,你可以在本文底部找到前几篇的链接。

关于装饰者模式

让我们回到这篇博客的初衷:学习装饰者模式。就像我说的,这个模式允许我们不通过子类继承的方式给对象添加新功能。相反我们用另一个已经添加了功能的并有相同接口的对象来装饰(包裹)它。为了更好地理解我所说的,我们先假设有一个不了解装饰者模式,并且还是有面向对象编程背景的人。

// 父类
var Car = function() {...};

// 有不同功能的子类
var CarWithPowerLocks = function() {...};
var CarWithPowerWindows = function() {...};
var CarWithPowerLocksAndPowerWindows = function() {...};
var CarWithAC = function() {...};
var CarWithACAndPowerLocks = function() {...};
var CarWithACAndPowerWindows = function() {...};
var CarWithACAndPowerLocksAndPowerWindows = function() {...};
...

如你所见,每一种功能都用一个新的“类(class)”代表。功能不多的情况下还好,一但功能开始增加,它就会变成你的恶梦。当然,如果你想当个土鳖,你就可以用这种方式来开发你的应用,然后把它留给别人维护,但是我不知道人家在想添加新功能(不多,也就5个)时会不会冲上来打你的脸。

使用装饰者模式

谢天谢地,装饰者模式能让事情变得简单,也让后面维护的哥们轻松。首先我们要创建一个基类,只是一个没有任何功能的Car。装饰者就是以它为蓝本来构建接口的。

var Car = function() {
    console.log('装配(assemble):组建车架,添加主要部件');
}

// 装饰者也需要实现这些方法,遵守Car的接口
Car.prototype = {
    start: function() {
        console.log('伴随着引擎的轰鸣声,车子发动了!');
    },
    drive: function() {
        console.log('走起!');
    },
    getPrice: function() {
        return 11000.00;
    }
}

接下来我们会创建装饰者“类”,每一个装饰者都会继承它。你会发现装饰者的每个方法只是简单地包装了一下Car上的同名方法。在下面的例子里,只有assemble(译注:指的是构造函数里log打印的内容,也就是构造函数)和getPrice被重写了。

// 你需要传递一个Car(或者是CarDecorator)才能为它添加功能。
var CarDecorator = function(car) {
    this.car = car;
}

// CarDecorator 实现相同的接口
CarDecorator.prototype = {
    start: function() {
        this.car.start();
    },
    drive: function() {
        this.car.drive();
    },
    getPrice: function() {
        return this.car.getPrice();
    }
}

接下来我们要为每一个功能创建一个装饰者对象,重写父级方法,添加我们想要的功能。

var PowerLocksDecorator = function(car) {
    // 这是JavaScript里调用父类构造函数的方式
    CarDecorator.call(this, car);
    console.log('装配:添加动力锁');
}
PowerLocksDecorator.prototype = new CarDecorator();
PowerLocksDecorator.prototype.drive = function() {
    // 你可以这么写
    this.car.drive();
    // 或者你可以调用父类的drive方法:
    // CarDecorator.prototype.drive.call(this);
    console.log('车门自动上锁');
}

var PowerWindowsDecorator = function(car) {
    CarDecorator.call(this, car);
    console.log('装配:添加动力表盘');
}
PowerWindowsDecorator.prototype = new CarDecorator();

var ACDecorator = function(car) {
    CarDecorator.call(this, car);
    console.log('装配:添加空调');
}
ACDecorator.prototype = new CarDecorator();
ACDecorator.prototype.start = function() {
    this.car.start();
    console.log('冷风吹起来');
}

注意我们在包装对象里总是调用同名方法。这和组合模式多少有些相似,但是这两种模式的相似性也仅限于此。在这个例子中,我们总是先调用装饰者的方法(如果存在这个方法的话)才去执行后面的代码。也就是说有些核心的方法要先执行,但是在另外一些程序中可能并不想按这个顺序来执行,也有可能根本不会调用包装对象的方法,比如就是想完全改变这个功能,而不是在它上面又附加功能。

看看我们的代码能做些什么

那么我们花这么长时间写的代码怎么用呢?实际使用的代码在下面,但我有必要先解释一下。当然如果你认为自己已经明白了,那么就可以直接跳过这段去看代码了。

首先我们创建了一个Car对象,接着我们创建了装饰者,为的是给传递进构造函数里的Car添加我们想要的功能。装饰者的构造函数返回的对象又赋值给了之前用来保存Car对象的那个变量,因为装饰者使用了相同的接口,所以它们(装饰者)也可以被看做是Car对象。我们可以继续添加更多的功能直到我们满意为止,我们拥有了自己梦想中的汽车(car),现在我们可以做自己想做的事了。

var car = new Car();                    // log打印 "装配(assemble):组建车架,添加主要部件"

// 给车装上动力表盘
car = new PowerWindowDecorator(car);    // log打印 "装配:添加动力表盘"

// 现在加装动力锁和空调
car = new PowerLocksDecorator(car);     // log打印 "装配:添加动力锁"
car = new ACDecorator(car);             // log打印 "装配:添加空调"

// 让我们发动这个坏小子出去兜兜风吧!
car.start(); // log打印 '伴随着引擎的轰鸣声,车子发动了!' 和 '冷风吹起来'
car.drive(); // log打印 '走起!' 和 '车门自动上锁'

总结

装饰者模式是保持对象功能差异性的一种很好的方式,从长远来看有助于提高代码的可维护性。你可能已经注意到了,我没有添加任何代码逻辑来避免我们意外添加相同功能。别担心,下一篇博客我们会给出一个不用修改任何现有代码的完美答案。在装饰者里添加检测代码有点恼人。