1. 程式人生 > >javascript設計模式——裝飾者模式

javascript設計模式——裝飾者模式

應用 提交表單 不同 ora fin input 是否為空 插件 和數

前面的話

  在程序開發中,許多時候都並不希望某個類天生就非常龐大,一次性包含許多職責。那麽可以使用裝飾者模式。裝飾者模式可以動態地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其他對象。本文將詳細介紹裝飾者模式

概念

  在傳統的面向對象語言中,給對象添加功能常常使用繼承的方式,但是繼承的方式並不靈活,還會帶來許多問題:一方面會導致超類和子類之間存在強耦合性,當超類改變時,子類也會隨之改變;另一方面,繼承這種功能復用方式通常被稱為“白箱復用”,“白箱”是相對可見性而言的,在繼承方式中,超類的內部細節是對子類可見的,繼承常常被認為破壞了封裝性

  使用繼承還會帶來另外一個問題,在完成一些功能復用的同時,有可能創建出大量的子類,使子類的數量呈爆炸性增長。比如現在有4種型號的自行車,為每種自行車都定義了一個單獨的類。現在要給每種自行車都裝上前燈、尾燈和鈴鐺這3種配件。如果使用繼承的方式來給每種自行車創建子類,則需要4×3=12個子類。但是如果把前燈、尾燈、鈴鐺這些對象動態組合到自行車上面,則只需要額外增加3個類

  這種給對象動態地增加職責的方式稱為裝飾者(decorator)模式。裝飾者模式能夠在不改變對象自身的基礎上,在程序運行期間給對象動態地添加職責。跟繼承相比,裝飾者是一種更輕便靈活的做法,這是一種“即用即付”的方式,比如天冷了就多穿一件外套,需要飛行時就在頭上插一支竹蜻蜓

  作為一門解釋執行的語言,給javascript中的對象動態添加或者改變職責是一件再簡單不過的事情,雖然這種做法改動了對象自身,跟傳統定義中的裝飾者模式並不一樣,但這無疑更符合javascript的語言特色。代碼如下:

var obj ={
  name:match,
  address:北京
};
obj.address= obj.address + 平谷區;

  傳統面向對象語言中的裝飾者模式在javascript中適用的場景並不多,如上面代碼所示,通常並不太介意改動對象自身

  假設在編寫一個飛機大戰的遊戲,隨著經驗值的增加,操作的飛機對象可以升級成更厲害的飛機,一開始這些飛機只能發射普通的子彈,升到第二級時可以發射導彈,升到第三級時可以發射原子彈

  下面來看代碼實現,首先是原始的飛機類:

var Plane = function(){};

Plane.prototype.fire = function(){
    console.log( 發射普通子彈 );
}

  接下來增加兩個裝飾類,分別是導彈和原子彈:

var MissileDecorator = function( plane ){
    this.plane = plane;
}
MissileDecorator.prototype.fire = function(){
    this.plane.fire();
    console.log( 發射導彈 );
}
var AtomDecorator = function( plane ){
    this.plane = plane;
}
AtomDecorator.prototype.fire = function(){
    this.plane.fire();
    console.log( 發射原子彈 );
}

  導彈類和原子彈類的構造函數都接受參數plane對象,並且保存好這個參數,在它們的fire方法中,除了執行自身的操作之外,還調用plane對象的fire方法。這種給對象動態增加職責的方式,並沒有真正地改動對象自身,而是將對象放入另一個對象之中,這些對象以一條鏈的方式進行引用,形成一個聚合對象。這些對象都擁有相同的接口(fire方法),當請求達到鏈中的某個對象時,這個對象會執行自身的操作,隨後把請求轉發給鏈中的下一個對象

  因為裝飾者對象和它所裝飾的對象擁有一致的接口,所以它們對使用該對象的客戶來說是透明的,被裝飾的對象也並不需要了解它曾經被裝飾過,這種透明性使得可以遞歸地嵌套任意多個裝飾者對象

  在《設計模式》成書之前,GoF原想把裝飾者(decorator)模式稱為包裝器(wrapper)模式。從功能上而言,decorator能很好地描述這個模式,但從結構上看,wrapper的說法更加貼切。裝飾者模式將一個對象嵌入另一個對象之中,實際上相當於這個對象被另一個對象包裝起來,形成一條包裝鏈。請求隨著這條鏈依次傳遞到所有的對象,每個對象都有處理這條請求的機會

