JavaScript設計模式(8)-裝飾者模式

裝飾者模式

1. 做用:

  • 可用來透明地把對象包裝在具備一樣接口的另外一對象之中,這樣能夠給一個方法添加一些行爲,而後將方法調用傳遞給原始對象。
  • 可用於爲對象增長功能,用來代替大量子類。
  • 裝飾者對其組件進行了透明包裝,兩者能夠互換使用,由於他們 實現了一樣的接口

2. 例子:自行車

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 !== superClass) {
        superClass.prototype.constructor = superClass
    }
}

// var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair', 'getPrice']);

/**
* [AcmeComfortCruiser 自行車類]
*/
var AcmeComfortCruiser = function(){};
AcmeComfortCruiser.prototype = {
    assemble: function() {},
    wash: function() {},
    ride: function() {},
    repair: function() {},
    getPrice: function() {
        return 399.00
    }
}

/**
* [BicycleDecorator 裝飾類的抽象類]
*/
var BicycleDecorator = function(bicycle) {
    // Interface.ensureImplements(bicycle, Bicycle);
    this.bicycle = bicycle;
}
BicycleDecorator.prototype = {
    assemble: function() {
        return this.bicycle.assemble();
    },
    wash: function() {
        return this.bicycle.wash();
    },
    ride: function() {
        return this.bicycle.ride();
    },
    repair: function() {
        return this.bicycle.repair();
    },
    getPrice: function() {
        return this.bicycle.getPrice();
    }
}

/**
* [HeadlightDecorator 裝飾者類]
*/
var HeadlightDecorator = function(bicycle) {
    HeadlightDecorator.superclass.constructor.call(this, bicycle)
}
extend(HeadlightDecorator, BicycleDecorator)
HeadlightDecorator.prototype.assemble = function() {
    return this.bicycle.assemble() + ' Attach headlight to handlebars.';
}
HeadlightDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 15.00;
}

/**
* [TaillightDecorator 裝飾者類]
*/
var TaillightDecorator = function(bicycle) {
    TaillightDecorator.superclass.constructor.call(this, bicycle)
}
extend(TaillightDecorator, BicycleDecorator);
TaillightDecorator.prototype.assemble = function() {
    return this.bicycle.assemble() + ' Attach taillight to the seat post.';
}
TaillightDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 9.00;
}

// usage
var myBicycle = new AcmeComfortCruiser();
console.log(myBicycle.getPrice())

myBicycle = new HeadlightDecorator(myBicycle);
console.log(myBicycle.getPrice())

myBicycle = new TaillightDecorator(myBicycle);
console.log(myBicycle.getPrice())

3. 裝飾者模式與組合模式的比較

  • 相同點:
    • 裝飾者對象和組合對象都是用來包裝別的對象(那些對象組合模式中稱爲子對象,裝飾者模式中稱爲組件)
    • 實現了一樣的接口,而且會把任何調用傳遞給這些對象
  • 差別點:
    • 組織模式:用於組織對象;裝飾者模式:用於在不修改現有對象及不從其派生子類的前提下爲其增添職責。
    • 裝飾模式的子對象只有一個

4. 裝飾者修改其組件的方式

4.1 在方法以後添加行爲

先調用組件方法,並在其放回後實施一些附加行爲javascript

HeadlightDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 15.00;
}

4.2 在方法以前添加行爲

var FrameColorDecorator = function(bicycle, frameColor) {
    FrameColorDecorator.superclass.constructor.call(this, bicycle);

    // 添加了用以實現其提供的附加特性的屬性
    this.frameColor = frameColor;
}
extend(FrameColorDecorator, BicycleDecorator);
FrameColorDecorator.prototype.assemble = function() {
    // 方法添加的步驟出如今其方法以前
    return 'Print the frame ' + this.frameColor + ' and allow it to dry. ' + this.bicycle.assemble()
}
FrameColorDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 30.00;
}

// usage
var myBicycle = new AcmeComfortCruiser();
console.log(myBicycle.assemble())

myBicycle = new FrameColorDecorator(myBicycle, 'red')
console.log(myBicycle.assemble())

4.3 使用替代方法

