javaScript设计模式-Decorator(装饰者)模式

Decorator(装饰者)模式

Decorator 是一种结构设计模式,旨在促进代码复用。于 Mixin 相类似,它们可以被认为是另一可行对象子类化的替代方案。

通常,Decorator 提供了将行为动态添加至系统的现有类的能力。

装饰者可以用于修改现有的系统,希望在系统中为对象添加额外的功能,而不需要大量修改实用它们的底层代码。

Decorator 模式并不严重依赖于创建对象的方式,而是关注扩展其额外功能。我们实用了一个单一的基本对象并逐步添加提供额外功能的 decorator 对象,而不是仅仅依赖原型继承。这个想法是:向基本对象添加(装饰)属性或方法,而不是进行子类化,因此它较为精简。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 车辆vehicle构造函数
function vehicle(vehicleType) {
// 默认值
this.vehicleType = vehicleType;
this.model = "default";
this.license = "00000-00";
}

// 测试基本的vehicle实例
var testInstance = new vehicle("car");
console.log(testInstance);

// 创建一个vehicle实例进行装饰
var truck = new vehicle("truck");

// 给truck装饰新的功能
truck.setModel = function(modelName) {
this.model = modelName;
};

truck.setColor = function(color) {
this.color = color;
};
// 测试赋值操作是否正常工作
truck.setModel("CAT");
truck.setColor("blue");

console.log(truck);
// 下面的代码,展示vehicle依然是不被改变的
var secondInstance = new vehicle("car");
console.log(secondInstance);

这种类型的简单实现是可行的,但它并不能真正证明装饰者所提供的所有又是。为此,首先要查以下改编的咖啡示例,该示例来自 Freeman、Sierra 和 Bates 所著的一本名《深入浅出设计模式》书籍,它围绕的是模拟购买苹果笔记本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 被装饰的对象构造函数
function MacBook() {
this.cost = function() {
return 100;
};
this.screenSize = function() {
return 11.6;
};
}
// 装饰器1
function Memory(macbook) {
var v = macbook.cost();
macbook.cost = function() {
return v + 75;
};
}

// 装饰器2
function Engraving(mackbook) {
var v = mackbook.cost();

mackbook.cost = function() {
return v + 200;
};
}

// 装饰器3
function Insurance(mackbook) {
var v = mackbook.cost();
mackbook.cost = function() {
return v + 250;
};
}

var mb = new MacBook();
Memory(mb);
console.log(mb.cost()); // 175

Engraving(mb);
console.log(mb.cost()); // 375
Insurance(mb);
console.log(mb.cost()); // 625

console.log(mb.screenSize()); // 11.6

在这个示例中,Decorator 重写 MacBoook()超类对象的 cost()函数返回 MacBook 的当前价格加上特定的升级价格。

我们认为装饰作为并没有重写原始 Macbook 对象的构造函数方法(如 screenSize()),为 Mackbook 定义的其他属性也一样,依然保持不变完好无损。

伪经典 Decorator(装饰者)

接口

《JavaScript 设计模式》(PJDP)将 Decorator 模式描述为一种用于在相同接口的其他对象内部透明地包装对象的模式。接口应该是对象定义方法的一种方式,但是,它实际上并不直接指定如何实现这些方法。

下面是使用鸭子类型在 Javascript 中实现接口的一个示例,这种方法帮助确定一个对象是否基于实现方法的构造函数/对象的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//定义一个静态方法来实现接口与实现类的直接检验
//静态方法不要写出Interface.prototype ,因为这是写到接口的原型链上的
//我们要把静态的函数直接写到类层次上
//定义一个接口类
var Interface = function(name, methods) {
//name:接口名字
if (arguments.length < 2) {
alert("必须是两个参数");
}
this.name = name;
this.methods = []; //定义一个空数组装载函数名
for (var i = 0; i < methods.length; i++) {
if (typeof methods[i] != "string") {
alert("函数名必须是字符串类型");
} else {
this.methods.push(methods[i]);
}
}
};
Interface.ensureImplement = function(object) {
if (arguments.length < 2) {
throw new Error("参数必须不少于2个");
return false;
}
for (var i = 1; i < arguments.length; i++) {
var inter = arguments[i];

//如果是接口就必须是Interface类型
if (inter.constructor != Interface) {
throw new Error("如果是接口类的话,就必须是Interface类型");
}
//判断接口中的方法是否全部实现
//遍历函数集合分析
for (var j = 0; j < inter.methods.length; j++) {
var method = inter.methods[j]; //接口中所有函数
//object[method]传入的函数
//最终是判断传入的函数是否与接口中所用函数匹配
if (!object[method] || typeof object[method] != "function") {
//实现类中必须有方法名字与接口中所用方法名相同
throw new Error("实现类中没有完全实现接口中的所有方法");
}
}
}
};

var reminder = new Interface("List", ["summary", "placeOrder"]);
console.log(reminder);

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";
}
}
};

function Todo(config) {
Interface.ensureImplement(config.actions, reminder);
this.name = config.name;
this.methods = config.actions;
}

var todoItem = new Todo(properties);

console.log(todoItem.methods.summary()); //Remember to buy the milk, we are almost out!
console.log(todoItem.methods.placeOrder()); //Ordering milk from your local grocery store

