装饰器模式
装饰器模式是一种旨在提升代码复用率的结构性模式。有点类似于混入模式,它被认为是一种可以替代子类的可行方案。
一般来说,装饰器提供一种动态的为系统中的类添加行为的能力。装饰器本身没必要有类的基本功能,否则它自己就可以作为父类了。
他们通常可以被用来修改现存的系统,为一些对象加入一些额外的功能(特性),而不用大量修改底层代码。开发人员使用他们的一个常见原因可能包含特征——需要大量不同类型的对象。想象一下要定义几百种不同的对象构造函数,比如一个js游戏。
对象构造函数可能表示不同类型的角色类型,每个角色都有不同的能力。魔戒这款游戏可能需要霍比特人,精灵,兽人,精灵,山岭巨人,石巨人等等,这些很容易有成百上千个。如果我们再考虑能力,想象得为每种能力的结合创建一个子类,例如带指环的霍比特人,带剑的霍比特人,带指环和剑的霍比特人等等。当我们考虑伴随着能力类型数量的增长,这是非常不实用且不可想象的。
装饰器模式没有过分的关注如何对象如何被创建,而是如何扩展他们的功能。相比较原型继承,我们用一个单一的基本对象,并逐步第为它增加提供额外能力的对象的方式工作。这个想法相比较子类,通过增加一些属性或者方法到基本对象上,可以更加精简。
为js对像添加新属性是一个非常直接的过程,考虑着一点,一个非常简单的装饰器可能实现如下:
案例1:具有新功能的装饰构造器
// A vehicle constructor
function Vehicle( vehicleType ){
// some sane defaults
this.vehicleType = vehicleType || "car";
this.model = "default";
this.license = "00000-000";
}
// Test instance for a basic vehicle
var testInstance = new Vehicle( "car" );
console.log( testInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
// Lets create a new instance of vehicle, to be decorated
var truck = new Vehicle( "truck" );
// New functionality we're decorating vehicle with
truck.setModel = function( modelName ){
this.model = modelName;
};
truck.setColor = function( color ){
this.color = color;
};
// Test the value setters and value assignment works correctly
truck.setModel( "CAT" );
truck.setColor( "blue" );
console.log( truck );
// Outputs:
// vehicle:truck, model:CAT, color: blue
// Demonstrate "vehicle" is still unaltered
var secondInstance = new Vehicle( "car" );
console.log( secondInstance );
// Outputs:
// vehicle: car, model:default, license: 00000-000
这种最简单的实现是有多种用途的,但是并没有真正的展示装饰器提供的全部力量。对于这一点,我们将首先完成一个优秀的案例。
案例2:用多个装饰器装饰一个对象
// The constructor to decorate
function MacBook() {
this.cost = function () { return 997; };
this.screenSize = function () { return 11.6; };
}
// Decorator 1
function memory( macbook ) {
var v = macbook.cost();
macbook.cost = function() {
return v + 75;
};
}
// Decorator 2
function engraving( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 200;
};
}
// Decorator 3
function insurance( macbook ){
var v = macbook.cost();
macbook.cost = function(){
return v + 250;
};
}
var mb = new MacBook();
memory( mb );
engraving( mb );
insurance( mb );
// Outputs: 1522
console.log( mb.cost() );
// Outputs: 11.6
console.log( mb.screenSize() );
In the above example, our Decorators are overriding the MacBook() super-class objects .cost() function to return the current price of the Macbook plus the cost of the upgrade being specified.
在上面的例子中,我们就要重写了macbook()父类对象的cost()函数以返回的MacBook升级特定部分后加总的目前价格。
装饰器只修改原对象的部分。
伪-经典 装饰器
这种装饰器模式的特别变种提供参考用途,如果发现它太过复杂,我建议选择一个之前提到过的简单实现。
接口
PJDP 描述装饰器作为一种模式被用来,将对象透明的包装在其他具有相同接口中的对象中。接口是一种一个对象的方法应该做什么的定义方式。然而,他并不指明如何实现这些方法。
他们可以指明方法所使用的参数,但这被认为是可选的。
那么,我们为什么要在js中使用接口呢?我们的像是,接口自身就是种记录,并且可以提高复用性。理论上说,接口也使得代码更稳定,通过确保改变他们也必须改变实现他们的对象。
以下是一种通过js鸭式编程(一种方法,帮助决定一个对象是否是一个构造函数/基于构造函数生成对象的实例,的实现)方式实现的接口的例子。
// Create interfaces using a pre-defined Interface
// constructor that accepts an interface name and
// skeleton methods to expose.
// In our reminder example summary() and placeOrder()
// represent functionality the interface should
// support
var reminder = new Interface( "List", ["summary", "placeOrder"] );
var properties = {
name: "Remember to buy the milk",
date: "05/06/2016",
actions:{
summary: function (){
return "Remember to buy the milk, we are almost out!";
},
placeOrder: function (){
return "Ordering milk from your local grocery store";
}
}
};
// Now create a constructor implementing the above properties
// and methods
function Todo( config ){
// State the methods we expect to be supported
// as well as the Interface instance being checked
// against
Interface.ensureImplements( config.actions, reminder );
this.name = config.name;
this.methods = config.actions;
}
// Create a new instance of our Todo constructor
var todoItem = new Todo( properties );
// Finally test to make sure these function correctly
console.log( todoItem.methods.summary() );
console.log( todoItem.methods.placeOrder() );
// Outputs:
// Remember to buy the milk, we are almost out!
// Ordering milk from your local grocery store
上面的代码中,Interface.ensureImplements
确保严格的功能检查,关于Interface
实现可以看这里:[URL:https://gist.github.com/1057989]
使用接口最大的问题是,js并没提供内置的接口支持,当我们试图模拟其他语言的特性的有可能是不合适的。清理的接口可能不会造成太大的性能开销,我们接下来将看一下使用相同概念的抽象装饰器模式。
抽象装饰器
为了展示这个版本的的装饰模式,我们准备再次想象有个父类模型Macbook
,和一个允许我们付费升级我们Macbook的商店。
升级可以包括,4GB Ram到8GB Ram,雕刻或者其他。如果我们的模型使用独立的子类组合各种可能的升级选项,那么将看起来像这样:
var Macbook = function(){
//...
};
var MacbookWith4GBRam = function(){},
MacbookWith8GBRam = function(){},
MacbookWith4GBRamAndEngraving = function(){},
MacbookWith8GBRamAndEngraving = function(){},
MacbookWith8GBRamAndParallels = function(){},
MacbookWith4GBRamAndParallels = function(){},
MacbookWith8GBRamAndParallelsAndCase = function(){},
MacbookWith4GBRamAndParallelsAndCase = function(){},
MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};
这是一个不切实际的解决方案,作为一个新的子类将需要每一个可能的组合。因为我们喜欢将事情简单化而不是维护一堆的子类,让我们看看装饰器模式如何更好的帮我们解决这个问题。
相比要求我们早期看到的全部组合,我们只需要创建五个装饰器类。在这些加强类上调用的方法将被传递给我们的Macbook类。
在下一个例子中,装饰器透明的包装他们的组件,并且有趣的是可以使用相同的接口互换他们。
这是我们将定义的Macbook接口:
var Macbook = new Interface( "Macbook",
["addEngraving",
"addParallels",
"add4GBRam",
"add8GBRam",
"addCase"]);
// A Macbook Pro might thus be represented as follows:
var MacbookPro = function(){
// implements Macbook
};
MacbookPro.prototype = {
addEngraving: function(){
},
addParallels: function(){
},
add4GBRam: function(){
},
add8GBRam:function(){
},
addCase: function(){
},
getPrice: function(){
// Base price
return 900.00;
}
};
为了使以后我们更容易的添加更多的选项,一个抽象抽象的装饰器被定义,并且需要默认实现Macbook类所定义的接口,剩余的选项讲需要子类。抽象装饰器确保我们可以装饰一个基本类独立于为了不同的组合而存在许多装饰器,不需要为每一可能的组合派生出一个类。
// Macbook decorator abstract decorator class
var MacbookDecorator = function( macbook ){
Interface.ensureImplements( macbook, Macbook );
this.macbook = macbook;
};
MacbookDecorator.prototype = {
addEngraving: function(){
return this.macbook.addEngraving();
},
addParallels: function(){
return this.macbook.addParallels();
},
add4GBRam: function(){
return this.macbook.add4GBRam();
},
add8GBRam:function(){
return this.macbook.add8GBRam();
},
addCase: function(){
return this.macbook.addCase();
},
getPrice: function(){
return this.macbook.getPrice();
}
};
上面的示例中Macbook装饰器接受一个对象作为我们的基本组件,它使用我们之前定义的Macbook接口并且每个方法只需要调用组件上相同的方法。我们现在可以创建我们的选项类,用来装饰Macbook。
// First, define a way to extend an object a
// with the properties in object b. We'll use
// this shortly!
function extend( a, b ){
for( var key in b )
if( b.hasOwnProperty(key) )
a[key] = b[key];
return a;
}
var CaseDecorator = function( macbook ){
this.macbook = macbook;
};
// Let's now extend (decorate) the CaseDecorator
// with a MacbookDecorator
extend( CaseDecorator, MacbookDecorator );
CaseDecorator.prototype.addCase = function(){
return this.macbook.addCase() + "Adding case to macbook";
};
CaseDecorator.prototype.getPrice = function(){
return this.macbook.getPrice() + 45.00;
};
我们在这里重写了需要被装饰的addCase()和getPrice()方法,并且我们实现它是通过首先在原始的Macbook对象中调用它。并简单的加一个字符串或者一个数字值。
到目前为止有大量的信息被展现在本章节。让我们试着用一个简单的案例总结在一起,这样有望于突出我们所学。
// Instantiation of the macbook
var myMacbookPro = new MacbookPro();
// Outputs: 900.00
console.log( myMacbookPro.getPrice() );
// Decorate the macbook
var decoratedMacbookPro = new CaseDecorator( myMacbookPro );
// This will return 945.00
console.log( decoratedMacbookPro.getPrice() );
作为装饰器可以动态的修改对象,这对于修改现存系统是一个非常棒的模式。有时,他只是简单的额创建一个相关的对象,以对抗为每个对象类型维护一堆子类。这使得需要维护一大堆子类对象的应用变得更直接。
本例的功能版本可以在JSBin上找到。
jQuery中的装饰器
正如我们上面所提到的其他模式一样,这里也有一个装饰模式的例子可以通过jQuery实现。jQuery.extend()允许我们在程序运行中扩展(或者合并)两个或者更多个对象到一个对象中。
在这种情况下,一个目标对象可以从原对象/父类,被装饰新的功能而不用打破或者重写现有的方法。
在下面的案例中,我们定义了三个对象:defaults,options 和settings。我们的目标是用在options,settings中的额外功能去装饰默认的对象。我们必须:
a)保留“default”的状态在一种未接触的状态下,即我们不会在之后的时间点失去访问它属性和方法的权限。
b)获得options中用来装饰属性和功能的能力。
var decoratorApp = decoratorApp || {};
// define the objects we're going to use
decoratorApp = {
defaults: {
validate: false,
limit: 5,
name: "foo",
welcome: function () {
console.log( "welcome!" );
}
},
options: {
validate: true,
name: "bar",
helloWorld: function () {
console.log( "hello world" );
}
},
settings: {},
printObj: function ( obj ) {
var arr = [],
next;
$.each( obj, function ( key, val ) {
next = key + ": ";
next += $.isPlainObject(val) ? printObj( val ) : val;
arr.push( next );
} );
return "{ " + arr.join(", ") + " }";
}
};
// merge defaults and options, without modifying defaults explicitly
decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp.options);
// what we have done here is decorated defaults in a way that provides
// access to the properties and functionality it has to offer (as well as
// that of the decorator "options"). defaults itself is left unchanged
$("#log")
.append( decoratorApp.printObj(decoratorApp.settings) +
+ decoratorApp.printObj(decoratorApp.options) +
+ decoratorApp.printObj(decoratorApp.defaults));
// settings -- { validate: true, limit: 5, name: bar, welcome: function (){ console.log( "welcome!" ); },
// helloWorld: function (){ console.log( "hello world" ); } }
// options -- { validate: true, name: bar, helloWorld: function (){ console.log( "hello world" ); } }
// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } }
优点和缺点
开发人员喜欢使用这种模式,因为它可以被透明的使用。并且也十分自由,正如我们看到的那样,对象可以被包裹或者装饰而含有新的行为,继续被使用并且不用担心原来对象被修改。在更管饭的上下文中,这种模式也可以避免我们需要依赖一大子类而获得同样的效用。
当然实现这种模式的时候我们也应该意识到一些缺点。如果管理不善,它可以显著的复杂话我们的应用架构,因为它给我们的命名空间进入了许多小但是相似的对象。除了变得难以管理,其他不熟悉这种模式的开发者可能很难了解为什么这种模式被使用。
充足的交流和模式的研究可以对第二个问题有帮助,然而,我们应该控制我们的应用程序中装饰器的广泛成都,并且统计他们的全部数量。