引入替換組件方法的裝飾者後,必須設法確保按正確的順序應用裝飾者(如:使用工廠方法,ps:參考第 5 大點)java

/**
* [LifetimeWarrantyDecorator 裝飾者類]
*/
var LifetimeWarrantyDecorator = function(bicycle) {
    LifetimeWarrantyDecorator.superclass.constructor.call(this, bicycle);
}
extend(LifetimeWarrantyDecorator, BicycleDecorator);

// 把原來的 repair 方法替換爲一個新方法,而組件的方法則不會再被調用
LifetimeWarrantyDecorator.prototype.repair = function() {
    return 'This bicycle is covered by a lifetime warranty.'
}
LifetimeWarrantyDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 199.00
}

/**
* [TimedWarrantyDecorator 裝飾者類]
*/
var TimedWarrantyDecorator = function(bicycle, coverageLengthInYears) {
    TimedWarrantyDecorator.superclass.constructor.call(this, bicycle);
    this.coverageLength = coverageLengthInYears;
    this.expDate = new Date();
    var coverageLengthInMs = this.coverageLength * 365 * 24 * 60 * 60 * 1000;
    this.expDate.setTime(this.expDate.getTime() + coverageLengthInMs)
}
extend(TimedWarrantyDecorator, BicycleDecorator);

// 根據某種條件決定是否替代組件方法,在條件知足是替代,不然使用組件的方法
TimedWarrantyDecorator.prototype.repair = function() {
    var repairInstructions;
    var currentDate = new Date();
    if(currentDate < this.expDate) {
        repairInstructions = 'This bicycle is currently covered by a warrenty.'
    }else {
        repairInstructions = this.bicycle.repair();
    }
    return repairInstructions;
}
TimedWarrantyDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + (40.00 * this.coverageLength)
}

// usage
var myBicycle = new AcmeComfortCruiser();
console.log(myBicycle.getPrice())

// 替代
myBicycle = new LifetimeWarrantyDecorator(myBicycle)
console.log(myBicycle.repair())

// 判斷是否替代
myBicycle = new TimedWarrantyDecorator(myBicycle, 1)
console.log(myBicycle.getPrice())

4.4 添加新方法

想穩妥地實現這一點並不容易,想要使用這些新方法,外圍代碼必須知道有這些新方法。因爲這些新方法並非在接口中定義的,而是動態添加的,所以有必要進行類型檢查,以驗明用於包裝組件對象的最外層裝飾者與用新方法裝飾的組件對象相同架構

var BellDecorator = function(bicycle) {
    BellDecorator.superclass.constructor.call(this, bicycle);
}
extend(BellDecorator, BicycleDecorator);
BellDecorator.prototype.assemble = function() {
    return this.bicycle.assemble() + 'Attach bell to handlebars.'
}
BellDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 6.00;
}
// 這裏添加了一個新方法
BellDecorator.prototype.ringBell = function() {
    return 'Bell rung'
}

// usage
var myBicycle = new AcmeComfortCruiser();

myBicycle = new BellDecorator(myBicycle)
myBicycle = new HeadlightDecorator(myBicycle);

console.log(myBicycle.ringBell())  // 這樣子會報錯,由於 BellDecorator 添加的 ringBell 方法(及其餘方法)會在 HeadlightDecorator 類經過 extend() 繼承 new F() 時被抹除(也不是被抹除,只是不能在經過當前對象的原型鏈找到,其實這個方法在新對象的 bicycle 屬性裏面仍是能經過其原型鏈找到)。

// BellDecorator 必須放在最後應用,不然這個新方法將沒法訪問。這是由於其餘裝飾者只能傳遞他們知道的方法,也即那些定義在接口中的方法。P170

關於上面說到的方法被抹除問題的解決方案app

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 !== superClass) {
        superClass.prototype.constructor = superClass
    }
}
var Interface = function(name, methods) {
    if(arguments.length !== 2) {
        throw new Error("Interface constructor called with "+ arguments.length + "arguments, but expected exactly 2")
    }
    this.name = name;
    this.methods = [];
    for(var i=0, len=methods.length; i<len; i++) {
        if(typeof methods[i] !== 'string') {
            throw new Error("Interface constructor expects method names to be passed in as a string")
        }
        this.methods.push(methods[i]);
    }
}  

