常用的Javascript設計模式

tweeto31 8年前發布 | 42K 次閱讀 設計模式 JavaScript開發 JavaScript

來自: https://yq.aliyun.com/articles/6849

《 Practical Common Lisp 》的作者 Peter Seibel 曾說,如果你需要一種模式,那一定是哪里出了問題。他所說的問題是指因為語言的天生缺陷,不得不去尋求和總結一種通用的解決方案。

不管是弱類型或強類型,靜態或動態語言,命令式或說明式語言、每種語言都有天生的優缺點。一個牙買加運動員, 在短跑甚至拳擊方面有一些優勢,在練瑜伽上就欠缺一些。

術士和暗影牧師很容易成為一個出色的輔助,而一個背著梅肯滿地圖飛的敵法就會略顯尷尬。 換到程序中, 靜態語言里可能需要花很多功夫來實現裝飾者,而js由于能隨時往對象上面扔方法,以至于裝飾者模式在js里成了雞肋。

講 Javascript 設計模式的書還比較少,《 Pro javaScript Design Patterns 》是比較經典的一本,但是它里面的例子舉得比較啰嗦,所以結合我在工作中寫過的代碼,把我的理解總結一下。如果我的理解出現了偏差,請不吝指正。

一 單例模式

單例模式的定義是產生一個類的唯一實例,但js本身是一種“無類”語言。很多講js設計模式的文章把{}當成一個單例來使用也勉強說得通。因為js生成對象的方式有很多種,我們來看下另一種更有意義的單例。

有這樣一個常見的需求,點擊某個按鈕的時候需要在頁面彈出一個遮罩層。比如web.qq.com點擊登錄的時候.

這個生成灰色背景遮罩層的代碼是很好寫的.

JavaScript

1
2
3
4
5

varcreateMask =function(){

returndocument,body.appendChild(  document.createElement(div)  );

}

JavaScript

1
2
3
4
5
6
7

$('button').click(function(){

Var mask  = createMask();

mask.show();

})

問題是, 這個遮罩層是全局唯一的, 那么每次調用createMask都會創建一個新的div, 雖然可以在隱藏遮罩層的把它remove掉. 但顯然這樣做不合理.

再看下第二種方案, 在頁面的一開始就創建好這個div. 然后用一個變量引用它.

JavaScript

1
2
3
4
5
6
7

