鍍金池/ 教程/ HTML/ 裝飾模式
中介者模式
MVVM
亨元模式
設(shè)計(jì)模式分類概覽表
ES Harmony
組合模式
CommonJS
jQuery 插件的設(shè)計(jì)模式
外觀模式
觀察者模式
建造者模式
構(gòu)造器模式
外觀模式
簡介
AMD
原型模式
設(shè)計(jì)模式的分類
觀察者模式
命名空間模式
代理模式
編寫設(shè)計(jì)模式
適配器模式
反模式
什么是設(shè)計(jì)模式
模塊化模式
MVC
Mixin 模式
裝飾模式
設(shè)計(jì)模式的結(jié)構(gòu)
單例模式
迭代器模式
命令模式
工廠模式
MVP
暴露模塊模式
惰性初始模式

裝飾模式

裝飾器是旨在提升重用性能的一種結(jié)構(gòu)性設(shè)計(jì)模式。同Mixin類似,它可以被看作是應(yīng)用子類劃分的另外一種有價(jià)值的可選方案。

典型的裝飾器提供了向一個(gè)系統(tǒng)中現(xiàn)有的類動態(tài)添加行為的能力。其創(chuàng)意是裝飾本身并不關(guān)心類的基礎(chǔ)功能,而只是將它自身拷貝到超類之中。

它們能夠被用來在不需要深度改變使用它們的對象的依賴代碼的前提下,變更我們希望向其中附加功能的現(xiàn)有系統(tǒng)之中。開發(fā)者使用它們的一個(gè)通常的理由是,它們的應(yīng)用程序也許包含了需要大量彼此不相干類型對象的特性。想象一下不得不要去定義上百個(gè)不同對象的構(gòu)造器,比方說,一個(gè)Javascript游戲。

對象構(gòu)造器可以代表不同播放器類型,每一種類型具有不同的功能。一種叫做領(lǐng)主戒指的游戲會需要霍比特人、巫術(shù)師,獸人,巨獸,精靈,山嶺巨人,亂世陸地等對象的構(gòu)造器,而這些的數(shù)量很容易過百。而我們還要考慮為每一個(gè)類型的能力組合創(chuàng)建子類。

例如,帶指環(huán)的霍比特人,帶劍的霍比特人和插滿寶劍的陸地等等。這并不是非常的實(shí)用,當(dāng)我們考慮到不同能力的數(shù)量在不斷增長這一因素時(shí),最后肯定是不可控的。

裝飾器模式并不去深入依賴于對象是如何創(chuàng)建的,而是專注于擴(kuò)展它們的功能這一問題上。不同于只依賴于原型繼承,我們在一個(gè)簡單的基礎(chǔ)對象上面逐步添加能夠提供附加功能的裝飾對象。它的想法是,不同于子類劃分,我們向一個(gè)基礎(chǔ)對象添加(裝飾)屬性或者方法,因此它會是更加輕巧的。

向Javascript中的對象添加新的屬性是一個(gè)非常直接了當(dāng)?shù)倪^程,因此將這一特定牢記于心,一個(gè)非常簡單的裝飾器可以實(shí)現(xiàn)如下:

示例1:帶有新功能的裝飾構(gòu)造器

// 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

這種類型的簡單實(shí)現(xiàn)是實(shí)用的,但它沒有真正展示出裝飾能夠貢獻(xiàn)出來的全部潛能。為這個(gè),我們首先區(qū)分一下我的Coffee示例和Freeman,Sierra和Bates所著Head First Design Patterns這一本優(yōu)秀的書中圍繞Mackbook商店建立的模型,這兩個(gè)之間的不同。

示例2:帶有多個(gè)裝飾器的裝飾對象

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

在上面的示例中,我們的裝飾器重載了超類對象MacBook()的 object.cost()函數(shù),使其返回的Macbook的當(dāng)前價(jià)格加上了被定制后升級的價(jià)格。

這被看做是對原來的Macbook對象構(gòu)造器方法的裝飾,它并沒有將其重寫(例如,screenSize()),我們所定義的Macbook的其它屬性也保持不變,完好無缺。

上面的示例并沒有真正定義什么接口,而且我們也轉(zhuǎn)移了從創(chuàng)造者到接受者移動時(shí)確保一個(gè)對象對應(yīng)一個(gè)接口的責(zé)任。

偽古典裝飾器

我們現(xiàn)在要來試試首見于Dustin Diaz與Ross Harmes合著的Pro Javascript Design Patterns(PJDP)中一種裝飾器的變體。

不像早些時(shí)候的一些實(shí)例,Diaz和Harms堅(jiān)持更加近似于其他編程語言(如Java或者C++)如何使用一種“接口”的概念來實(shí)現(xiàn)裝飾器,我們不久就將對此進(jìn)行詳細(xì)的定義。

