##一。最簡單的裝飾者實現
JS 在動態擴展方面有着天生的優勢,很容易就能實現裝飾者:
// 初始類型
function Dog() {
console.log('I am a dog');
}
// 裝飾類型
function CanRun(dog) {
dog.run = function() {
console.log('I can run');
}
return dog;
}
function CanWalk(dog) {
dog.walk = function() {
console.log('I can walk');
}
return dog;
}
function CanBark(dog) {
dog.bark = function() {
console.log('I can bark');
}
return dog;
}
// ...
// test
var dog = new Dog();
CanWalk(CanBark(CanRun(dog))); // 通過「包裹」擴展 dog 的功能
dog.run();
dog.bark();
dog.walk();
簡單是足夠簡單了,但存在一些問題:
- 好像不需要用裝飾者模式吧,直接把功能全放進
Dog裡不就好了嗎?
從上面的例子看確實是這樣的,但如果 Dog 是我們無法直接修改的第三方組件呢,這時通過裝飾者模式來擴展功能就很合適了。從這個角度看,裝飾者模式和外觀模式很相像,唯一的差別是目的不同,前者是為了擴展新功能,後者追求現有接口易用
- 哪些功能應該作為裝飾類型存在?
基礎的、必要的功能應該是 Dog 的組成部分,可選的、額外的、不常用的功能應該有裝飾類型提供
- 裝飾者不小心重寫了原有的屬性怎麼辦?
確實存在屬性被重寫的風險,因為我們沒有做任何類型上的約束,各個裝飾者之間也是相對獨立的,還有可能覆蓋掉其他裝飾者添上的屬性,我們需要更可靠的(後文介紹的)裝飾者實現來避免這些風險
##二。偽經典裝飾者
JS 沒有提供 Interface 支持,我們無法通過接口來約束類型,提高其可靠性,但我們可以自己實現 Interface 來約束類型,簡單的 Interface 可能是這樣的:
function Interface(strName, arrMethodNames) {
this.name = strName;
this.strMethods = arrMethodNames;
}
Interface.ensureImplements = function(obj, interface) {
for(var i = 0; i < interface.strMethods.length; i++) {
if (!(interface.strMethods[i] in obj)) {
throw new TypeError('Interface.ensureImplements: no ' + interface.strMethods[i] + '\'s here');
}
}
}
利用自定義的 Interface 來實現類型約束,裝飾者模式可以變成這樣:
// 作用相當於裝飾對象
var spec = {
attr: 'value',
actions: {
fun1: function() {
console.log('fun1');
},
fun2: function() {
console.log('fun2');
}
}
}
var myInterface = new Interface('myInterface', ['fun1', 'fun2']);
// 構造函數
function MyObject(spec) {
// 接口檢查
Interface.ensureImplements(spec.actions, myInterface);
this.attr = spec.attr;
this.methods = spec.actions;
}
// test
var obj = new MyObject(spec);
obj.methods.fun1();
obj.methods.fun2();
雖然利用接口實現了類型約束,但結構不夠清晰,不便於管理,最易於管理的當然是層級結構,也就是下面抽象裝飾者中的繼承機制
##三。抽象裝飾者
把可選功能先定義在抽象裝飾者類中,但不提供實現,由具體裝飾者子類提供實現,並利用接口實現類型約束,示例代碼如下:
// 定義接口
var iCoffee = new Interface('coffee', ['addMilk', 'addSalt', 'addSugar']);
// 定義基類
function Coffee() {
console.log('make a cup of coffee');
}
Coffee.prototype = {
addMilk: function() {},
addSalt: function() {},
addSugar: function() {},
getPrice: function() {
// 原味價格
return 30;
}
}
// 定義抽象裝飾者類
function CoffeeDecorator(coffee) {
Interface.ensureImplements(coffee, iCoffee);
this.coffee = coffee;
}
CoffeeDecorator.prototype = {
addMilk: function() {
return this.coffee.addMilk();
},
addSalt: function() {
return this.coffee.addSalt();
},
addSugar: function() {
return this.coffee.addSugar();
},
getPrice: function() {
return this.coffee.getPrice();
}
}
// 裝飾者子類
function MilkDecorator(coffee) {
// 調用父類構造函數
this.superType(coffee);
}
// 定義繼承
function extend(subType, superType) {
var F = function() {};
F.prototype = superType.prototype;
subType.prototype = new F(); // 繼承原型屬性
subType.prototype.superType = superType;
console.log(subType.prototype.superType);
}
extend(MilkDecorator, CoffeeDecorator); // 繼承
// 重寫父類方法(擴展)
MilkDecorator.prototype.addMilk = function() {
console.log('add some milk');
}
MilkDecorator.prototype.getPrice = function() {
return this.coffee.getPrice() + 8;
}
// ...定義其它裝飾者子類
// test
var coffee = new Coffee();
console.log(coffee.getPrice()); // 30
coffee = new MilkDecorator(coffee);
console.log(coffee.getPrice()); // 38
這種實現方式的優點是結構清晰,缺點是複雜度增加了,手動模擬語言本身沒有提供的特性可能有潛在的風險,我們模擬了接口和繼承機制,可能埋下其它隱患
##四。jQuery 提供的裝飾者機制
嗯,又是 $.extend(),[Mixin 模式_JavaScript 設計模式 10](/articles/mixin 模式-javascript 設計模式 10/) 中說 $.extend() 提供了 Mixin 模式的實現,這裡又說提供了裝飾者模式的實現,並不衝突,因為這兩種模式的目的都是擴展現有組件的功能,嚴格地說,jQuery 的 extend 更像 Mixin 模式(合併幾個組件產生新組件,如果說這種機制算裝飾者模式,也勉強說得過去)
關於 jQuery 的 extend 的更多信息請查看:jQuery.extend 函數詳解
##五。裝飾者模式的優缺點
###優點
-
分離了基礎功能和可選功能(裝飾功能)
-
可以動態擴展對象的功能,而不會意外修改基本對象
###缺點
-
引入了大量小類型,可能會引起命名空間混亂
-
管理不當會使應用的結構更複雜,尤其是基於繼承機制的實現,深層繼承會極大地降低可讀性
###參考資料
- 《JavaScript 設計模式》
暫無評論,快來發表你的看法吧