接口的最大问题是,在 Javascript 中没有为它们提供内置支持,试图模仿可能不太合适的另外一中语言特点是有风险的。可以在不花费大量性能成本的情况下使用享元接口,下面继续看一下使用相同概念的抽象装饰者。

抽象 Decorator(抽象装饰者)

为了演示改版本的 Decorator 模式的结构,假设我们有一个超类,再次模拟 Macbook,以及模拟一个商店允许我们“装饰”苹果笔记本并收取增强功能的额外费用。

增强功能可以包括将内存升级到 8GB、雕刻、Parallels 或外壳。如果为每个增强选项组合使用单个子类来模拟它,看起来可能就是这样的:

1
2
3
4
5
6
7
8
9
var Macbook = function() {
// ...
};

var MacbookWith8GBRamAndParallels = function() {},
MacbookWith8GBRamAndParallelsAndCase = function() {},
MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function() {};

...

这是一个不切实际的方案,这里我们试着使用装饰者来更好解决这个问题。

我们只需要创建五个新的装饰者类,而不是需要之前看到的所有组合。在这些增强类上调用的方法将被传递给 Macbook 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
var Macbook = new Interface("Macbook",[

"addEngraving",
"addParallels",
"add8GBRam",
"addCase",
]);

var MacbookPro = function() {
// 实现 Macbook
};

MacbookPro.prototype = {
addEngraving: function() {
// console.log('addEngraving')
return 'addEngraving';
},
addParallels: function() {
console.log('addParallels');
},
add8GBRam: function() {
console.log('add8GBRam');
},
addCase: function() {
console.log("addCase");
return 'addCase';
},
getPrice: function() {
// 基本价格
return 900.00;
}

};

// Macbook装饰者抽象装饰者类
var MacbookDecorator = function(macbook) {
console.log(mackbook)
Interface.ensureImplement(macbook, Macbook);
this.macbook = macbook;
};

MacbookDecorator.prototype = {
addEngraving: function() {
return this.macbook.addEngraving();
},

addParallels: function() {
return this.macbook.addParallels();
},

add8GBRam: function() {
return this.macbook.add8GBRam();
},

addCase: function() {
return this.macbook.addCase();
},

getPrice: function() {
return this.macbook.getPrice();
}
}
/*
* 上面的示例演示的是:Macbook decorator接受一个对象作为组件。它使用了我们前面定义的Macbook接口,针对每个方法,在组件上会调用相同的方法。我们现在可以仅通过使用MacbookDecorator创建选项类;简单调用超类构造函数,必要时可以重写任何方法。
*/
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;

subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}

var CaseDecorator = function(macbook) {
// this.__proto__.constructor === CaseDecorator
//接下来调用超类的构造函数
CaseDecorator.superclass.constructor(macbook);
};

// 扩展超类
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()方法,首先执行改组件的原有方法,然后加上额外的内容。
*/

// 实例化mackbook
var myMacbookPro = new MacbookPro();
console.log(myMacbookPro.getPrice());

//装饰macbook
myMacbookPro = new CaseDecorator(myMacbookPro);
console.log(myMacbookPro)
console.log(myMacbookPro.getPrice())
console.log(myMacbookPro.addCase())
console.log(myMacbookPro.addEngraving())

使用jQuery的装饰者

与我们涉及到的其他模式一样,也有一些使用jQuery实现的装饰者模式的示例。jQuery.extend()允许我们在运行时或者在随后一个点上动态地将两个或两个以上的对象(和它们的属性)一起扩展(或合并)为一个单一对象。

在这种情况下,一个目标对象可以用新功能来装饰,而不会在源/超类对象中破坏或重写现有的方法

接下来的示例有三个对象:defaults、options和settings。该任务的目的是为了装饰defaults对象,将options中的额外功能附加到defaults上。我们必须首先使defaults保持未接触状态,并且保持稍后可以访问其属性或函数的能力,然后,给defaults赋予使用装饰属性和函数的能力,这些装饰属性和函数是从options里获取来的;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var decoratorApp = decoratorApp || {};

// 定义要使用的对象
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(", ") + '}';
}
};

// 合并defaults和options,没有显示的修改defaults
decoratorApp.settings = $.extend({}, decoratorApp.defaults, decoratorApp.options);

// 这里所做的就是装饰可以访问defaults属性和功能的方式(options也一样),defaults本身未做出改变
$("#log").append(decoratorApp.printObj(decoratorApp.settings) + '<br/>' + decoratorApp.printObj(decoratorApp.options) + '<br/>'+ decoratorApp.printObj(decoratorApp.defaults));

//{ validate: true, limit: 5, name: bar, welcome: function() { console.log('welcome'); }, helloWorld: function() { console.log('hello world'); }}
// { validate: true, name: bar, helloWorld: function() { console.log('hello world'); }}
// { validate: false, limit: 5, name: foo, welcome: function() { console.log('welcome'); }}

优点和缺点

对象可以被新行为包装或“装饰”,然后可以被继续使用,而不必担心被修改的基本对象。在一个更广泛的上下文中,这种模式也使我们不必依靠大量的子类来获得同样的好处。

如果管理不当,它会极大的复杂化应用程序架构,因为它向我们的命名空间引入了很多小型但类似的对象。