跳到主要內容
黯羽輕揚每天積累一點點

裝飾者模式_JavaScript 設計模式 11

免費2015-08-12#JS#Design_Pattern#JavaScript装饰者模式#伪经典装饰者#抽象装饰者

裝飾者模式的核心是「包裹」,提供多個可選的裝飾類型,給初始類型對象包裹需要的裝飾類型即可擴展初始類型的功能,避免出現類型大爆炸。本文詳細介紹 JS 實現的裝飾者(Decorator)模式

##一。最簡單的裝飾者實現

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();

簡單是足夠簡單了,但存在一些問題:

  1. 好像不需要用裝飾者模式吧,直接把功能全放進 Dog 裡不就好了嗎?

從上面的例子看確實是這樣的,但如果 Dog 是我們無法直接修改的第三方組件呢,這時通過裝飾者模式來擴展功能就很合適了。從這個角度看,裝飾者模式和外觀模式很相像,唯一的差別是目的不同,前者是為了擴展新功能,後者追求現有接口易用

  1. 哪些功能應該作為裝飾類型存在?

基礎的、必要的功能應該是 Dog 的組成部分,可選的、額外的、不常用的功能應該有裝飾類型提供

  1. 裝飾者不小心重寫了原有的屬性怎麼辦?

確實存在屬性被重寫的風險,因為我們沒有做任何類型上的約束,各個裝飾者之間也是相對獨立的,還有可能覆蓋掉其他裝飾者添上的屬性,我們需要更可靠的(後文介紹的)裝飾者實現來避免這些風險

##二。偽經典裝飾者

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 模式的實現,這裡又說提供了裝飾者模式的實現,並不衝突,因為這兩種模式的目的都是擴展現有組件的功能,嚴格地說,jQueryextend 更像 Mixin 模式(合併幾個組件產生新組件,如果說這種機制算裝飾者模式,也勉強說得過去)

關於 jQueryextend 的更多信息請查看:jQuery.extend 函數詳解

##五。裝飾者模式的優缺點

###優點

  1. 分離了基礎功能和可選功能(裝飾功能)

  2. 可以動態擴展對象的功能,而不會意外修改基本對象

###缺點

  1. 引入了大量小類型,可能會引起命名空間混亂

  2. 管理不當會使應用的結構更複雜,尤其是基於繼承機制的實現,深層繼承會極大地降低可讀性

###參考資料

  • 《JavaScript 設計模式》

評論

暫無評論,快來發表你的看法吧

提交評論