var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair', 'getPrice']);

var BicycleDecorator = function(bicycle) {
    this.bicycle = bicycle;
    this.interface = Bicycle;
    outerloop:   // 使用標記,能夠在程序的任何地方使用這個名字來引用他
    for(var key in this.bicycle) {
        if(typeof this.bicycle[key] !== 'function') {
            continue outerloop;
        }
        for(var i=0, len=this.interface.methods.length; i<len; i++) {
            if(key === this.interface.methods[i]) {
                continue outerloop
            }
        }
        var that = this;
        (function(methodName) {
            that[methodName] = function() {
                return that.bicycle[methodName]();
            }
        })(key);
    }
}
BicycleDecorator.prototype = {
    assemble: function() {
        return this.bicycle.assemble();
    },
    wash: function() {
        return this.bicycle.wash();
    },
    ride: function() {
        return this.bicycle.ride();
    },
    repair: function() {
        return this.bicycle.repair();
    },
    getPrice: function() {
        return this.bicycle.getPrice();
    }
}

/**
* [AcmeComfortCruiser 自行車類]
*/
var AcmeComfortCruiser = function(){};
AcmeComfortCruiser.prototype = {
    assemble: function() {
        return 'assemble:'
    },
    wash: function() {},
    ride: function() {},
    repair: function() {},
    getPrice: function() {
        return 399.00
    }
}

/**
* [HeadlightDecorator 裝飾者類]
*/
var HeadlightDecorator = function(bicycle) {
    HeadlightDecorator.superclass.constructor.call(this, bicycle)
}
extend(HeadlightDecorator, BicycleDecorator)
HeadlightDecorator.prototype.assemble = function() {
    return this.bicycle.assemble() + ' Attach headlight to handlebars.';
}
HeadlightDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 15.00;
}

/**
* [BellDecorator 裝飾者類]
*/
var BellDecorator = function(bicycle) {
    BellDecorator.superclass.constructor.call(this, bicycle);
}
extend(BellDecorator, BicycleDecorator);
BellDecorator.prototype.assemble = function() {
    return this.bicycle.assemble() + 'Attach bell to handlebars.'
}
BellDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 6.00;
}
BellDecorator.prototype.ringBell = function() {
    return 'Bell rung'
}

var myBicycle = new AcmeComfortCruiser();
console.log(myBicycle.getPrice())

myBicycle = new BellDecorator(myBicycle)

myBicycle = new HeadlightDecorator(myBicycle);
console.log(myBicycle.getPrice())

console.log(myBicycle.ringBell())

5. 工廠的角色

能夠使用工廠模式配合裝飾者模式,這樣就能夠事先規定好實例化時的裝飾者應用順序,從而避免上面說到的新添加的方法在通過別的裝飾類包裝後訪問不到添加的方法的問題ide

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 !== superClass) {
        superClass.prototype.constructor = superClass
    }
}

var Interface = function(name, methods) {
    if(arguments.length !== 2) {
        throw new Error("Interface constructor called with "+ arguments.length + "arguments, but expected exactly 2")
    }
    this.name = name;
    this.methods = [];
    for(var i=0, len=methods.length; i<len; i++) {
        if(typeof methods[i] !== 'string') {
            throw new Error("Interface constructor expects method names to be passed in as a string")
        }
        this.methods.push(methods[i]);
    }
}  
Interface.ensureImplements = function(object) {
    if(arguments.length < 2) {
        throw new Error("Function Interface.ensureImplements call with " + arguments.length + "arguments, but expected at least 2")
    }
    for(var i=1,len=arguments.length; i<len; i++) {
        var interface = arguments[i];
        if(interface.constructor !== Interface) {
            throw new Error("Function Interface.ensureImplements expects arguments two and above to be instances of Interface");
        }
        for(var j=0, methodsLen = interface.methods.length; j<methodsLen; j++) {
            var method = interface.methods[j];
            if(!object[method] || typeof object[method] !== 'function') {
            throw new Error('Function Interface.ensureImplements: Object does not implement the '+ interface.name + " interface. Method " + method + " was not found")
            }
        }
    }
}