javascript裝飾者

  javascript語言動態改變對象相當容易,可以直接改寫對象或者對象的某個方法,並不需要使用“類”來實現裝飾者模式

var plane = {
    fire: function(){
        console.log( 發射普通子彈 );
    }
}
var missileDecorator = function(){
    console.log( 發射導彈 );
}
var atomDecorator = function(){
    console.log( 發射原子彈 );
}
var fire1 = plane.fire;
plane.fire = function(){
    fire1();
    missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function(){
    fire2();
    atomDecorator();
}
plane.fire();
// 分別輸出: 發射普通子彈、發射導彈、發射原子彈

裝飾函數

  在javascript中可以很方便地給某個對象擴展屬性和方法,但卻很難在不改動某個函數源代碼的情況下,給該函數添加一些額外的功能。在代碼的運行期間,很難切入某個函數的執行環境。要想為函數添加一些功能,最簡單粗暴的方式就是直接改寫該函數,但這是最差的辦法,直接違反了開放——封閉原則

var a = function(){
  alert(1);
}
//改成:
var a = function(){
  alert(1);
  alert(2);
}

  很多時候不想去碰原函數,也許原函數是由其他同事編寫的,裏面的實現非常雜亂。現在需要一個辦法,在不改變函數源代碼的情況下,能給函數增加功能,通過保存原引用的方式就可以改寫某個函數:

var a =  function(){
  alert(1);
}
var _a = a;

a = function(){
  _a();
  alert(2);
}
a();

  這是實際開發中很常見的一種做法,比如想給window綁定onload事件,但是又不確定這個事件是不是已經被其他人綁定過,為了避免覆蓋掉之前的window.onload函數中的行為,一般都會先保存好原先的window.onload,把它放入新的window.onload裏執行:

window.onload=function(){
  alert(1);
}
var _onload=window.onload||function(){};
window.onload=function(){
  _onload();
  alert(2);
}

  這樣的代碼當然是符合開放——封閉原則的,在增加新功能的時候,確實沒有修改原來的window.onload代碼,但是這種方式存在以下兩個問題

  1、必須維護_onload這個中間變量,雖然看起來並不起眼,但如果函數的裝飾鏈較長,或者需要裝飾的函數變多,這些中間變量的數量也會越來越多

  2、遇到了this被劫持的問題,在window.onload的例子中沒有這個煩惱,是因為調用普通函數_onload時,this也指向window,跟調用window.onload時一樣(函數作為對象的方法被調用時,this指向該對象,所以此處this也只指向window)。現在把window.onload換成document.getElementById,代碼如下:

var _getElementById = document.getElementById;
document.getElementById= function(id){
  alert(1);
  return _getElementById(id);    //(1)
}
var button = document.getElementById(button);

  執行這段代碼,看到在彈出alert(1)之後,緊接著控制臺拋出了異常:

//輸出:Uncaught TypeError:Illegal invocation

  異常發生在(1)處的_getElementById(id)這句代碼上,此時_getElementById是一個全局函數,當調用一個全局函數時,this是指向window的,而document.getElementById方法的內部實現需要使用this引用,this在這個方法內預期是指向document,而不是window,這是錯誤發生的原因,所以使用現在的方式給函數增加功能並不保險

  改進後的代碼可以滿足需求,要手動把document當作上下文this傳入_getElementById:

<button id="button"></button>
<script>
var _getElementById = document.getElementById;
document.getElementById=function(){
  alert(1);
  return _getElementById.apply(document,arguments);
}
var button = document.getElementById(button);
</script>

  但這樣做顯然很不方便

AOP

  下面使用AOP來提供一種完美的方法給函數動態增加功能

  首先給出Function.prototype.before方法和Function.prototype.after方法:

Function.prototype.before = function( beforefn ){
    var __self = this; // 保存原函數的引用
    return function(){ // 返回包含了原函數和新函數的"代理"函數
        beforefn.apply( this, arguments ); // 執行新函數,且保證this 不被劫持,新函數接受的參數
    // 也會被原封不動地傳入原函數,新函數在原函數之前執行
        return __self.apply( this, arguments ); // 執行原函數並返回原函數的執行結果,
    // 並且保證this 不被劫持
    }
}
Function.prototype.after = function( afterfn ){
    var __self = this;
    return function(){
        var ret = __self.apply( this, arguments );
        afterfn.apply( this, arguments );
        return ret;
    }
};

  Function.prototype.before接受一個函數當作參數,這個函數即為新添加的函數,它裝載了新添加的功能代碼。接下來把當前的this保存起來,這個this指向原函數,然後返回一個“代理”函數,這個“代理”函數只是結構上像代理而已,並不承擔代理的職責(比如控制對象的訪問等)。它的工作是把請求分別轉發給新添加的函數和原函數,且負責保證它們的執行順序,讓新添加的函數在原函數之前執行(前置裝飾),這樣就實現了動態裝飾的效果。通過Function.prototype.apply來動態傳入正確的this,保證了函數在被裝飾之後,this不會被劫持。Function.prototype.after的原理跟Function.prototype.before一模一樣,唯一不同的地方在於讓新添加的函數在原函數執行之後再執行

  下面是一個例子

<button id="button"></button>
<script>
    Function.prototype.before = function( beforefn ){
        var __self = this;
        return function(){
            beforefn.apply( this, arguments );
            return __self.apply( this, arguments );
        }
    }
    document.getElementById = document.getElementById.before(function(){
        alert (1);
    });
    var button = document.getElementById( button );
    console.log( button );
</script>

  再回到window.onload的例子,用Function.prototype.before來增加新的window.onload事件非常簡單

window.onload = function(){
    alert (1);
}
window.onload = ( window.onload || function(){} ).after(function(){
    alert (2);
}).after(function(){
    alert (3);
}).after(function(){
    alert (4);
});

  值得提到的是,上面的AOP實現是在Function.prototype上添加before和after方法,但許多人不喜歡這種汙染原型的方式,那麽可以做一些變通,把原函數和新函數都作為參數傳入before或者after方法:

var before = function( fn, beforefn ){
    return function(){
        beforefn.apply( this, arguments );
        return fn.apply( this, arguments );
    }
}
var a = before(
    function(){alert (3)},
    function(){alert (2)}
    );
a = before( a, function(){alert (1);} );
a();

AOP應用實例

  用AOP裝飾函數的技巧在實際開發中非常有用。不論是業務代碼的編寫,還是在框架層面,都可以把行為依照職責分成粒度更細的函數,隨後通過裝飾把它們合並到一起,這有助於編寫一個松耦合和高復用性的系統

【數據統計上報】

  分離業務代碼和數據統計代碼,無論在什麽語言中,都是AOP的經典應用之一。在項目開發的結尾階段難免要加上很多統計數據的代碼,這些過程可能讓我們被迫改動早已封裝好的函數。比如頁面中有一個登錄button,點擊這個button會彈出登錄浮層,與此同時要進行數據上報,來統計有多少用戶點擊了這個登錄button

<html>
<button tag="login" id="button">點擊打開登錄浮層</button>
<script>
    var showLogin = function(){
        console.log( 打開登錄浮層 );
        log( this.getAttribute( tag ) );
    }
    var log = function( tag ){
        console.log( 上報標簽為:  + tag );
// (new Image).src = ‘http://xx.com/report?tag=‘ + tag; // 真正的上報代碼略
}
document.getElementById( button ).onclick = showLogin;
</script>
</html>

  在showLogin函數裏,既要負責打開登錄浮層,又要負責數據上報,這是兩個層面的功能,在此處卻被耦合在一個函數裏。使用AOP分離之後,代碼如下:

<html>
<button tag="login" id="button">點擊打開登錄浮層</button>
<script>
    Function.prototype.after = function( afterfn ){
        var __self = this;
        return function(){
            var ret = __self.apply( this, arguments );
            afterfn.apply( this, arguments );
            return ret;
        }
    };
    var showLogin = function(){
        console.log( 打開登錄浮層 );
    }
    var log = function(){
        console.log( 上報標簽為:  + this.getAttribute( tag ) );
    }

    showLogin = showLogin.after( log ); // 打開登錄浮層之後上報數據
    document.getElementById( button ).onclick = showLogin;
</script>
</html>

【用AOP動態改變函數的參數】

  觀察Function.prototype.before方法:

Function.prototype.before=function(beforefn){
  var self = this;
  return function(){
    beforefn.apply(this,arguments);    //(1)
    return __self.apply(this,arguments);    //(2)
  }
}

  從這段代碼的(1)處和(2)處可以看到,beforefn和原函數__self共用一組參數列表arguments,在beforefn的函數體內改變arguments時,原函數__self接收的參數列表自然也會變化

  下面的例子展示了如何通過Function.prototype.before方法給函數func的參數param動態地添加屬性b:

var func = function(param){
  console.log(param);    //輸出:{a:"a",b:"b"}
}

func = func.before(
  function(param){
    param.b=b;
});

func({a:a});

  現在有一個用於發起ajax請求的函數,這個函數負責項目中所有的ajax異步請求:

var ajax =f unction(type,url,param){
  console.dir(param);
  //發送ajax請求的代碼略
};
ajax(get,http://xx.com/userinfo,{name:match});

  上面的偽代碼表示向後臺cgi發起一個請求來獲取用戶信息,傳遞給cgi的參數是{name:‘match‘}。ajax函數在項目中一直運轉良好,跟cgi的合作也很愉快。直到有一天,網站遭受了CSRF攻擊。解決CSRF攻擊最簡單的一個辦法就是在HTTP請求中帶上一個Token參數。假設已經有一個用於生成Token的函數:

var getToken = function(){
  returnToken;
}

  現在的任務是給每個ajax請求都加上Token參數:

var ajax = function(type,url,param){
  param=param||{};
  Param.Token=getToken();    //發送ajax請求的代碼略...
};

  雖然已經解決了問題,但ajax函數相對變得僵硬了,每個從ajax函數裏發出的請求都自動帶上了Token參數,雖然在現在的項目中沒有什麽問題,但如果將來把這個函數移植到其他項目上,或者把它放到一個開源庫中供其他人使用,Token參數都將是多余的。也許另一個項目不需要驗證Token,或者是Token的生成方式不同,無論是哪種情況,都必須重新修改ajax函數

  為了解決這個問題,先把ajax函數還原成一個幹凈的函數:

var ajax = function(type,url,param){
  console.log(param);    //發送ajax請求的代碼略
};

  然後把Token參數通過Function.prototyte.before裝飾到ajax函數的參數param對象中:

var getToken =function(){
  returnToken;
}
ajax=ajax.before(function(type,url,param){
  param.Token=getToken();
});

ajax(get,http://xx.com/userinfo,{name:match});

  從ajax函數打印的log可以看到,Token參數已經被附加到了ajax請求的參數中:

{name:"match",Token:"Token"}

  明顯可以看到,用AOP的方式給ajax函數動態裝飾上Token參數,保證了ajax函數是一個相對純凈的函數,提高了ajax函數的可復用性,它在被遷往其他項目的時候,不需要做任何修改

【插件式表單驗證】

  在一個Web項目中,可能存在非常多的表單,如註冊、登錄、修改用戶信息等。在表單數據提交給後臺之前,常常要做一些校驗,比如登錄的時候需要驗證用戶名和密碼是否為空,代碼如下:

<body>
    用戶名:<input id="username" type="text"/>
    密碼: <input id="password" type="password"/>
    <input id="submitBtn" type="button" value="提交"></button>
<script>
    var username = document.getElementById( username ),
    password = document.getElementById( password ),
    submitBtn = document.getElementById( submitBtn );
    var formSubmit = function(){
        if ( username.value === ‘‘ ){
            return alert ( 用戶名不能為空 );
        }
        if ( password.value === ‘‘ ){
            return alert ( 密碼不能為空 );
        }
        var param = {
            username: username.value,
            password: password.value
        }
        ajax( http://xx.com/login, param ); // ajax 具體實現略
    }
    submitBtn.onclick = function(){
        formSubmit();
    }
</script>
</body>

  formSubmit函數在此處承擔了兩個職責,除了提交ajax請求之外,還要驗證用戶輸入的合法性。這種代碼一來會造成函數臃腫,職責混亂,二來談不上任何可復用性。下面來分離校驗輸入和提交ajax請求的代碼,把校驗輸入的邏輯放到validata函數中,並且約定當validata函數返回false的時候,表示校驗未通過,代碼如下:

var validata = function(){
    if ( username.value === ‘‘ ){
        alert ( 用戶名不能為空 );
        return false;
    }
    if ( password.value === ‘‘ ){
        alert ( 密碼不能為空 );
        return false;
    }
}

var formSubmit = function(){
    if ( validata() === false ){ // 校驗未通過
        return;
    }
    var param = {
        username: username.value,
        password: password.value
    }
    ajax( http:// xxx.com/login, param );
}

submitBtn.onclick = function(){
    formSubmit();
}

  現在的代碼已經有了一些改進,把校驗的邏輯都放到了validata函數中,但formSubmit函數的內部還要計算validata函數的返回值,因為返回值的結果表明了是否通過校驗。接下來進一步優化這段代碼,使validata和formSubmit完全分離開來。首先要改寫Function.prototype.before,如果beforefn的執行結果返回false,表示不再執行後面的原函數,代碼如下:

Function.prototype.before = function( beforefn ){
    var __self = this;
    return function(){
        if ( beforefn.apply( this, arguments ) === false ){
        // beforefn 返回false 的情況直接return,不再執行後面的原函數
            return;
        }
        return __self.apply( this, arguments );
    }
}

var validata = function(){
    if ( username.value === ‘‘ ){
        alert ( 用戶名不能為空 );
        return false;
    }
    if ( password.value === ‘‘ ){
        alert ( 密碼不能為空 );
        return false;
    }
}
var formSubmit = function(){
    var param = {
        username: username.value,
        password: password.value
    }
    ajax( http://xx.com/login, param );
}

formSubmit = formSubmit.before( validata );

submitBtn.onclick = function(){
    formSubmit();
}

  在這段代碼中,校驗輸入和提交表單的代碼完全分離開來,它們不再有任何耦合關系,formSubmit=formSubmit.before(validata)這句代碼,如同把校驗規則動態接在formSubmit函數之前,validata成為一個即插即用的函數,它甚至可以被寫成配置文件的形式,這有利於分開維護這兩個函數。再利用策略模式稍加改造,就可以把這些校驗規則都寫成插件的形式,用在不同的項目當中

  值得註意的是,因為函數通過Function.prototype.before或者Function.prototype.after被裝飾之後,返回的實際上是一個新的函數,如果在原函數上保存了一些屬性,那麽這些屬性會丟失。代碼如下:

var func = function(){
  alert(1);
}
func.a=a;
func=func.after(function(){
  alert(2);
});
alert(func.a);    //輸出:undefined

  另外,這種裝飾方式也疊加了函數的作用域,如果裝飾的鏈條過長,性能上也會受到一些影響

裝飾者模式和代理模式

  裝飾者模式和代理模式的結構看起來非常相像,這兩種模式都描述了怎樣為對象提供一定程度上的間接引用,它們的實現部分都保留了對另外一個對象的引用,並且向那個對象發送請求。代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情。裝飾者模式的作用就是為對象動態加入行為。換句話說,代理模式強調一種關系(Proxy與它的實體之間的關系),這種關系可以靜態的表達,也就是說,這種關系在一開始就可以被確定。而裝飾者模式用於一開始不能確定對象的全部功能時。代理模式通常只有一層代理——本體的引用,而裝飾者模式經常會形成一條長長的裝飾鏈

javascript設計模式——裝飾者模式