varmask = document.body.appendChild( document.createElement(''div' ) );

$( ''button').click(function(){

mask.show();

} )

這樣確實在頁面只會創建一個遮罩層div, 但是另外一個問題隨之而來, 也許我們永遠都不需要這個遮罩層, 那又浪費掉一個div, 對dom節點的任何操作都應該非常吝嗇.

如果可以借助一個變量. 來判斷是否已經創建過div呢?

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

varmask;

varcreateMask =function(){

if( mask )returnmask;

else{

mask = document,body.appendChild(  document.createElement(div)  );

returnmask;

}

}

看起來不錯, 到這里的確完成了一個產生單列對象的函數. 我們再仔細看這段代碼有什么不妥.

首先這個函數是存在一定副作用的, 函數體內改變了外界變量mask的引用, 在多人協作的項目中, createMask是個不安全的函數. 另一方面, mask這個全局變量并不是非需不可. 再來改進一下.

JavaScript

1
2
3
4
5
6

varcreateMask =function(){

varmask;

returnfunction(){

returnmask || ( mask = document.body.appendChild( document.createElement('div') ) )

}

}()

用了個簡單的閉包把變量mask包起來, 至少對于createMask函數來講, 它是封閉的.

可能看到這里, 會覺得單例模式也太簡單了. 的確一些設計模式都是非常簡單的, 即使從沒關注過設計模式的概念, 在平時的代碼中也不知不覺用到了一些設計模式. 就像多年前我明白老漢推車是什么回事的時候也想過尼瑪原來這就是老漢推車.

GOF里的23種設計模式, 也是在軟件開發中早就存在并反復使用的模式. 如果程序員沒有明確意識到他使用過某些模式, 那么下次他也許會錯過更合適的設計 (這段話來自《松本行弘的程序世界》).

再回來正題, 前面那個單例還是有缺點. 它只能用于創建遮罩層. 假如我又需要寫一個函數, 用來創建一個唯一的xhr對象呢? 能不能找到一個通用的singleton包裝器.

js中函數是第一型, 意味著函數也可以當參數傳遞. 看看最終的代碼.

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12

varsingleton =function( fn ){

varresult;

returnfunction(){

returnresult || ( result = fn .apply(this, arguments ) );

}

}

varcreateMask = singleton(function(){

returndocument.body.appendChild( document.createElement('div') );

})

用一個變量來保存第一次的返回值, 如果它已經被賦值過, 那么在以后的調用中優先返回該變量. 而真正創建遮罩層的代碼是通過回調函數的方式傳人到singleton包裝器中的. 這種方式其實叫橋接模式. 關于橋接模式, 放在后面一點點來說.

然而singleton函數也不是完美的, 它始終還是需要一個變量result來寄存div的引用. 遺憾的是js的函數式特性還不足以完全的消除聲明和語句.

二 簡單工廠模式

簡單工廠模式是由一個方法來決定到底要創建哪個類的實例, 而這些實例經常都擁有相同的接口. 這種模式主要用在所實例化的類型在編譯期并不能確定, 而是在執行期決定的情況。 說的通俗點,就像公司茶水間的飲料機,要咖啡還是牛奶取決于你按哪個按鈕。

簡單工廠模式在創建ajax對象的時候也非常有用.

之前我寫了一個處理ajax異步嵌套的庫,地址在 https://github.com/AlloyTeam/DanceRequest.

這個庫里提供了幾種ajax請求的方式,包括xhr對象的get, post, 也包括跨域用的jsonp和iframe. 為了方便使用, 這幾種方式都抽象到了同一個接口里面.

JavaScript

1
2
3
4
5
6
7
8
9
10
11

varrequest1 = Request('cgi.xx.com/xxx',''get' );

request1.start();

request1.done( fn );

var request2 = Request('cgi.xx.com/xxx' , ''jsonp');

request2.start();

request2.done( fn );

Request實際上就是一個工廠方法, 至于到底是產生xhr的實例, 還是jsonp的實例. 是由后來的代碼決定的。

實際上在js里面,所謂的構造函數也是一個簡單工廠。只是批了一件new的衣服. 我們扒掉這件衣服看看里面。

通過這段代碼, 在firefox, chrome等瀏覽器里,可以完美模擬new.

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

functionA( name ){

this.name = name;

}

functionObjectFactory(){

varobj = {},

Constructor = Array.prototype.shift.call( arguments );

obj.__proto__ = typeofConstructor .prototype ==='number' ? Object.prototype

:  Constructor .prototype;

varret = Constructor.apply( obj, arguments );

returntypeofret ==='object'? ret : obj;

}

vara = ObjectFactory( A,'svenzeng');

alert ( a.name ); //svenzeng

這段代碼來自es5的new和構造器的相關說明, 可以看到,所謂的new, 本身只是一個對象的復制和改寫過程, 而具體會生成什么是由調用ObjectFactory時傳進去的參數所決定的。

三 觀察者模式

觀察者模式( 又叫發布者-訂閱者模式 )應該是最常用的模式之一. 在很多語言里都得到大量應用. 包括我們平時接觸的dom事件. 也是js和dom之間實現的一種觀察者模式.

JavaScript

1
2
3
4
5

div.onclick  = functionclick (){

alert (''click' )

}

只要訂閱了div的click事件. 當點擊div的時候, function click就會被觸發.

那么到底什么是觀察者模式呢. 先看看生活中的觀察者模式。

好萊塢有句名言. “不要給我打電話, 我會給你打電話”. 這句話就解釋了一個觀察者模式的來龍去脈。 其中“我”是發布者, “你”是訂閱者。

再舉個例子,我來公司面試的時候,完事之后每個面試官都會對我說:“請留下你的聯系方式, 有消息我們會通知你”。 在這里“我”是訂閱者, 面試官是發布者。所以我不用每天或者每小時都去詢問面試結果, 通訊的主動權掌握在了面試官手上。而我只需要提供一個聯系方式。

觀察者模式可以很好的實現2個模塊之間的解耦。 假如我正在一個團隊里開發一個html5游戲. 當游戲開始的時候,需要加載一些圖片素材。加載好這些圖片之后開始才執行游戲邏輯. 假設這是一個需要多人合作的項目. 我完成了Gamer和Map模塊, 而我的同事A寫了一個圖片加載器loadImage.

loadImage的代碼如下

JavaScript

1
2
3
4
5
6
7

loadImage(  imgAry, function(){

Map.init();

Gamer.init();

} )

當圖片加載好之后, 再渲染地圖, 執行游戲邏輯. 嗯, 這個程序運行良好. 突然有一天, 我想起應該給游戲加上聲音功能. 我應該讓圖片加載器添上一行代碼.

JavaScript

1
2
3
4
5
6
7
8
9

loadImage(  imgAry, function(){

Map.init();

Gamer.init();

Sount.init();

} )

可是寫這個模塊的同事A去了外地旅游. 于是我打電話給他, 喂. 你的loadImage函數在哪, 我能不能改一下, 改了之后有沒有副作用. 如你所想, 各種不淡定的事發生了. 如果當初我們能這樣寫呢:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

loadImage.listen(''ready', function(){

Map.init();

})

loadImage.listen( ''ready',function(){

Gamer.init();

})

loadImage.listen(''ready',function(){

Sount.init();

})

loadImage完成之后, 它根本不關心將來會發生什么, 因為它的工作已經完成了. 接下來它只要發布一個信號.

loadImage.trigger( ”ready’ );

那么監聽了loadImage的’ready’事件的對象都會收到通知. 就像上個面試的例子. 面試官根本不關心面試者們收到面試結果后會去哪吃飯. 他只負責把面試者的簡歷搜集到一起. 當面試結果出來時照著簡歷上的電話挨個通知.

說了這么多概念, 來一個具體的實現. 實現過程其實很簡單. 面試者把簡歷扔到一個盒子里, 然后面試官在合適的時機拿著盒子里的簡歷挨個打電話通知結果.

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

Events =function() {

varlisten, log, obj, one, remove, trigger, __this;

obj = {};

__this =this;

listen =function( key, eventfn ) { //把簡歷扔盒子, key就是聯系方式.

varstack, _ref; //stack是盒子

stack = ( _ref = obj[key] ) !=null? _ref : obj[ key ] = [];

returnstack.push( eventfn );

};

one =function( key, eventfn ) {

remove( key );

returnlisten( key, eventfn );

};

remove =function( key ) {

var_ref;

return( _ref = obj[key] ) !=null? _ref.length = 0 : void 0;

};

trigger =function() { //面試官打電話通知面試者

varfn, stack, _i, _len, _ref, key;

key = Array.prototype.shift.call( arguments );

stack = ( _ref = obj[ key ] ) !=null? _ref : obj[ key ] = [];

for( _i = 0, _len = stack.length; _i < _len; _i++ ) {

fn = stack[ _i ];

if( fn.apply( __this,  arguments ) ===false) {

returnfalse;

}

}

return{

listen: listen,

one: one,

remove: remove,

trigger: trigger

}

}

最后用觀察者模式來做一個成人電視臺的小應用.

//訂閱者

JavaScript

1
2
3
4
5
6
7
8
9
10
11

varadultTv = Event();

adultTv .listen( ''play',  function( data ){

alert ( "今天是誰的電影" + data.name );

});

//發布者

adultTv .trigger(  ''play',  {'name':'麻生希'}  )

四 適配器模式

去年年前當時正在開發dev.qplus.com, 有個存儲應用分類id的js文件, 分類id的結構最開始設計的比較笨重. 于是我決定重構它. 我把它定義成一個json樹的形式, 大概是這樣:

JavaScript

1
2
3
4
5
6
7
8
9
10
11

varcategory = {

music: {

id: 1,

children: [ , , , , ]

}

}

dev.qplus.com里大概有4,5個頁面都調用這個category對象. 春節前我休了1個星期假. 過年來之后發現郵箱里有封郵件, 設計數據庫的同學把category..js也重構了一份, 并且其他幾個項目里都是用了這份category.js, 我拿過來一看就傻眼了, 和我之前定的數據結構完全不一樣.

當然這是一個溝通上的反面例子. 但接下來的重點是我已經在N個文件里用到了之前我定的category.js. 而且惹上了一些復雜的相關邏輯. 怎么改掉我之前的代碼呢. 全部重寫肯定是不愿意. 所以現在適配器就派上用場了.

只需要把同事的category用一個函數轉成跟我之前定義的一樣.

JavaScript

1

my.category = adapterCategory ( afu.category );

適配器模式的作用很像一個轉接口. 本來iphone的充電器是不能直接插在電腦機箱上的, 而通過一個usb轉接口就可以了.

所以, 在程序里適配器模式也經常用來適配2個接口, 比如你現在正在用一個自定義的js庫. 里面有個根據id獲取節點的方法$id(). 有天你覺得jquery里的$實現得更酷, 但你又不想讓你的工程師去學習新的庫和語法. 那一個適配器就能讓你完成這件事情.

JavaScript

1
2
3
4
5

$id =function( id ){

returnjQuery('#'+ id )[0];

}

五 代理模式

代理模式的定義是把對一個對象的訪問, 交給另一個代理對象來操作.

舉一個例子, 我在追一個MM想給她送一束花,但是我因為我性格比較靦腆,所以我托付了MM的一個好朋友來送。

這個例子不是非常好, 至少我們沒看出代理模式有什么大的用處,因為追MM更好的方式是送一臺寶馬。

再舉個例子,假如我每天都得寫工作日報( 其實沒有這么慘 ). 我的日報最后會讓總監審閱. 如果我們都直接把日報發給 總監 , 那可能 總監 就沒法工作了. 所以通常的做法是把日報發給我的組長 , 組長把所有組員一周的日報都匯總后再發給總監 .

實際的編程中, 這種因為性能問題使用代理模式的機會是非常多的。比如頻繁的訪問dom節點, 頻繁的請求遠程資源. 可以把操作先存到一個緩沖區, 然后自己選擇真正的觸發時機.

再來個詳細的例子,之前我寫了一個街頭霸王的游戲, 地址在 http://alloyteam.github.com/StreetFighter/

游戲中隆需要接受鍵盤的事件, 來完成相應動作.

于是我寫了一個keyManage類. 其中在游戲主線程里監聽keyManage的變化.

JavaScript

1
2
3
4
5
6
7

varkeyMgr = keyManage();

keyMgr.listen(''change',function( keyCode ){

console.log( keyCode );

});

圖片里面隆正在放升龍拳, 升龍拳的操作是前下前+拳. 但是這個keyManage類只要發生鍵盤事件就會觸發之前監聽的change函數. 這意味著永遠只能取得前,后,前,拳這樣單獨的按鍵事件,而無法得到一個按鍵組合。

好吧,我決定改寫我的keyManage類, 讓它也支持傳遞按鍵組合. 但是如果我以后寫個html5版雙截龍,意味著我每次都得改寫keyManage. 我總是覺得, 這種函數應該可以抽象成一個更底層的方法, 讓任何游戲都可以用上它.

所以最后的keyManage只負責映射鍵盤事件. 而隆接受到的動作是通過一個代理對象處理之后的.

JavaScript

1
2
3
4
5
6
7

varkeyMgr = keyManage();

keyMgr.listen(''change', proxy(function( keyCode ){

console.log( keyCode ); //前下前+拳

)} );