注意:裝飾模式的這一特殊變體是提供出來做參考用的。如果發(fā)現(xiàn)它過于復(fù)雜,建議你選擇前面更加簡單的實(shí)現(xiàn)。

接口

PJDP所描述的裝飾器是一種被用于將具備相同接口的對象進(jìn)行透明封裝的對象,這樣一種模式。接口是一種定義一個(gè)對象應(yīng)該具有哪些方法的途徑,然而,它實(shí)際上并不指定那些方法應(yīng)該如何實(shí)現(xiàn)。

它們也可以聲明方法應(yīng)該有些什么參數(shù),但這被看做是可選項(xiàng)。

因此,為什么我們要在Javascript中使用接口呢?這個(gè)想法意在讓它們具有自說明文檔特性,并促進(jìn)其重用性。在理論上,接口通過確保了其被改變的同時(shí)也要讓其對象實(shí)現(xiàn)這些改變,從而使得代碼更加的穩(wěn)定。

下面是一個(gè)在Javascript中使用鴨式類型來實(shí)現(xiàn)接口的示例,鴨式類型是一種基于所實(shí)現(xiàn)的方法來幫助判定一個(gè)對象是否是一種構(gòu)造器/對象的實(shí)體的方法。

// 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 = 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

在上面的代碼中,接口確保了實(shí)現(xiàn)提供嚴(yán)格的功能檢查,而這個(gè)和接口構(gòu)造器的接口代碼能在這里找到。

使用接口最大的問題是,由于這并不是Javascript內(nèi)置的對它們的支持,對我們而言就會存在嘗試去模仿另外一種語言的特性,但看著并不完全合適,這樣一種風(fēng)險(xiǎn)。然而對于沒有太大性能消耗的輕量級接口是可以被使用的,并且下面我們將要看到的抽象裝飾器同樣使用了這個(gè)概念。

抽象裝飾者

為了闡明這個(gè)版本的裝飾者模式的結(jié)構(gòu),我們想象有一個(gè)超級類,還是一個(gè)Macbook模型,以及一個(gè)store,使我們可以用耗費(fèi)額外費(fèi)用的許多種增強(qiáng)來“裝飾”Macbook。

增強(qiáng)可以包括升級到4GB或8GB的Ram,雕刻,或相似案例。如果現(xiàn)在我們要針對每一種增強(qiáng)選項(xiàng)的組合,使用單獨(dú)的子類進(jìn)行建模,可能看起來是這樣的:

var Macbook = function(){
        //...
};

var  MacbookWith4GBRam =  function(){},
     MacbookWith8GBRam = function(){},
     MacbookWith4GBRamAndEngraving = function(){},
     MacbookWith8GBRamAndEngraving = function(){},
     MacbookWith8GBRamAndParallels = function(){},
     MacbookWith4GBRamAndParallels = function(){},
     MacbookWith8GBRamAndParallelsAndCase = function(){},
     MacbookWith4GBRamAndParallelsAndCase = function(){},
     MacbookWith8GBRamAndParallelsAndCaseAndInsurance = function(){},
     MacbookWith4GBRamAndParallelsAndCaseAndInsurance = function(){};

等等。

這不是一個(gè)實(shí)際的解決方案,因?yàn)橐粋€(gè)新的子類可能需要具有每一種可能的增強(qiáng)組合。由于我們傾向于保持事物簡單,不想維持一個(gè)巨大的子類集合,我們來看看怎樣用裝飾者更好的解決這個(gè)問題。

不需要我們前面看到的所有組合,我們只需要簡單的創(chuàng)建五個(gè)新的裝飾者類。對這些增強(qiáng)類的方法調(diào)用,將會傳遞給Macbook類。

在我們下一個(gè)例子中,裝飾者透明的包裝了它們的組件,而且有趣的是,可以在相同的接口互換。

這里是我們給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;        
    }
};

為了使得我們稍后更加容易的添加所需的更多選項(xiàng),一種帶有被用來實(shí)現(xiàn)Mackbook接口的默認(rèn)方法的抽象裝飾器方法被定義了出來,其剩余的選項(xiàng)將會進(jìn)行子類劃分。抽象裝飾器確保了我們能夠獨(dú)立于盡可能多的在不同的組合中所需的裝飾器,去裝飾一個(gè)基礎(chǔ)類(記得早先的那個(gè)示例么?),而不需要去為了每一種可能的組合而去驅(qū)動一個(gè)類。

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

