標籤:
設計模式
設計模式是命名、抽象和識別對可重用的物件導向設計有用的的通用設計結構。設計模式確定類和他們的實體、他們的角色和協作、還有他們的責任分配。
每一個設計模式都聚焦於一個物件導向的設計難題或問題。它描述了在其它設計的約束下它能否使用,使用它後的後果和得失。因為我們必須最終實現我們的設計模式,所以每個設計模式都提供了例子,代碼來對實現進行闡釋.
雖然設計模式被描述為物件導向的設計,它們基於那些已經被主流物件導向語言實現過的解決方案...”。
種類
設計模式可以被分成幾個不同的種類。在這個部分我們將分為三類:建立型設計模式、結構設計模式、行為設計模式。
建立型設計模式
建立型設計模式關注於對象建立的機制方法,通過該方法,對象以適應工作環境的方式被建立。基本的對象建立方法可能會給項目增加額外的複雜性,而這些模式的目的就是為了通過控制建立過程解決這個問題。
屬於這一類的一些模式是:構造器模式(Constructor),原廠模式(Factory),抽象原廠模式(Abstract),原型模式(Prototype),單例模式(Singleton)以及 建造者模式(Builder)。
結構設計模式
結構模式關注於對象組成和通常識別的方式實現不同對象之間的關係。該模式有助於在系統的某一部分發生改變的時候,整個系統結構不需要改變。該模式同樣有助於對系統中某部分沒有達到某一目的的部分進行重組。
在該分類下的模式有:裝飾模式,面板模式,享元模式,適配器模式和代理模式。
行為設計模式
行為模式關注改善或精簡在系統中不同對象間通訊。
行為模式包括:迭代模式,中介者模式,觀察者模式和訪問者模式。
下面我們通過分開介紹各個常用的設計模式,來加深對設計模式的理解。
構造器模式
構造器是一個當建立對象的記憶體被分配後,用來初始化該對象的一個特殊函數。物件建構器是被用來建立特殊類型的對象的,首先它要準備使用的對象,其次在對象初次被建立時,通過接收參數,構造器要用來對成員的屬性和方法進行賦值。
由於javascript不支援類的概念,所以必須通過構造器來使用new關鍵字初始化對象。一個基礎的構造器代碼如下:
function Car( model, year, miles ) { this.model = model; this.year = year; this.miles = miles; this.toString = function () { return this.model + " has done " + this.miles + " miles"; };} // 使用: // 我們可以樣本化一個Carvar civic = new Car( "Honda Civic", 2009, 20000 );var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); // 開啟瀏覽器控制台查看這些對象toString()方法的輸出值// output of the toString() method being called on// these objectsconsole.log( civic.toString() );console.log( mondeo.toString() );
但是這樣的話,繼承起來比較麻煩,而且每個Car建構函式建立的對象中,toString之類的函數都會被重新定義。所以還是要利用原型,來實現最佳的構造器:
function Car( model, year, miles ) { this.model = model; this.year = year; this.miles = miles; } // 注意這裡我們使用Note here that we are using Object.prototype.newMethod 而不是// Object.prototype ,以避免我們重新定義原型對象Car.prototype.toString = function () { return this.model + " has done " + this.miles + " miles";}; // 使用: var civic = new Car( "Honda Civic", 2009, 20000 );var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); console.log( civic.toString() );console.log( mondeo.toString() );
原廠模式
一個工廠能提供一個建立對象的公用介面,我們可以在其中指定我們希望被建立的工廠對象的類型。說的簡單點,就像飲水機,要咖啡還是牛奶取決於你按哪個按鈕。
簡單原廠模式在建立ajax對象的時候可以體現出來,可以通過jquery中的$.ajax方法來理解,也可以通過我自己寫的一個ajax方法來理解,地址在:http://runjs.cn/code/j5dkikwu
ajax("test002.txt",{type:"GET",data:{name:"liuf",age:23},onsuccess:function(responseText,xhr){document.getElementById("input").value=responseText;},onfail:function(){//document.write("fail");}});
ajax實際上就是一個Factory 方法,至於到底是用get方法還是post方法,都由後面的代碼來決定。這就是前面所說的“一個工廠能提供一個建立對象的公用介面,我們可以在其中指定我們希望被建立的工廠對象的類型”。
單例模式
單例模式之所以這麼叫,是因為它限制一個類只能有一個執行個體化對象。經典的實現方式是,建立一個類,這個類包含一個方法,這個方法在沒有對象存在的情況下,將會建立一個新的執行個體對象。如果對象存在,這個方法只是返回這個對象的引用。但是javascript本來就是無類的,所以簡單地來說,就是沒有就建立,有就不建立直接用。
那麼我們看看現實中的案例吧,點擊一個按鈕後出現一個遮罩層,這是一個常用的需求吧。代碼如下:
var createMask = function(){ return document,body.appendChild( document.createElement(div) ); }$( 'button' ).click( function(){ var mask = createMask(); mask.show(); })
這樣寫就會出現一個問題,就是每次調用createMask都會建立一個新的div,雖然可以在隱藏遮罩層時將其remove,但是這樣還是會帶來效能的損耗,那麼可以做如下改進,就是在頁面一開始就建立div,代碼如下:
var mask = document.body.appendChild( document.createElement( ''div' ) ); $( ''button' ).click( function(){ mask.show(); } )
這樣確實可以保證頁面只會建立一個遮罩層,但是也有一個問題,就是如果使用者不需要用到這個div,豈不是白白建立了它。於是我們可以藉助一個變數來判斷是否已經建立過div,代碼如下:
var mask; var createMask = function(){ if ( mask ) return mask; else{ mask = document,body.appendChild( document.createElement(div) ); return mask; } }
這樣看起來不錯,但是mask作為一個全域變數,是否會造成汙染呢?所以最好的辦法如下:
var createMask = function(){ var mask; return function(){ return mask || ( mask = document.body.appendChild( document.createElement('div') ) ) }}()
這就是前面所說的“沒有就建立,有就不建立直接用”。沒錯,單例模式就是這麼簡單,設計模式其實並不難,編程中我們其實一直有用到,只是自己沒有發現罷了。
橋接模式
橋接模式就是將實現部分和抽象部分分離開來,以便兩者可以獨立的變化。在實現api的時候,橋接模式非常常用。
我們以javascript的forEach方法為例:
forEach = function( ary, fn ){ for ( var i = 0, l = ary.length; i < l; i++ ){ var c = ary[ i ]; if ( fn.call( c, i, c ) === false ){ return false; } }}
可以看到,forEach函數並不關心fn裡面的具體實現. fn裡面的邏輯也不會被forEach函數的改寫影響.
使用代碼如下:
forEach( [1,2,3], function( i, n ){ alert ( n*2 ) } ) forEach( [1,2,3], function( i, n ){ alert ( n*3 ) } )
面板模式
面板模式是一種無處不在的模式,面板模式提供一個高層介面,這個介面使得用戶端或者子系統調用起來更加方法。比如:
var getName = function(){ return ''svenzeng"}var getSex = function(){ return 'man'}
現在我要調用兩個方法,我就可以使用一個更加高層的介面來調用:
var getUserInfo = function(){ var info = getName () + getSex (); return info;}
這樣就方便組裝,如果一開始就把兩個寫到一個函數中,那就不能夠只單獨調用其中一個了。
享元模式
享元模式是一個最佳化重複、緩慢和低效資料共用代碼的經典結構化解決方案。它的目標是以相關對象儘可能多的共用資料,來減少應用程式中記憶體的使用(例如:應用程式的配置、狀態等)。通俗的講,享元模式就是用來減少程式所需的對象個數。
舉一個例子,網頁中的瀑布流,或者webqq的好友名單中,每次往下拉時,都會建立新的div。那麼如果有很對div呢?瀏覽器豈不是卡死了?所以我們會想到一種辦法,就是把已經消失在視線外的div都刪除掉,這樣頁面就可以保持一定數量的節點,但是頻繁的刪除和添加節點,又會帶來很大的效能開銷。
這個時候就可以用到享元模式了,享元模式可以提供一些共用的對象以便重複利用。比如頁面中只能顯示10個div,那始終出現在使用者視線中的這10個div就可以寫成享元。
原理其實很簡單, 把剛隱藏起來的div放到一個數組中, 當需要div的時候, 先從該數組中取, 如果數組中已經沒有了, 再重新建立一個. 這個數組裡的div就是享元, 它們每一個都可以當作任何使用者資訊的載體.代碼如下:
var getDiv = (function(){ var created = []; var create = function(){ return document.body.appendChild( document.createElement( 'div' ) ); } var get = function(){ if ( created.length ){ return created.shift(); }else{ return create(); } }/* 一個假設的事件,用來監聽剛消失在視線外的div,實際上可以通過監聽捲軸位置來實現 */ userInfoContainer.disappear(function( div ){ created.push( div ); }) })() var div = getDiv(); div.innerHTML = "${userinfo}";
適配器模式
適配器模式就是將一個類的介面轉換成客戶希望的另外一個介面。通俗一點的說,將像蘋果手機不能差在電腦機箱上,必須有一個轉換器,而這個轉換器就是適配器。
在程式裡適配器模式也經常用來適配2個介面, 比如你現在正在用一個自訂的js庫. 裡面有個根據id擷取節點的方法$id(). 有天你覺得jquery裡的$實現得更酷, 但你又不想讓你的工程師去學習新的庫和文法. 那一個適配器就能讓你完成這件事情.
$id = function( id ){ return jQuery( '#' + id )[0]; }
這樣就不用再一個一個的修改了。
代理模式
代理模式就是把對一個對象的訪問,交給另一個代理對象來操作。說得通俗一點,程式員每天寫日報,日報最後會給總監審閱,但是如果所有人都直接發給總監,那總監就沒法工作了。所以每個人會把自己的日報發給自己的組長,再由組長轉寄給總監。這個組長就是代理。
編程中用到代理模式的情況也不少,比如大量操作dom時,我們會先建立文檔片段,再統一加到dom樹中。
樣本如下:
中介者模式
中介者模式是觀察者模式中的共用被觀察者對象。在這個系統中的對象之間直接的發布/訂閱關係被犧牲掉了,取而代之的是維護一個通訊的中心節點。中介者模式和代理模式是有區別的,區別如下:
中介者對象可以讓各個對象之間不需要相互引用,從而使其耦合鬆散,而且可以獨立的改變透明之間的互動。通俗點講,銀行在存款人和貸款人之間也能看成一個中介。存款人A並不關心他的錢最後被誰借走。貸款人B也不關心他借來的錢來自誰的存款。因為有中介的存在,這場交易才變得如此方便。
在編程中,大名鼎鼎的MVC結構中的Controller不就是一個中介者嗎?拿backbone舉例. 一個mode裡的資料並不確定最後被哪些view使用. view需要的資料也可以來自任意一個mode. 所有的綁定關係都是在controler裡決定. 中介者把複雜的多對多關係, 變成了2個相對簡單的1對多關係。
範例程式碼如下:
var mode1 = Mode.create(), mode2 = Mode.create();var view1 = View.create(), view2 = View.create();var controler1 = Controler.create( mode1, view1, function(){ view1.el.find( ''div' ).bind( ''click', function(){ this.innerHTML = mode1.find( 'data' ); } )})var controler2 = Controler.create( mode2 view2, function(){ view1.el.find( ''div' ).bind( ''click', function(){ this.innerHTML = mode2.find( 'data' ); } )})觀察者模式
觀察者模式是這樣一種設計模式。一個被稱作被觀察者的對象,維護一組被稱為觀察者的對象,這些對象依賴於被觀察者,被觀察者自動將自身的狀態的任何變化通知給它們。
當一個被觀察者需要將一些變化通知給觀察者的時候,它將採用廣播的方式,這條廣播可能包含特定於這條通知的一些資料。
當特定的觀察者不再需要接受來自於它所註冊的被觀察者的通知的時候,被觀察者可以將其從所維護的組中刪除。
通俗點理解,就是面試官是被觀察者,而等待通知的人是觀察者。
javascript中平時接觸的dom事件,其實就是一種觀察者模式的體現:
div.onclick = function click (){ alert ( ''click' ) }
只要訂閱了div的click事件,當點擊div的時候,click函數就會執行。
觀察者模式可以很好的實現連個模組之間的解耦,假如一個多人合作的項目,我負責Map和Gamer模式:
loadImage( imgAry, function(){ Map.init(); Gamer.init(); } )
<p>而別人負責loadImage方法的維護,如果有一天,我要加上一個Sound模組,而我無權動用別人的代碼,只能空空等待別人回來添加:</p><pre name="code" class="html">loadImage( imgAry, function(){ Map.init(); Gamer.init(); Sount.init(); } )
所以我們可以做如下改動:
loadImage.listen( ''ready', function(){ Map.init(); }) loadImage.listen( ''ready', function(){ Gamer.init(); }) loadImage.listen( ''ready', function(){ Sount.init(); })
loadImage完成之後,不用再關心未來會發送什麼,接下來它只需要發送一個訊號:
loadImage.trigger( ‘ready’ );
所有監聽ready事件的對象就都會收到通知。這就是觀察者模式的應用情境。
說到這裡,常用的設計模式也講完了,深入理解javascript系列也將告一段落。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
深入理解javascript之設計模式