var Bicycle = new Interface('Bicycle', ['assemble', 'wash', 'ride', 'repair', 'getPrice']);

// model 1
var AcmeComfortCruiser = function(){};
AcmeComfortCruiser.prototype = {
    assemble: function() {},
    wash: function() {},
    ride: function() {},
    repair: function() {},
    getPrice: function() {
        return 399.00
    }
}

// model 2
var AcmeSpeedster = function() {}
extend(AcmeSpeedster, AcmeComfortCruiser)

/**
* [BicycleDecorator 裝飾類的抽象類]
*/
var BicycleDecorator = function(bicycle) {
    // Interface.ensureImplements(bicycle, Bicycle);
    this.bicycle = bicycle;
}
BicycleDecorator.prototype = {
    assemble: function() {
        return this.bicycle.assemble();
    },
    wash: function() {
        return this.bicycle.wash();
    },
    ride: function() {
        return this.bicycle.ride();
    },
    repair: function() {
        return this.bicycle.repair();
    },
    getPrice: function() {
        return this.bicycle.getPrice();
    }
}

/**
* [HeadlightDecorator 裝飾者類]
*/
var HeadlightDecorator = function(bicycle) {
    HeadlightDecorator.superclass.constructor.call(this, bicycle)
}
extend(HeadlightDecorator, BicycleDecorator)
HeadlightDecorator.prototype.assemble = function() {
    return this.bicycle.assemble() + ' Attach headlight to handlebars.';
}
HeadlightDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 15.00;
}

/**
* [BellDecorator 裝飾者類]
*/
var BellDecorator = function(bicycle) {
    BellDecorator.superclass.constructor.call(this, bicycle);
}
extend(BellDecorator, BicycleDecorator);
BellDecorator.prototype.assemble = function() {
    return this.bicycle.assemble() + 'Attach bell to handlebars.'
}
BellDecorator.prototype.getPrice = function() {
    return this.bicycle.getPrice() + 6.00;
}
BellDecorator.prototype.ringBell = function() {
    return 'Bell rung'
}

// BicycleShop class 是一個抽象類,須要在繼承後實現裏面的方法
var BicycleShop = function() {};
BicycleShop.prototype = {
    sellBicycle: function(model) {
        var bicycle = this.createBicycle(model)
        bicycle.assemble()
    return bicycle;
    },
    // 工廠方法
    createBicycle: function(model) {
        throw new Error('Unsupported operation on an abstract class.')
    }
}

var AcmeBicycleShop = function() {};
extend(AcmeBicycleShop, BicycleShop);
AcmeBicycleShop.prototype.createBicycle = function(model, options) {
    var bicycle = new AcmeBicycleShop.models[model]();

    // 有必要時能夠在這裏對裝飾者組件前後應用進行排序,下面使用的是直接遍歷按順序應用

    for(var i=0, len= options.length; i<len; i++) {
        var decorator = AcmeBicycleShop.options[options[i].name]
        if(typeof decorator !== 'function') {
            throw new Error('Decorator ' + options[i].name + 'not found');
        }
        var argument = options[i].arg;
        bicycle = new decorator(bicycle, argument)
    }
    
    Interface.ensureImplements(bicycle, Bicycle);
    return bicycle

}
AcmeBicycleShop.models = {
    'The Speedster' : AcmeSpeedster,
    'The Comfort Cruiser' : AcmeComfortCruiser
}
AcmeBicycleShop.options = {
    'headlight' : HeadlightDecorator,
    'bell': BellDecorator
}

var alecsCruisers = new AcmeBicycleShop();
var myBicycle = alecsCruisers.createBicycle('The Speedster', [
        {name: 'headlight'},
        {name: 'bell'}
    ])

myBicycle.ringBell()

6. 函數裝飾者

用於包裝獨立的函數和方法的裝飾者函數

6.1 包裝函數

// 將傳入函數的執行結果轉化爲大寫形式
function upperCaseDecorator(func) {
    return function() {
        return func.apply(this, arguments).toUpperCase()
    }
}

function getDate() {
    return (new Date()).toString()
}

getDateCaps = upperCaseDecorator(getDate)

// usage
getDateCaps()