至于proxy里面怎么實現,完全可以自由發揮。

還有個例子就是在調用ajax請求的時候,無論是各種開源庫,還是自己寫的Ajax類, 都會給xhr對象設置一個代理. 我們不可能頻繁的去操作xhr對象發請求, 而應該是這樣.

JavaScript

1
2
3
4
5
6
7

varrequest = Ajax.get('cgi.xx.com/xxx');

request.send();

request.done(function(){

});

六 橋接模式

橋接模式的作用在于將實現部分和抽象部分分離開來, 以便兩者可以獨立的變化。在實現api的時候, 橋接模式特別有用。比如最開始的singleton的例子.

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12

varsingleton =function( fn ){

varresult;

returnfunction(){

returnresult || ( result = fn .apply(this, arguments ) );

}

}

varcreateMask = singleton(function(){

returndocument.body.appendChild( document.createElement('div') );

})

singleton是抽象部分, 而createMask是實現部分。 他們完全可以獨自變化互不影響。 如果需要再寫一個單例的createScript就一點也不費力.

JavaScript

1
2
3
4
5

varcreateScript = singleton(function(){

returndocument.body.appendChild( document.createElement('script') );

})

另外一個常見的例子就是forEach函數的實現, 用來迭代一個數組.

JavaScript

1
2
3
4
5
6
7
8

