前面的話 面向對象的設計原則,可以說每種設計模式都是為了讓代碼迎合其中一個或多個原則而出現的, 它們本身已經融入了設計模式之中,給面向對象編程指明瞭方向。適合javascript開發的設計原則包括是單一職責原則、最少知識原則和開放封閉原則。本文將詳細介紹面向對象的設計原則 單一職責原則 就一個類而言 ...
前面的話
面向對象的設計原則,可以說每種設計模式都是為了讓代碼迎合其中一個或多個原則而出現的, 它們本身已經融入了設計模式之中,給面向對象編程指明瞭方向。適合javascript開發的設計原則包括是單一職責原則、最少知識原則和開放封閉原則。本文將詳細介紹面向對象的設計原則
單一職責原則
就一個類而言,應該僅有一個引起它變化的原因。在javascript中,需要用到類的場景並不太多,單一職責原則更多地是被運用在對象或者方法級別上
單一職責原則(SRP)的職責被定義為“引起變化的原因”。如果有兩個動機去改寫一個方法,那麼這個方法就具有兩個職責。每個職責都是變化的一個軸線,如果一個方法承擔了過多的職責,那麼在需求的變遷過程中,需要改寫這個方法的可能性就越大。此時,這個方法通常是一個不穩定的方法,修改代碼總是一件危險的事情,特別是當兩個職責耦合在一起的時候,一個職責發生變化可能會影響到其他職責的實現,造成意想不到的破壞,這種耦合性得到的是低內聚和脆弱的設計。因此,SRP原則體現為:一個對象(方法)只做一件事情
SRP原則在很多設計模式中都有著廣泛的運用,例如代理模式、迭代器模式、單例模式和裝飾者模式
【代理模式】
通過增加虛擬代理的方式,把預載入圖片的職責放到代理對象中,而本體僅僅負責往頁面中添加img標簽,這也是它最原始的職責
myImage負責往頁面中添加img標簽:
var myImage = (function(){ var imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); return { setSrc: function( src ){ imgNode.src = src; } } })();
proxyImage負責預載入圖片,併在預載入完成之後把請求交給本體 myImage:
var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage.setSrc( this.src ); } return { setSrc: function( src ){ myImage.setSrc( 'file://loading.gif' ); img.src = src; } } })(); proxyImage.setSrc( 'http://test.jpg' );
把添加img標簽的功能和預載入圖片的職責分開放到兩個對象中,這兩個對象各自都只有一個被修改的動機。在它們各自發生改變的時候,也不會影響另外的對象
【迭代器模式】
有這樣一段代碼,先遍歷一個集合,然後往頁面中添加一些div,這些div的innerHTML分別對應集合里的元素:
var appendDiv = function( data ){ for ( var i = 0, l = data.length; i < l; i++ ){ var div = document.createElement( 'div' ); div.innerHTML = data[ i ]; document.body.appendChild( div ); } }; appendDiv( [ 1, 2, 3, 4, 5, 6 ] );
這其實是一段很常見的代碼,經常用於ajax請求之後,在回調函數中遍歷ajax請求返回的數據,然後在頁面中渲染節點。appendDiv函數本來只是負責渲染數據,但是在這裡它還承擔了遍歷聚合對象data的職責。如果有一天cgi返回的data數據格式從array變成了object,那遍歷data的代碼就會出現問題,必須改成for in的方式,這時候必須去修改appendDiv里的代碼,否則因為遍歷方式的改變,導致不能順利往頁面中添加div節點
有必要把遍歷data的職責提取出來,這正是迭代器模式的意義,迭代器模式提供了一種方法來訪問聚合對象,而不用暴露這個對象的內部表示。
當把迭代聚合對象的職責單獨封裝在each函數中後,即使以後還要增加新的迭代方式,只需要修改each函數即可,appendDiv函數不會受到牽連,代碼如下:
var each = function( obj, callback ) { var value, i = 0, length = obj.length, isArray = isArraylike( obj ); // isArraylike 函數未實現 if ( isArray ) { // 迭代類數組 for ( ; i < length; i++ ) { callback.call( obj[ i ], i, obj[ i ] ); } } else { for ( i in obj ) { // 迭代object 對象 value = callback.call( obj[ i ], i, obj[ i ] ); } } return obj; }; var appendDiv = function( data ){ each( data, function( i, n ){ var div = document.createElement( 'div' ); div.innerHTML = n; document.body.appendChild( div ); }); }; appendDiv( [ 1, 2, 3, 4, 5, 6 ] ); appendDiv({a:1,b:2,c:3,d:4} );
【單例模式】
下麵是一段代碼
var createLoginLayer = (function(){ var div; return function(){ if ( !div ){ div = document.createElement( 'div' ); div.innerHTML = '我是登錄浮窗'; div.style.display = 'none'; document.body.appendChild( div ); } return div; } })();
現在把管理單例的職責和創建登錄浮窗的職責分別封裝在兩個方法里,這兩個方法可以獨立變化而互不影響,當它們連接在一起的時候,就完成了創建唯一登錄浮窗的功能,下麵的代碼顯然是更好的做法:
var getSingle = function( fn ){ // 獲取單例 var result; return function(){ return result || ( result = fn .apply(this, arguments ) ); } }; var createLoginLayer = function(){ // 創建登錄浮窗 var div = document.createElement( 'div' ); div.innerHTML = '我是登錄浮窗'; document.body.appendChild( div ); return div; }; var createSingleLoginLayer = getSingle( createLoginLayer ); var loginLayer1 = createSingleLoginLayer(); var loginLayer2 = createSingleLoginLayer(); alert ( loginLayer1 === loginLayer2 ); // 輸出: true
【裝飾者模式】
使用裝飾者模式時,通常讓類或者對象一開始只具有一些基礎的職責,更多的職責在代碼運行時被動態裝飾到對象上面。裝飾者模式可以為對象動態增加職責,從另一個角度來看, 這也是分離職責的一種方式
下麵把數據上報的功能單獨放在一個函數里,然後把這個函數動態裝飾到業務函數上面:
<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' ) ); }; document.getElementById( 'button' ).onclick = showLogin.after( log ); // 打開登錄浮層之後上報數據
SRP原則是所有原則中最簡單也是最難正確運用的原則之一。要明確的是,並不是所有的職責都應該一一分離。一方面,如果隨著需求的變化,有兩個職責總是同時變化,那就不必分離他們。比如在ajax請求的時候,創建xhr對象和發送xhr請求幾乎總是在一起的,那麼創建xhr對象的職責和發送xhr請求的職責就沒有必要分開。另一方面,職責的變化軸線僅當它們確定會發生變化時才具有意義,即使兩個職責已經被耦合在一起,但它們還沒有發生改變的徵兆,那麼也許沒有必要主動分離它們,在代碼需要重構的時候再進行分離也不遲
在人的常規思維中,總是習慣性地把一組相關的行為放到一起,如何正確地分離職責不是一件容易的事情。在實際開發中,因為種種原因違反SRP的情況並不少見。比如jQuery的attr等方法,就是明顯違反SRP原則的做法。jQuery的attr是個非常龐大的方法,既負責賦值,又負責取值,這對於jQuery的維護者來說,會帶來一些困難,但對於jQuery的用戶來說,卻簡化了用戶的使用。在方便性與穩定性之間要有一些取捨。具體是選擇方便性還是穩定性,並沒有標準答案,而是要取決於具體的應用環境
SRP原則的優點是降低了單個類或者對象的複雜度,按照職責把對象分解成更小的粒度,這有助於代碼的復用,也有利於進行單元測試。當一個職責需要變更的時候,不會影響到其他的職責。但SRP原則也有一些缺點,最明顯的是會增加編寫代碼的複雜度。當按照職責把對象分解成更小的粒度之後,實際上也增大了這些對象之間相互聯繫的難度
最少知識原則
最少知識原則(LKP)說的是一個軟體實體應當儘可能少地與其他實體發生相互作用。這裡的軟體實體是一個廣義的概念,不僅包括對象,還包括系統、類、模塊、函數、變數等
某軍隊中的將軍需要挖掘一些散兵坑。下麵是完成任務的一種方式:將軍可以通知上校讓他叫來少校,然後讓少校找來上尉,並讓上尉通知一個軍士,最後軍士喚來一個士兵,然後命令士兵挖掘一些散兵坑。這種方式十分荒謬,不是嗎?不過,還是先來看一下這個過程的等價代碼:
gerneral.getColonel(c).getMajor(m).getCaptain(c).getSergeant(s).getPrivate(p).digFoxhole();
讓代碼通過這麼長的消息鏈才能完成一個任務,這就像讓將軍通過那麼多繁瑣的步驟才能命令別人挖掘散兵坑一樣荒謬!而且,這條鏈中任何一個對象的改動都會影響整條鏈的結果。最有可能的是,將軍自己根本就不會考慮挖散兵坑這樣的細節信息。但是如果將軍真的考慮了這個問題的話,他一定會通知某個軍官:“我不關心這個工作如何完成,但是你得命令人去挖散兵坑。”
單一職責原則指導我們把對象劃分成較小的粒度,這可以提高對象的可復用性。但越來越多的對象之間可能會產生錯綜複雜的聯繫,如果修改了其中一個對象,很可能會影響到跟它相互引用的其他對象。對象和對象耦合在一起,有可能會降低它們的可復用性。
最少知識原則要求我們在設計程式時,應當儘量減少對象之間的交互。如果兩個對象之間不必彼此直接通信,那麼這兩個對象就不要發生直接的相互聯繫。常見的做法是引入一個第三者對象,來承擔這些對象之間的通信作用。如果一些對象需要向另一些對象發起請求,可以通過第三者對象來轉發這些請求
最少知識原則在設計模式中體現得最多的地方是中介者模式和外觀模式
【中介者模式】
在世界盃期間購買足球彩票,如果沒有博彩公司作為中介,上千萬的人一起計算賠率和輸贏絕對是不可能的事情。博彩公司作為中介,每個人都只和博彩公司發生關聯,博彩公司會根據所有人的投註情況計算好賠率,彩民們贏了錢就從博彩公司拿,輸了錢就賠給博彩公司。中介者模式很好地體現了最少知識原則。通過增加一個中介者對象,讓所有的相關對象都通過中介者對象來通信,而不是互相引用。所以,當一個對象發生改變時,只需要通知中介者對象即可
【外觀模式】
外觀模式主要是為子系統中的一組介面提供一個一致的界面,外觀模式定義了一個高層介面,這個介面使子系統更加容易使用
外觀模式的作用是對客戶屏蔽一組子系統的複雜性。外觀模式對客戶提供一個簡單易用的高層介面,高層介面會把客戶的請求轉發給子系統來完成具體的功能實現。大多數客戶都可以通過請求外觀介面來達到訪問子系統的目的。但在一段使用了外觀模式的程式中,請求外觀並不是強制的。如果外觀不能滿足客戶的個性化需求,那麼客戶也可以選擇越過外觀來直接訪問子系統
拿全自動洗衣機的一鍵洗衣按鈕舉例,這個一鍵洗衣按鈕就是一個外觀。如果是老式洗衣機,客戶要手動選擇浸泡、洗衣、漂洗、脫水這4個步驟。如果這種洗衣機被淘汰了,新式洗衣機的漂洗方式發生了改變,那還得學習新的漂洗方式。而全自動洗衣機的好處很明顯,不管洗衣機內部如何進化,客戶要操作的,始終只是一個一鍵洗衣的按鈕。這個按鈕就是為一組子系統所創建的外觀。但如果一鍵洗衣程式設定的預設漂洗時間是20分鐘,而客戶希望這個漂洗時間是30分鐘,那麼客戶自然可以選擇越過一鍵洗衣程式,自己手動來控制這些“子系統”運轉。外觀模式容易跟普通的封裝實現混淆。這兩者都封裝了一些事物,但外觀模式的關鍵是定義一個高層介面去封裝一組“子系統”。子系統在C++或者Java中指的是一組類的集合,這些類相互協作可以組成系統中一個相對獨立的部分。在javascript中通常不會過多地考慮“類”,如果將外觀模式映射到javascript中,這個子系統至少應該指的是一組函數的集合
最簡單的外觀模式應該是類似下麵的代碼:
var A = function(){ a1(); a2(); } var B = function(){ b1(); b2(); } var facade =function(){ A(); B(); } facade();
許多javascript設計模式的圖書或者文章喜歡把jQuery的$.ajax函數當作外觀模式的實現,這是不合適的。如果$.ajax函數屬於外觀模式,那幾乎所有的函數都可以被稱為“外觀模式”。問題是根本沒有辦法越過$.ajax“外觀”去直接使用該函數中的某一段語句
現在再來看看外觀模式和最少知識原則之間的關係。外觀模式的作用主要有兩點
1、為一組子系統提供一個簡單便利的訪問入口
2、隔離客戶與複雜子系統之間的聯繫,客戶不用去瞭解子系統的細節。從第二點來,外觀模式是符合最少知識原則的
封裝在很大程度上表達的是數據的隱藏。一個模塊或者對象可以將內部的數據或者實現細節隱藏起來,只暴露必要的介面API供外界訪問。對象之間難免產生聯繫,當一個對象必須引用另外一個對象的時候,可以讓對象只暴露必要的介面,讓對象之間的聯繫限制在最小的範圍之內。同時,封裝也用來限制變數的作用域。在javascript中對變數作用域的規定是:
1、變數在全局聲明,或者在代碼的任何位置隱式申明(不用var),則該變數在全局可見;
2、變數在函數內顯式申明(使用var),則在函數內可見。把變數的可見性限制在一個儘可能小的範圍內,這個變數對其他不相關模塊的影響就越小,變數被改寫和發生衝突的機會也越小。這也是廣義的最少知識原則的一種體現
假設要編寫一個具有緩存效果的計算乘積的函數function mult(){},需要一個對象var cache = {}來保存已經計算過的結果。cache對象顯然只對mult有用,把cache對象放在mult形成的閉包中,顯然比把它放在全局作用域更加合適,代碼如下:
var mult = (function(){ var cache = {}; return function(){ var args = Array.prototype.join.call( arguments, ',' ); if ( cache[ args ] ){ return cache[ args ]; } var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return cache[ args ] = a; } })(); mult( 1, 2, 3 ); // 輸出: 6
雖然遵守最小知識原則減少了對象之間的依賴,但也有可能增加一些龐大到難以維護的第三者對象。跟單一職責原則一樣,在實際開發中,是否選擇讓代碼符合最少知識原則,要根據具體的環境來定
開放封閉原則
在面向對象的程式設計中,開放——封閉原則(OCP)是最重要的一條原則。很多時候,一個程式具有良好的設計,往往說明它是符合開放——封閉原則的。開放——封閉原則的定義如下:軟體實體(類、模塊、函數)等應該是可以擴展的,但是不可修改
假設我們是一個大型Web項目的維護人員,在接手這個項目時,發現它已經擁有10萬行以上的javascript代碼和數百個JS文件。不久後接到了一個新的需求,即在window.onload函數中列印出頁面中的所有節點數量。打開文本編輯器,搜索出window.onload函數在文件中的位置,在函數內部添加以下代碼:
window.onload=function(){ //原有代碼略 console.log(document.getElementsByTagName('*').length); };
在項目需求變遷的過程中,經常會找到相關代碼,然後改寫它們。這似乎是理所當然的事情,不改動代碼怎麼滿足新的需求呢?想要擴展一個模塊,最常用的方式當然是修改它的源代碼。如果一個模塊不允許修改,那麼它的行為常常是固定的。然而,改動代碼是一種危險的行為,也許都遇到過bug越改越多的場景。剛剛改好了一個bug,但是又在不知不覺中引發了其他的bug
如果目前的window.onload函數是一個擁有500行代碼的巨型函數,裡面密佈著各種變數和交叉的業務邏輯,而需求又不僅僅是列印一個log這麼簡單。那麼“改好一個bug,引發其他bug”這樣的事情就很可能會發生。永遠不知道剛剛的改動會有什麼副作用,很可能會引發一系列的連鎖反應
那麼,有沒有辦法在不修改代碼的情況下,就能滿足新需求呢?通過增加代碼,而不是修改代碼的方式,來給window.onload函數添加新的功能,代碼如下:
Function.prototype.after = function( afterfn ){ var __self = this; return function(){ var ret = __self.apply( this, arguments ); afterfn.apply( this, arguments ); return ret; } }; window.onload = ( window.onload || function(){} ).after(function(){ console.log( document.getElementsByTagName( '*' ).length ); });
通過動態裝飾函數的方式,完全不用理會從前window.onload函數的內部實現,無論它的實現優雅或是醜陋。就算作為維護者,拿到的是一份混淆壓縮過的代碼也沒有關係。只要它從前是個穩定運行的函數,那麼以後也不會因為我們的新增需求而產生錯誤。新增的代碼和原有的代碼可以井水不犯河水
現在引出開放——封閉原則的思想:當需要改變一個程式的功能或者給這個程式增加新功能的時候,可以使用增加代碼的方式,但是不允許改動程式的源代碼
過多的條件分支語句是造成程式違反開放——封閉原則的一個常見原因。每當需要增加一個新的if語句時,都要被迫改動原函數。把if換成switch-case是沒有用的,這是一種換湯不換藥的做法。實際上,每當看到一大片的if或者swtich-case語句時,第一時間就應該考慮,能否利用對象的多態性來重構它們
利用對象的多態性來讓程式遵守開放——封閉原則,是一個常用的技巧。下麵先提供一段不符合開放——封閉原則的代碼。每當增加一種新的動物時,都需要改動makeSound函數的內部實現:
var makeSound = function( animal ){ if ( animal instanceof Duck ){ console.log( '嘎嘎嘎' ); }else if ( animal instanceof Chicken ){ console.log( '咯咯咯' ); } }; var Duck = function(){}; var Chicken = function(){}; makeSound( new Duck() ); // 輸出:嘎嘎嘎 makeSound( new Chicken() ); // 輸出:咯咯咯
動物世界里增加一隻狗之後,makeSound函數必須改成:
var makeSound = function( animal ){ if ( animal instanceof Duck ){ console.log( '嘎嘎嘎' ); }else if ( animal instanceof Chicken ){ console.log( '咯咯咯' ); }else if ( animal instanceof Dog ){ // 增加跟狗叫聲相關的代碼 console.log('汪汪汪' ); } }; var Dog = function(){}; makeSound( new Dog() ); // 增加一隻狗
利用多態的思想,把程式中不變的部分隔離出來(動物都會叫),然後把可變的部分封裝起來(不同類型的動物發出不同的叫聲),這樣一來程式就具有了可擴展性。想讓一隻狗發出叫聲時,只需增加一段代碼即可,而不用去改動原有的makeSound函數:
var makeSound = function( animal ){ animal.sound(); }; var Duck = function(){}; Duck.prototype.sound = function(){ console.log( '嘎嘎嘎' ); }; var Chicken = function(){}; Chicken.prototype.sound = function(){ console.log( '咯咯咯' ); }; makeSound( new Duck() ); // 嘎嘎嘎 makeSound( new Chicken() ); // 咯咯咯 /********* 增加動物狗,不用改動原有的makeSound 函數 ****************/ var Dog = function(){}; Dog.prototype.sound = function(){ console.log( '汪汪汪' ); }; makeSound( new Dog() ); // 汪汪汪
遵守開放——封閉原則的規律,最明顯的就是找出程式中將要發生變化的地方,然後把變化封裝起來。通過封裝變化的方式,可以把系統中穩定不變的部分和容易變化的部分隔離開來。在系統的演變過程中,只需要替換那些容易變化的部分,如果這些部分是已經被封裝好的,那麼替換起來也相對容易。而變化部分之外的就是穩定的部分。在系統的演變過程中,穩定的部分是不需要改變的
由於每種動物的叫聲都不同,所以動物具體怎麼叫是可變的,於是把動物具體怎麼叫的邏輯從makeSound函數中分離出來。而動物都會叫這是不變的,makeSound函數里的實現邏輯只跟動物都會叫有關,這樣一來,makeSound就成了一個穩定和封閉的函數。除了利用對象的多態性之外,還有其他方式可以幫助編寫遵守開放——封閉原則的代碼
【放置掛鉤】
放置掛鉤(hook)也是分離變化的一種方式。在程式有可能發生變化的地方放置一個掛鉤,掛鉤的返回結果決定了程式的下一步走向。這樣一來,原本的代碼執行路徑上就出現了一個分叉路口,程式未來的執行方向被預埋下多種可能性。
由於子類的數量是無限制的,總會有一些“個性化”的子類迫使不得不去改變已經封裝好的演算法骨架。於是可以在父類中的某個容易變化的地方放置掛鉤,掛鉤的返回結果由具體子類決定。這樣一來,程式就擁有了變化的可能
【使用回調函數】
在javascript中,函數可以作為參數傳遞給另外一個函數,這是高階函數的意義之一。在這種情況下,通常會把這個函數稱為回調函數。在javascript版本的設計模式中,策略模式和命令模式等都可以用回調函數輕鬆實現
回調函數是一種特殊的掛鉤。可以把一部分易於變化的邏輯封裝在回調函數里,然後把回調函數當作參數傳入一個穩定和封閉的函數中。當回調函數被執行的時候,程式就可以因為回調函數的內部邏輯不同,而產生不同的結果
比如,通過ajax非同步請求用戶信息之後要做一些事情,請求用戶信息的過程是不變的,而獲取到用戶信息之後要做什麼事情,則是可能變化的:
var getUserInfo = function( callback ){ $.ajax( 'http:// xxx.com/getUserInfo', callback ); }; getUserInfo( function( data ){ console.log( data.userName ); }); getUserInfo( function( data ){ console.log( data.userId ); });
另外一個例子是關於Array.prototype.map的。在不支持Array.prototype.map的瀏覽器中,可以簡單地模擬實現一個map函數
arrayMap函數的作用是把一個數組“映射”為另外一個數組。映射的步驟是不變的,而映射的規則是可變的,於是把這部分規則放在回調函數中,傳入arrayMap函數:
var arrayMap = function( ary, callback ){ var i = 0, length = ary.length, value, ret = []; for ( ; i < length; i++ ){ value = callback( i, ary[ i ] ); ret.push( value ); } return ret; } var a = arrayMap( [ 1, 2, 3 ], function( i, n ){ return n * 2; }); var b = arrayMap( [ 1, 2, 3 ], function( i, n ){ return n * 3; }); console.log( a ); // 輸出:[ 2, 4, 6 ] console.log( b ); // 輸出:[ 3, 6, 9 ]
有一種說法是,設計模式就是給做的好的設計取個名字。幾乎所有的設計模式都是遵守開放——封閉原則的。不管是具體的各種設計模式,還是更抽象的面向對象設計原則,比如單一職責原則、最少知識原則、依賴倒置原則等,都是為了讓程式遵守開放——封閉原則而出現的。可以這樣說,開放——封閉原則是編寫一個好程式的目標,其他設計原則都是達到這個目標的過程
【發佈——訂閱模式】
發佈——訂閱模式用來降低多個對象之間的依賴關係,它可以取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另外一個對象的某個介面。當有新的訂閱者出現時,發佈者的代碼不需要進行任何修改;同樣當發佈者需要改變時,也不會影響到之前的訂閱者
【模板方法模式】
模板方法模式是一種典型的通過封裝變化來提高系統擴展性的設計模式。在一個運用了模板方法模式的程式中,子類的方法種類和執行順序都是不變的,所以把這部分邏輯抽出來放到父類的模板方法裡面;而子類的方法具體怎麼實現則是可變的,於是把這部分變化的邏輯封裝到子類中。通過增加新的子類,便能給系統增加新的功能,並不需要改動抽象父類以及其他的子類,這也是符合開放——封閉原則的
【策略模式】
策略模式和模板方法模式是一對競爭者。在大多數情況下,它們可以相互替換使用。模板方法模式基於繼承的思想,而策略模式則偏重於組合和委托。策略模式將各種演算法都封裝成單獨的策略類,這些策略類可以被交換使用。策略和使用策略的客戶代碼可以分別獨立進行修改而互不影響。增加一個新的策略類也非常方便,完全不用修改之前的代碼
【代理模式】
拿預載入圖片舉例,現在已有一個給圖片設置src的函數myImage,想為它增加圖片預載入功能時,一種做法是改動myImage函數內部的代碼,更好的做法是提供一個代理函數proxyMyImage,代理函數負責圖片預載入,在圖片預載入完成之後,再將請求轉交給原來的myImage函數,myImage在這個過程中不需要任何改動。預載入圖片的功能和給圖片設置src的功能被隔離在兩個函數里,它們可以單獨改變而互不影響。myImage不知曉代理的存在,它可以繼續專註於自己的職責——給圖片設置src
【職責鏈模式】
把一個巨大的訂單函數分別拆成了500元訂單、200元訂單以及普通訂單的3個函數。這3個函數通過職責鏈連接在一起,客戶的請求會在這條鏈條裡面依次傳遞:
var order500yuan = new Chain(function( orderType, pay, stock ){ // 具體代碼略 }); var order200yuan = new Chain(function( orderType, pay, stock ){ // 具體代碼略 }); var orderNormal = new Chain(function( orderType, pay, stock ){ // 具體代碼略 }); order500yuan.setNextSuccessor( order200yuan ).setNextSuccessor( orderNormal ); order500yuan.passRequest( 1, true, 10 ); // 500 元定金預購,得到 100 優惠券
可以看到,當增加一個新類型的訂單函數時,不需要改動原有的訂單函數代碼,只需要在鏈條中增加一個新的節點
在職責鏈模式代碼中,開放——封閉原則要求只能通過增加源代碼的方式擴展程式的功能,而不允許修改源代碼。那往職責鏈中增加一個新的100元訂單函數節點時,不也必須改動設置鏈條的代碼嗎?代碼如下:
order500yuan.setNextSuccessor(order200yuan).setNextSuccessor(orderNormal);
變為:
order500yuan.setNextSuccessor(order200yuan).setNextSuccessor(order100yuan).setNextSuccessor(orderNormal);
實際上,讓程式保持完全封閉是不容易做到的。就算技術上做得到,也需要花費太多的時間和精力。而且讓程式符合開放——封閉原則的代價是引入更多的抽象層次,更多的抽象有可能會增大代碼的複雜度。更何況,有一些代碼是無論如何也不能完全封閉的,總會存在一些無法對其封閉的變化
作為程式員,可以做到的有下麵兩點
1、挑選出最容易發生變化的地方,然後構造抽象來封閉這些變化
2、在不可避免發生修改的時候,儘量修改那些相對容易修改的地方。拿一個開源庫來說,修改它提供的配置文件,總比修改它的源代碼來得簡單