6.2 包裝方法

function upperCaseDecorator(func) {
    return function() {
        return func.apply(this, arguments).toUpperCase()
    }
}

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 !== superClass) {
        superClass.prototype.constructor = superClass
    }
}

/**
    * [AcmeComfortCruiser 自行車類]
    */
var AcmeComfortCruiser = function(){};
AcmeComfortCruiser.prototype = {
    assemble: function() {},
    wash: function() {},
    ride: function() {},
    repair: function() {},
    getPrice: function() {
        return 399.00
    }
}

/**
    * [BicycleDecorator 裝飾類的抽象類]
    */
var BicycleDecorator = function(bicycle) {
    this.bicycle = bicycle;
}
BicycleDecorator.prototype = {
    assemble: function() {
        return this.bicycle.assemble();
    },
    wash: function() {
        return this.bicycle.wash();
    },
    ride: function() {
        return this.bicycle.ride();
    },
    repair: function() {
        return this.bicycle.repair();
    },
    getPrice: function() {
        return this.bicycle.getPrice();
    }
}

/**
    * [BellDecorator 裝飾者類]
    */
var BellDecorator = function(bicycle) {
    BellDecorator.superclass.constructor.call(this, bicycle);
}
extend(BellDecorator, BicycleDecorator);

BellDecorator.prototype.ringBell = function() {
    return 'Bell rung'
}

// 使用函數裝飾者裝飾方法
BellDecorator.prototype.ringBellLoudly = upperCaseDecorator(BellDecorator.prototype.ringBell)

var myBicycle = new AcmeComfortCruiser();
myBicycle = new BellDecorator(myBicycle)
myBicycle.ringBell()
myBicycle.ringBellLoudly()

7. 裝飾者模式的適用場合

  • 爲類添加特效或職能,而從該類派生子類又不實際時(不實際多是子類的數量大)
  • 須要爲對象添加特效而又不想改變使用該對象的代碼的話,也可以使用裝飾者模式。由於裝飾者模式能夠動態而透明的修改對象,因此它們很適合於修改現有系統這一任務。

8. 示例:方法性能分析器

/**
* [ListBuilder]
* @param {[type]} parent [description]
*/
var ListBuilder = function(parent) {
    this.parentEl = document.getElementById(parent);
}
ListBuilder.prototype = {
    buildList: function(listLength) {
        var list = document.createElement('ol');
        this.parentEl.appendChild(list);

        for(var i=0; i< listLength; i++) {
            var item = document.createElement('li');
            list.appendChild(item)
        }
    }
}

/**
* [MethodProfiler class]
* @param {[type]} component [description]
*/
var MethodProfiler = function(component) {
    this.component = component;
    this.timers = {};

    for(var key in this.component) {
        if(typeof this.component[key] !== 'function') {
            continue;
        }
        var that = this;
        // 使用匿名函數的做用是保留正確的 methodName 變量值
        (function(methodName) {
            that[methodName] = function() {
                that.startTimer(methodName);
                var returnValue = that.component[methodName].apply(that.component, arguments);
                that.displayTime(methodName, that.getElapsedTime(methodName));
                return returnValue;
            }
        })(key)
    }
}
MethodProfiler.prototype = {
    startTimer: function(methodName) {
        this.timers[methodName] = (new Date()).getTime();
    },
    getElapsedTime: function(methodName) {
        return (new Date()).getTime() - this.timers[methodName];
    },
    displayTime: function(methodName, time) {
        console.log(methodName + ': ' + time + ' ms')
    }
}

// usage
var list = new ListBuilder('feed-readers')
var listp = new MethodProfiler(list)
listp.buildList(500)

9. 裝飾者的利與弊

  • 利:
    • 方便,靈活,透明
    • 不用從新定義對象就能對其進去擴充
  • 弊:
    • 在遇到用裝飾者包裝起來的對象時,那些依賴於類型檢查的代碼會出問題。
    • 使用裝飾者模式每每會增長架構的複雜度。(添加了一些小對象;實現動態接口的裝飾者涉及的語法細節也使人生畏)

注意oop

轉載、引用,但請標明做者和原文地址post

相關文章
相關標籤/搜索