forEach =function( ary, fn ){

for(vari = 0, l = ary.length; i < l; i++ ){

varc = ary[ i ];

if( fn.call( c, i, c ) ===false){

returnfalse;

}

}

}

可以看到, forEach函數并不關心fn里面的具體實現. fn里面的邏輯也不會被forEach函數的改寫影響.

JavaScript

1
2
3
4
5
6
7
8
9
10
11

forEach( [1,2,3],function( i, n ){

alert ( n*2 )

} )

forEach( [1,2,3],function( i, n ){

alert ( n*3 )

} )

七 外觀模式

外觀模式(門面模式),是一種相對簡單而又無處不在的模式。外觀模式提供一個高層接口,這個接口使得子系統更加方便調用。用一段再簡單不過的代碼來表示

JavaScript

1
2
3
4
5
6

vargetName =function(){

return''svenzeng"

}

vargetSex =function(){

return'man'

}

如果你需要分別調用getName和getSex函數. 那可以用一個更高層的接口getUserInfo來調用.

JavaScript

1
2
3
4

vargetUserInfo =function(){

varinfo = a() + b();

returninfo;

}

也許你會問為什么一開始不把getName和getSex的代碼寫到一起, 比如這樣

JavaScript

1
2
3

vargetNameAndSex =function(){

return'svenzeng" + "man";

}