上述示例中所發(fā)生的是Macbook裝飾器在像組件一樣的使用一個(gè)對象。它使用了我們早先定義的Macbook接口,對于每一個(gè)方法都調(diào)用了組件上相同的方法。我們現(xiàn)在就能夠只使用Macbook裝飾器來創(chuàng)建我們的選項(xiàng)類了——通過簡單調(diào)用超類的構(gòu)造器和根據(jù)需要可以被重載的方法。

var CaseDecorator = function( macbook ){

   // call the superclass's constructor next
   this.superclass.constructor( macbook );  

};

// Let's now extend the superclass
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; 
};

如我們所見,大多數(shù)都是相對應(yīng)的直接實(shí)現(xiàn)。我們所做的是重載需要被裝飾的addCase()和getPrise()方法,而我們通過首先執(zhí)行組件的方法然后將其添加到它里面,來達(dá)到目的。

鑒于到目前為止本節(jié)所介紹的信息一斤相當(dāng)?shù)亩嗔耍屛覀冊囋噷⑵淙糠诺揭粋€(gè)單獨(dú)的實(shí)例中,以期突出我們所學(xué)。

// Instantiation of the macbook
var myMacbookPro = new MacbookPro(); 

// Outputs: 900.00
console.log( myMacbookPro.getPrice() );

// Decorate the macbook
myMacbookPro = new CaseDecorator( myMacbookPro );

// This will return 945.00
console.log( myMacbookPro.getPrice() );

由于裝飾器能夠動態(tài)的修改對象,它們就是改變現(xiàn)有系統(tǒng)的理想模式。有時(shí)候,它只是簡單的圍繞一個(gè)對象及其維護(hù)針對每一個(gè)對象類型單獨(dú)的子類劃分所產(chǎn)生的麻煩,來創(chuàng)建裝飾器的。這使得維護(hù)起可能需要大量子類劃分對象的應(yīng)用程序來更加顯著的直接。

裝飾器和jQuery

同我們所涵蓋的其它模式一起,也有許多裝飾器模式的示例能夠使用jQuery來實(shí)現(xiàn)。jQuery.extend()允許我們將兩個(gè)或者更多個(gè)對象(以及它們的屬性)擴(kuò)展(或者混合)到一個(gè)對象中,不論是在運(yùn)行時(shí)或者動態(tài)的在一個(gè)稍后的時(shí)點(diǎn)上。

在這一場景中,目標(biāo)對象沒必要打斷或者重載源/超類中現(xiàn)有的方法(盡管這可以被做到)就能夠使用新的功能裝飾起來。 在接下來的示例中,我們定義了三個(gè)對象:默認(rèn),選項(xiàng)和設(shè)置。任務(wù)的目標(biāo)是用在選項(xiàng)中找到的附加功能來裝飾默認(rèn)對象。

  • 將“默認(rèn)”放置在一個(gè)不可觸及的狀態(tài)之中,在這里我們不會失去訪問稍后會在其中發(fā)現(xiàn)的屬性和方法的能力
  • 贏得了使用在“選項(xiàng)”中找到被裝飾起來的屬性和函數(shù)的能力。
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!"); } }
// options -- { validate: true, name: bar, helloWorld: function (){ console.log("hello!"); } }
// defaults -- { validate: false, limit: 5, name: foo, welcome: function (){ console.log("welcome!"); } }

優(yōu)點(diǎn) & 缺點(diǎn)

因?yàn)樗梢员煌该鞯氖褂?,并且也相?dāng)?shù)撵`活,因此開發(fā)者都挺樂意去使用這個(gè)模式——如我們所見,對象可以用新的行為封裝或者“裝飾”起來,而后繼續(xù)使用,并不用去擔(dān)心基礎(chǔ)的對象被改變。在一個(gè)更加廣泛的范圍內(nèi),這一模式也避免了我們?nèi)ヒ蕾嚧罅孔宇悂韺?shí)現(xiàn)同樣的效果。

然而在實(shí)現(xiàn)這個(gè)模式時(shí),也存在我們應(yīng)該意識到的缺點(diǎn)。如果窮于管理,它也會由于引入了許多微小但是相似的對象到我們的命名空間中,從而顯著的使得我們的應(yīng)用程序架構(gòu)變得復(fù)雜起來。這里所擔(dān)憂的是,除了漸漸變得難于管理,其他不能熟練使用這個(gè)模式的開發(fā)者也可能會有一段要掌握它被使用的理由的艱難時(shí)期。

足夠的注釋或者對模式的研究,對此應(yīng)該有助益,而只要我們對在我們的應(yīng)程序中的多大范圍內(nèi)使用這一模式有所掌控的話,我們就能讓兩方面都得到改善。

上一篇:惰性初始模式下一篇:命令模式