答案是顯而易見的,飯堂的炒菜師傅不會因為你預定了一份燒鴨和一份白菜就把這兩樣菜炒在一個鍋里。他更愿意給你提供一個燒鴨飯套餐。同樣在程序設計中,我們需要保證函數或者對象盡可能的處在一個合理粒度,畢竟不是每個人喜歡吃燒鴨的同時又剛好喜歡吃白菜。外觀模式還有一個好處是可以對用戶隱藏真正的實現細節,用戶只關心最高層的接口。比如在燒鴨飯套餐的故事中,你并不關心師傅是先做燒鴨還是先炒白菜,你也不關心那只鴨子是在哪里成長的。

最后寫個我們都用過的外觀模式例子

JavaScript

1
2
3
4

varstopEvent =function( e ){  //同時阻止事件默認行為和冒泡

e.stopPropagation();

e.preventDefault();

}

八 訪問者模式

GOF官方定義: 訪問者模式是表示一個作用于某個對象結構中的各元素的操作。它使可以在不改變各元素的類的前提下定義作用于這些元素的新操作。我們在使用一些操作對不同的對象進行處理時,往往會根據不同的對象選擇不同的處理方法和過程。在實際的代碼過程中,我們可以發現,如果讓所有的操作分散到各個對象中,整個系統會變得難以維護和修改。且增加新的操作通常都要重新編譯所有的類。因此,為了解決這個問題,我們可以將每一個類中的相關操作提取出來,包裝成一個獨立的對象,這個對象我們就稱為訪問者(Visitor)。利用訪問者,對訪問的元素進行某些操作時,只需將此對象作為參數傳遞給當前訪問者,然后,訪問者會依據被訪問者的具體信息,進行相關的操作。

據統計,上面這段話只有5%的人會看到最后一句。那么通俗點講,訪問者模式先把一些可復用的行為抽象到一個函數(對象)里,這個函數我們就稱為訪問者(Visitor)。如果另外一些對象要調用這個函數,只需要把那些對象當作參數傳給這個函數,在js里我們經常通過call或者apply的方式傳遞this對象給一個Visitor函數.訪問者模式也被稱為GOF總結的23種設計模式中最難理解的一種。不過這有很大一部分原因是因為《設計模式》基于C++和Smalltalk寫成. 在強類型語言中需要通過多次重載來實現訪問者的接口匹配。

而在js這種基于鴨子類型的語言中,訪問者模式幾乎是原生的實現, 所以我們可以利用apply和call毫不費力的使用訪問者模式,這一小節更關心的是這種模式的思想以及在js引擎中的實現。

我們先來了解一下什么是鴨子類型,說個故事:很久以前有個皇帝喜歡聽鴨子呱呱叫,于是他召集大臣組建一個一千只鴨子的合唱團。大臣把全國的鴨子都抓來了,最后始終還差一只。有天終于來了一只自告奮勇的雞,這只雞說它也會呱呱叫,好吧在這個故事的設定里,它確實會呱呱叫。 后來故事的發展很明顯,這只雞混到了鴨子的合唱團中。— 皇帝只是想聽呱呱叫,他才不在乎你是鴨子還是雞呢。

這個就是鴨子類型的概念,在js這種弱類型語言里,很多方法里都不做對象的類型檢測,而是只關心這些對象能做什么。Array構造器和String構造器的prototype上的方法就被特意設計成了訪問者。這些方法不對this的數據類型做任何校驗。這也就是為什么arguments能冒充array調用push方法.

看下v8引擎里面Array.prototype.push的代碼:

JavaScript

1
2

functionArrayPush() { varn = TO_UINT32(this.length );

varm = %_ArgumentsLength();   for(vari = 0; i < m; i++) {   this[i+n] = %_Arguments(i);   //屬性拷貝  }  this.length = n + m;             //修正length  return this.length;}

可以看到,ArrayPush方法沒有對this的類型做任何顯示的限制,所以理論上任何對象都可以被傳入ArrayPush這個訪問者。

不過在代碼的執行期,還是會受到一些隱式限制,在上面的例子很容易看出要求:

1、 this對象上面可儲存屬性. //反例: 值類型的數據

2、 this的length屬性可寫. //反例: functon對象, function有一個只讀的length屬性, 表示形參個數.

如果不符合這2條規則的話,代碼在執行期會報錯. 也就是說, Array.prototype.push.call( 1, ‘first’ )和Array.prototoype.push.call( function(){}, ‘first’ )都達不到預期的效果.

利用訪問者,我們來做個有趣的事情. 給一個object對象增加push方法.

JavaScript

1
2
3
4
5
6
7
8
9

varVisitor = {}

Visitor .push  = function(){

returnArray.prototype.push.apply(this, arguments );

}

varobj = {};

obj.push = Visitor .push;

obj.push( '"first");

alert ( obj[0] ) //"first"

alert ( obj.length ); //1

九 策略模式

策略模式的意義是定義一系列的算法,把它們一個個封裝起來,并且使它們可相互替換。

一個小例子就能讓我們一目了然。

回憶下jquery里的animate方法.

JavaScript

1
2

$( div ).animate( {"left: 200px"}, 1000,'linear'); //勻速運動

$( div ).animate( {"left: 200px"}, 1000,'cubic'); //三次方的緩動

這2句代碼都是讓div在1000ms內往右移動200個像素. linear(勻速)和cubic(三次方緩動)就是一種策略模式的封裝.再來一個例子. 上半年我寫的dev.qplus.com, 很多頁面都會有個即時驗證的表單. 表單的每個成員都會有一些不同的驗證規則.

比如姓名框里面, 需要驗證非空,敏感詞,字符過長這幾種情況。 當然是可以寫3個if else來解決,不過這樣寫代碼的擴展性和維護性可想而知。如果表單里面的元素多一點,需要校驗的情況多一點,加起來寫上百個if else也不是沒有可能。所以更好的做法是把每種驗證規則都用策略模式單獨的封裝起來。需要哪種驗證的時候只需要提供這個策略的名字。就像這樣:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14

nameInput.addValidata({

notNull:true,

dirtyWords:true,

maxLength: 30

})

而notNull,maxLength等方法只需要統一的返回true或者false,來表示是否通過了驗證。

validataList = {

notNull:function( value ){

returnvalue !=='';

},

maxLength:function( value, maxLen ){

returnvalue.length() > maxLen;

}

}

可以看到,各種驗證規則很容易被修改和相互替換。如果某天產品經理建議字符過長的限制改成60個字符。那只需要0.5秒完成這次工作。

十 模版方法模式

模式方法是預先定義一組算法,先把算法的不變部分抽象到父類,再將另外一些可變的步驟延遲到子類去實現。聽起來有點像工廠模式( 非前面說過的簡單工廠模式 ).最大的區別是,工廠模式的意圖是根據子類的實現最終獲得一種對象. 而模版方法模式著重于父類對子類的控制.

按GOF的描敘,模版方法導致一種反向的控制結構,這種結構有時被稱為“好萊塢法則”,即“別找我們,我們找你”。這指的是一個父類調用一個子類的操作,而不是相反。一個很常用的場景是在一個公司的項目中,經常由架構師搭好架構,聲明出抽象方法。下面的程序員再去分頭重寫這些抽象方法。

在深入了解之前,容許我先扯遠一點。作為一個進化論的反對者,假設這個世界是上帝用代碼創造的。那么上帝創造生命的時候可能就用到了模版方法模式。看看他是怎么在生命構造器中聲明模版方法的:

JavaScript

1
  <div>
    2 
  </div>

  <div>
    3 
  </div>

  <div>
    4 
  </div>

  <div>
    5 
  </div>

  <div>
    6 
  </div>

  <div>
    7 
  </div>

  <div>
    8 
  </div>

  <div>
    9 
  </div>

  <div>
    10 
  </div>

  <div>
    11 
  </div>

  <div>
    12 
  </div>

  <div>
    13 
  </div>

  <div>
    14 
  </div>

  <div>
    15 
  </div>

  <div>
    16 
  </div>

  <div>
    17 
  </div>

  <div>
    18 
  </div>

  <div>
    19 
  </div>

  <div>
    20 
  </div>

</td>

 <td> 
  <div> 
   <p>varLife =function(){</p>

   <p>}</p>

   <p>Life.prototype.init =function(){</p>

   <p>this.DNA復制();</p>

   <p>this.出生();</p>

   <p>this.成長();</p>

   <p>this.衰老();</p>

   <p>this.死亡();</p>

   <p>}</p>

   <p>this.prototype.DNA復制 =function(){</p>

   <p>&*$%&^%^&(&(&(&&(^^(*) //看不懂的代碼</p>

   <p>}</p>

   <p>Life.prototype.出生 =function(){</p>

   <p>}</p>

   <p>Life.prototype.成長 =function(){</p>

   <p>}</p>

   <p>Life.prototype.衰老 =function(){</p>

   <p>}</p>

   <p>Life.prototype.死亡 =function(){</p>

   <p>}</p>

  </div>

</td>

</tr>

</tbody>

</table>

其中DNA復制是預先定義的算法中不變部分. 所有子類都不能改寫它. 如果需要我們可以寫成protected的類型.

而其他的函數在父類中會被先定義成一個空函數(鉤子). 然后被子類重寫,這就是模版方法中所謂的可變的步驟。

假設有個子類哺乳動物類繼承了Life類.

JavaScript

1
2
3

varMammal =function(){

}

Mammal.prototype = Life.prototype;  //繼承Life

然后重寫出生和衰老這兩個鉤子函數.

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Mammal.prototope.出生 =function(){

'胎生()

}

Mammal.prototype.成長 =function(){

//再留給子類去實現

}

Mammal.prototope.衰老 =function(){

自由基的過氧化反應()

}

Life.prototype.死亡 =function(){

//再留給子類去實現

}

//再實現一個Dog類

var= Dog =function(){

}

//Dog繼承自哺乳動物.

Dog.prototype = Mammal.prototype;

vardog =newDog();

dog.init();

至此,一只小狗的生命會依次經歷DNA復制,出生,成長,衰老,死亡這幾個過程。這些步驟早在它出生前就決定了。所幸的是,上帝沒有安排好它生命的所有細節。它還是能通過對成長函數的重寫,來成為一只與眾不同的小狗。

舉個稍微現實點的例子,游戲大廳中的所有游戲都有登錄,游戲中,游戲結束這幾個過程,而登錄和游戲結束之后彈出提示這些函數都是應該公用的。那么首先需要的是一個父類。

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

vargameCenter =function(){

}

gameCenter.ptototype.init =function(){

this.login();

this.gameStart();

this.end();

}

gameCenter.prototype.login=function(){

//do something

}

gameCenter.prototype.gameStart=function(){

//空函數, 留給子類去重寫

}

gameCenter.prototype.end=function(){

alert ("歡迎下次再來玩");

}

接下來創建一個斗地主的新游戲, 只需要繼承gameCenter然后重寫它的gameStart函數.

JavaScript

1
2
3
4
5
6
7

var斗地主 =function(){

}

斗地主.prototype = gameCenter.prototype; //繼承

斗地主.prototype.gameStart =function(){

//do something

}

(new斗地主).init();

這樣一局新的游戲就開始了.

十一 中介者模式

中介者對象可以讓各個對象之間不需要顯示的相互引用,從而使其耦合松散,而且可以獨立的改變它們之間的交互。

打個比方,軍火買賣雙方為了安全起見,找了一個信任的中介來進行交易。買家A把錢交給中介B,然后從中介手中得到軍火,賣家C把軍火賣給中介,然后從中介手中拿回錢。一場交易完畢,A甚至不知道C是一只猴子還是一只猛犸。因為中介的存在,A也未必一定要買C的軍火,也可能是D,E,F。

銀行在存款人和貸款人之間也能看成一個中介。存款人A并不關心他的錢最后被誰借走。貸款人B也不關心他借來的錢來自誰的存款。因為有中介的存在,這場交易才變得如此方便。

中介者模式和代理模式有一點點相似。都是第三者對象來連接2個對象的通信。具體差別可以從下圖中區別。

代理模式:

中介者模式

代理模式中A必然是知道B的一切,而中介者模式中A,B,C對E,F,G的實現并不關心.而且中介者模式可以連接任意多種對象。

切回到程序世界里的mvc,無論是j2ee中struts的Action. 還是js中backbone.js和spine.js里的Controler. 都起到了一個中介者的作用.拿backbone舉例. 一個mode里的數據并不確定最后被哪些view使用. view需要的數據也可以來自任意一個mode. 所有的綁定關系都是在controler里決定. 中介者把復雜的多對多關系, 變成了2個相對簡單的1對多關系.

一段簡單的示例代碼:

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12

varmode1 = Mode.create(),  mode2 = Mode.create();

varview1 = View.create(),   view2 = View.create();

varcontroler1 = Controler.create( mode1, view1,function(){

view1.el.find(''div' ).bind( ''click',function(){

this.innerHTML = mode1.find('data');

} )

})

varcontroler2 = Controler.create( mode2 view2,function(){

view1.el.find(''div' ).bind( ''click',function(){

this.innerHTML = mode2.find('data');

} )

})

十二 迭代器模式

迭代器模式提供一種方法順序訪問一個聚合對象中各個元素,而又不需要暴露該方法中的內部表示。

js中我們經常會封裝一個each函數用來實現迭代器。

array的迭代器:

JavaScript

1
2
3
4

forEach =function( ary, fn ){ for(vari = 0, l = ary.length; i < l; i++ ){   varc = ary[ i ];   if( fn.call( c, i , c ) ===false){     returnfalse;    }   }}

forEach( [ 1, 2, 3 ],function( i, n ){

alert ( i );

})

obejct的迭代器:

JavaScript

1
2
3
4

forEach =function( obj, fn ){ for(variinobj ){   varc = obj[ i ];   if( fn.call( c, i, c ) ===false){     returnfalse;    }   }}

forEach( {"a": 1,"b": 2},function( i, n ){

alert ( i );

})

十三 組合模式

組合模式又叫部分-整體模式,它將所有對象組合成樹形結構。使得用戶只需要操作最上層的接口,就可以對所有成員做相同的操作。一個再好不過的例子就是jquery對象,大家都知道1個jquery對象其實是一組對象集合。比如在這樣一個HTML頁面

JavaScript

1
2
3
4

<div>

<span></span>

<span></span>

</div>

我們想取消所有節點上綁定的事件, 需要這樣寫

JavaScript

1
2
3
4
5

varallNodes = document.getElementsByTagName("*");

varlen = allNodes.length;

while( len-- ){

allNodes.unbind("*");

}

但既然用了jquery,就肯定不會再做這么搓的事情。我們只需要$( ‘body’ ).unbind( ‘*’ );

當每個元素都實現unbind接口, 那么只需調用最上層對象$( ‘body’ )的unbind, 便可自動迭代并調用所有組合元素的unbind方法.

再來個具體點的例子, 還是dev.qplus.com這個網站的即時驗證表單。

注意下面那個修改資料的按鈕,如果有任意一個field的驗證沒有通過,修改資料的按鈕都將是灰色不可點的狀態。 這意味著我們重新填寫了表單內容后, 都得去校驗每個field, 保證它們全部OK.這代碼不難實現.

JavaScript

1
2
3

if( nameField.validata() && idCard.validata() && email.validata() && phone.validata() ){

alert ("驗證OK");

}

似乎我們用一個外觀模式也能勉強解決這里條件分支堆砌的問題,但真正的問題是,我們并不能保證表單里field的數量,也許明天產品經理就讓你刪掉一個或者增加兩個.那么這樣的維護方式顯然不能被接受.

更好的實現是有一個form.validata函數, 它負責把真正的validata操作分發給每個組合對象.

form.validata函數里面會依次遍歷所有需要校驗的field. 若有一個field校驗未通過, form.validata都會返回false. 偽代碼如下.

JavaScript

1
2
3
4
5
6
7
8

form.validata =function(){

forEach( fields,function( index, field ){

if( field.validata() ===false ){

returnfalse;

}

})

returntrue;

}

</div>

聲明:云棲社區站內文章,未經作者本人允許或特別聲明,嚴禁轉載,但歡迎分享。

</div>

 本文由用戶 tweeto31 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色