三. 設(shè)計(jì)原則和編程技巧
3.1 單一職責(zé)原則(SRP)
SRP 原則體現(xiàn)為:一個(gè)對(duì)象(方法)只做一件事情纷纫;
單一職責(zé)原則指的是,就一個(gè)類(通常也包括對(duì)象和函數(shù)等)而言三妈,應(yīng)該僅有一個(gè)引起它變化的原因。如果一個(gè)對(duì)象承擔(dān)了多項(xiàng)職責(zé)莫绣,就意味著這個(gè)對(duì)象將變得巨大畴蒲,引起它變化的原因可能會(huì)有多個(gè)。面向?qū)ο笤O(shè)計(jì)鼓勵(lì)將行為分布到細(xì)粒度的對(duì)象之中对室,如果一個(gè)對(duì)象承擔(dān)的職責(zé)過多模燥,等于把這些職責(zé)耦合到了一起,這種耦合會(huì)導(dǎo)致脆弱和低內(nèi)聚的設(shè)計(jì)掩宜。當(dāng)變化發(fā)生時(shí)蔫骂,設(shè)計(jì)可能會(huì)遭到意外的破壞;
3.1.1 設(shè)計(jì)模式中的 SRP 原則
SRP 原則在很多設(shè)計(jì)模式中都有著廣泛的運(yùn)用牺汤,例如代理模式辽旋、迭代器模式、單例模式和裝飾者模式。
- 代理模式:圖片預(yù)加載示例补胚,增加虛擬代理的方式码耐,把預(yù)加載圖片的職責(zé)放到代理對(duì)象中,而本體僅僅負(fù)責(zé)往頁面中添加 img 標(biāo)簽溶其,這也是它最原始的職責(zé)骚腥;
// myImage 負(fù)責(zé)往頁面中添加 img 標(biāo)簽:
var myImage = (function(){
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
return {
setSrc: function( src ){
imgNode.src = src;
}
}
})();
// proxyImage 負(fù)責(zé)預(yù)加載圖片,并在預(yù)加載完成之后把請(qǐng)求交給本體 myImage:
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/000GGDys0yA0Nk.jpg' );
- 迭代器模式瓶逃,實(shí)例:先遍歷一個(gè)集合束铭,然后往頁面中添加一些 div,這些 div 的 innerHTML 分別對(duì)應(yīng)集合里的元素厢绝;
// a. 基本實(shí)現(xiàn)
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 ] );
// b. SRP 原則實(shí)現(xiàn)
var each = function( obj, callback ) {
var value, i = 0, length = obj.length, isArray = isArraylike( obj ); // isArraylike 函數(shù)未實(shí)現(xiàn)契沫,可以翻閱 jQuery 源代碼
if ( isArray ) { // 迭代類數(shù)組
for ( ; i < length; i++ ) {
callback.call( obj[ i ], i, obj[ i ] );
}
} else {
for ( i in obj ) { // 迭代 object 對(duì)象
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} );
- 單例模式:惰性單例創(chuàng)建唯一一個(gè)登錄窗 div 的示例;
// a. 基本實(shí)現(xiàn)
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;
}
})();
// b. SRP 原則實(shí)現(xiàn)
var getSingle = function( fn ){ // 獲取單例
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
var createLoginLayer = function(){ // 創(chuàng)建登錄浮窗
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
- 裝飾者模式:裝飾函數(shù)實(shí)現(xiàn)代芜;
Function.prototype.after = function( afterfn ){
var __self = this;
return function(){
var ret = __self.apply( this, arguments );
afterfn.apply( this, arguments );
return ret;
}
};
3.1.2 何時(shí)應(yīng)該分離職責(zé)
要明確的是埠褪,并不是所有的職責(zé)都應(yīng)該一一分離。一方面挤庇,如果隨著需求的變化钞速,有兩個(gè)職責(zé)總是同時(shí)變化,那就不必分離他們嫡秕;另一方面渴语,職責(zé)的變化軸線僅當(dāng)它們確定會(huì)發(fā)生變化時(shí)才具有意義,即使兩個(gè)職責(zé)已經(jīng)被耦合在一起昆咽,但它們還沒有發(fā)生改變的征兆驾凶,那么也許沒有必要主動(dòng)分離它們,在代碼需要重構(gòu)的時(shí)候再進(jìn)行分離也不遲掷酗。
3.1.3 違反 SRP 原則
在常規(guī)思維中调违,總是習(xí)慣性地把一組相關(guān)的行為放到一起,如何正確地分離職責(zé)不是一件容易的事情泻轰。一方面技肩,我們受設(shè)計(jì)原則的指導(dǎo), 另一方面浮声, 我們未必要在任何時(shí)候都一成不變地遵守原則虚婿;在方便性與穩(wěn)定性之間要有一些取舍。具體是選擇方便性還是穩(wěn)定性泳挥,并沒有標(biāo)準(zhǔn)答案然痊,而是要取決于具體的應(yīng)用環(huán)境。
3.1.4 SRP 原則的優(yōu)缺點(diǎn)
SRP 原則的優(yōu)點(diǎn)是降低了單個(gè)類或者對(duì)象的復(fù)雜度屉符,按照職責(zé)把對(duì)象分解成更小的粒度剧浸,這有助于代碼的復(fù)用锹引,也有利于進(jìn)行單元測(cè)試。當(dāng)一個(gè)職責(zé)需要變更的時(shí)候辛蚊,不會(huì)影響到其他的職責(zé)粤蝎。但 SRP 原則也有一些缺點(diǎn),最明顯的是會(huì)增加編寫代碼的復(fù)雜度袋马。當(dāng)我們按照職責(zé)把對(duì)象分解成更小的粒度之后初澎,實(shí)際上也增大了這些對(duì)象之間相互聯(lián)系的難度。
3.2 最少知識(shí)原則(LKP)
最少知識(shí)原則( LKP):一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用虑凛;
3.2.1 減少對(duì)象之間的聯(lián)系
最少知識(shí)原則要求在設(shè)計(jì)程序時(shí)碑宴,應(yīng)當(dāng)盡量減少對(duì)象之間的交互。如果兩個(gè)對(duì)象之間不必彼此直接通信桑谍,那么這兩個(gè)對(duì)象就不要發(fā)生直接的相互聯(lián)系延柠。常見的做法是引入一個(gè)第三者對(duì)象,來承擔(dān)這些對(duì)象之間的通信作用锣披。如果一些對(duì)象需要向另一些對(duì)象發(fā)起請(qǐng)求贞间,可以通過第三者對(duì)象來轉(zhuǎn)發(fā)這些請(qǐng)求。
3.2.2 設(shè)計(jì)模式中的最少知識(shí)原則
最少知識(shí)原則在設(shè)計(jì)模式中體現(xiàn)得最多的地方是中介者模式和外觀模式雹仿,如下所示:
- 中介者模式:
如博彩公司示例增热,博彩公司作為中介,每個(gè)人都只和博彩公司發(fā)生關(guān)聯(lián)胧辽,博彩公司會(huì)根據(jù)所有人的投注情況計(jì)算好賠率峻仇,彩民們贏了錢就從博彩公司拿,輸了錢就賠給博彩公司邑商。中介者模式很好地體現(xiàn)了最少知識(shí)原則摄咆,通過增加一個(gè)中介者對(duì)象,讓所有的相關(guān)對(duì)象都通過中介者對(duì)象來通信人断,而不是互相引用吭从。所以,當(dāng)一個(gè)對(duì)象發(fā)生改變時(shí)恶迈,只需要通知中介者對(duì)象即可涩金。
- 外觀模式:主要是為子系統(tǒng)中的一組接口提供一個(gè)一致的界面,外觀模式定義了一個(gè)高層接口蝉绷,這個(gè)接口使子系統(tǒng)更加容易使用鸭廷;
外觀模式的作用是對(duì)客戶屏蔽一組子系統(tǒng)的復(fù)雜性枣抱,外觀模式對(duì)客戶提供一個(gè)簡(jiǎn)單易用的高層接口熔吗,高層接口會(huì)把客戶的請(qǐng)求轉(zhuǎn)發(fā)給子系統(tǒng)來完成具體的功能實(shí)現(xiàn)。大多數(shù)客戶都可以通過請(qǐng)求外觀接口來達(dá)到訪問子系統(tǒng)的目的佳晶,如果外觀不能滿足客戶的個(gè)性化需求桅狠,那么客戶也可以選擇越過外觀來直接訪問子系統(tǒng);
3.2.3 封裝在最少知識(shí)原則中的體現(xiàn)
封裝在很大程度上表達(dá)的是數(shù)據(jù)的隱藏。一個(gè)模塊或者對(duì)象可以將內(nèi)部的數(shù)據(jù)或者實(shí)現(xiàn)細(xì)節(jié)隱藏起來中跌,只暴露必要的接口 API 供外界訪問咨堤。對(duì)象之間難免產(chǎn)生聯(lián)系,當(dāng)一個(gè)對(duì)象必須引用另外一個(gè)對(duì)象的時(shí)候漩符,我們可以讓對(duì)象只暴露必要的接口一喘,讓對(duì)象之間的聯(lián)系限制在最小的范圍之內(nèi)。把變量的可見性限制在一個(gè)盡可能小的范圍內(nèi)嗜暴,這個(gè)變量對(duì)其他不相關(guān)模塊的影響就越小凸克,變量被改寫和發(fā)生沖突的機(jī)會(huì)也越小。這也是廣義的最少知識(shí)原的一種體現(xiàn)闷沥。
實(shí)例:具有緩存效果的計(jì)算乘積的函數(shù)萎战;
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
3.3 開放封閉原則(OCP)
開放封閉原則( OCP):軟件實(shí)體(類、模塊舆逃、函數(shù))等應(yīng)該是可以擴(kuò)展的蚂维,但是不可修改;
開放封閉原則的思想:當(dāng)需要改變一個(gè)程序的功能或者給這個(gè)程序增加新功能的時(shí)候路狮,可以使用增加代碼的方式虫啥,但是不允許改動(dòng)程序的源代碼。
3.3.1 擴(kuò)展 window.onload 函數(shù)
通過增加代碼览祖,而不是修改原代碼的方式孝鹊,來給 window.onload
函數(shù)添加新的功能,代碼如下:
// 使用裝飾函數(shù)實(shí)現(xiàn)函數(shù)功能擴(kuò)展
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 );
});
3.3.2 用對(duì)象的多態(tài)性消除條件分支
過多的條件分支語句是造成程序違反開放封閉原則的一個(gè)常見原因展蒂,每當(dāng)需要增加一個(gè)新的 if 語句時(shí)又活,都要被迫改動(dòng)原函數(shù)。因此當(dāng)一大片的 if-else
或者 swtich-case
語句時(shí)锰悼,第一時(shí)間考慮能否利用對(duì)象的多態(tài)性來重構(gòu)代碼柳骄,例如讓動(dòng)物發(fā)出叫聲的例子,每增加一種動(dòng)物箕般,就需要改動(dòng) makeSound
函數(shù)的內(nèi)部實(shí)現(xiàn):
// a. 使用 if-else 基本實(shí)現(xiàn)
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() ); // 輸出:咯咯咯
// b. 利用對(duì)象的多態(tài)性重構(gòu)代碼
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() ); // 咯咯咯
3.3.3 找出變化的地方
開放封閉原則是一個(gè)看起來比較虛幻的原則耐薯,并沒有實(shí)際的模板教導(dǎo)怎樣地實(shí)現(xiàn)它;但開發(fā)中能找到一些讓程序盡量遵守開放封閉原則的規(guī)律丝里,最明顯的就是找出程序中將要發(fā)生變化的地方曲初,然后把變化封裝起來。通過封裝變化的方式杯聚,可以把系統(tǒng)中穩(wěn)定不變的部分和容易變化的部分隔離開來臼婆,在系統(tǒng)的演變過程中,只需要替換那些容易變化的部分幌绍,而穩(wěn)定的部分是不需要改變的颁褂;
3.3.4 設(shè)計(jì)模式中的開放封閉原則
開放封閉原則在設(shè)計(jì)模式中應(yīng)用很廣泛故响,如之前的裝飾者模式示例,還有發(fā)布訂閱模式颁独、模板方法模式彩届、策略模式、代理模式誓酒、職責(zé)鏈模式樟蠕,如下所示:
- 發(fā)布訂閱模式
發(fā)布訂閱模式用來降低多個(gè)對(duì)象之間的依賴關(guān)系,它可以取代對(duì)象之間硬編碼的通知機(jī)制靠柑,一個(gè)對(duì)象不用再顯式地調(diào)用另外一個(gè)對(duì)象的某個(gè)接口坯墨。當(dāng)有新的訂閱者出現(xiàn)時(shí),發(fā)布者的代碼不需要進(jìn)行任何修改病往;同樣當(dāng)發(fā)布者需要改變時(shí)捣染,也不會(huì)影響到之前的訂閱者。
- 模板方法模式
模板方法模式是一種典型的通過封裝變化來提高系統(tǒng)擴(kuò)展性的設(shè)計(jì)模式停巷,在一個(gè)運(yùn)用了模板方法模式的程序中耍攘,子類的方法種類和執(zhí)行順序都是不變的,所以把這部分邏輯抽出來放到父類的模板方法里面畔勤;而子類的方法具體怎么實(shí)現(xiàn)則是可變的蕾各,于是把這部分變化的邏輯封裝到子類中。通過增加新的子類庆揪,便能給系統(tǒng)增加新的功能式曲,并不需要改動(dòng)抽象父類以及其他的子類,這也是符合開放封閉原則的缸榛。
- 策略模式
策略模式和模板方法模式在大多數(shù)情況下可以相互替換使用吝羞,其中模板方法模式基于繼承的思想,而策略模式則偏重于組合和委托内颗。策略模式將各種算法都封裝成單獨(dú)的策略類钧排,這些策略類可以被交換使用。策略和使用策略的客戶代碼可以分別獨(dú)立進(jìn)行修改而互不影響均澳。我們?cè)黾右粋€(gè)新的策略類也非常方便恨溜,完全不用修改之前的代碼。
- 代理模式
代理模式的圖片預(yù)加載示例中找前,代理函數(shù)負(fù)責(zé)圖片預(yù)加載糟袁,在圖片預(yù)加載完成之后,再將請(qǐng)求轉(zhuǎn)交給原來的 myImage
函數(shù)躺盛, myImage
在這個(gè)過程中不需要任何改動(dòng)项戴,預(yù)加載圖片的功能和給圖片設(shè)置 src 的功能被隔離在兩個(gè)函數(shù)里,它們可以單獨(dú)改變而互不影響颗品。 myImage
不知曉代理的存在肯尺,它可以繼續(xù)專注于自己的職責(zé)——給圖片設(shè)置 src
屬性;
- 職責(zé)鏈模式
職責(zé)鏈模式的訂單示例中躯枢,當(dāng)增加一個(gè)新類型的訂單函數(shù)時(shí)则吟,不需要改動(dòng)原有的訂單函數(shù)代碼,只需要在鏈條中增加一個(gè)新的節(jié)點(diǎn)锄蹂。
3.3.5 開放封閉原則的相對(duì)性
實(shí)際開發(fā)中氓仲,讓程序保持完全封閉是不容易做到,并且有一些代碼是無論如何也不能完全封閉的得糜,總會(huì)存在一些無法對(duì)其封閉的變化敬扛。因此我們可以做到的有下面兩點(diǎn):
- 挑選出最容易發(fā)生變化的地方,然后構(gòu)造抽象來封閉這些變化朝抖。
- 在不可避免發(fā)生修改的時(shí)候啥箭,盡量修改那些相對(duì)容易修改的地方。如一個(gè)開源庫(kù)治宣,修改它提供的配置文件急侥,總比修改它的源代碼來得簡(jiǎn)單。
系列鏈接
- JavaScript 設(shè)計(jì)模式(上)——基礎(chǔ)知識(shí)
- JavaScript 設(shè)計(jì)模式(中)——1.單例模式
- JavaScript 設(shè)計(jì)模式(中)——2.策略模式
- JavaScript 設(shè)計(jì)模式(中)——3.代理模式
- JavaScript 設(shè)計(jì)模式(中)——4.迭代器模式
- JavaScript 設(shè)計(jì)模式(中)——5.發(fā)布訂閱模式
- JavaScript 設(shè)計(jì)模式(中)——6.命令模式
- JavaScript 設(shè)計(jì)模式(中)——7.組合模式
- JavaScript 設(shè)計(jì)模式(中)——8.模板方法模式
- JavaScript 設(shè)計(jì)模式(中)——9.享元模式
- JavaScript 設(shè)計(jì)模式(中)——10.職責(zé)鏈模式
- JavaScript 設(shè)計(jì)模式(中)——11. 中介者模式
- JavaScript 設(shè)計(jì)模式(中)——12. 裝飾者模式
- JavaScript 設(shè)計(jì)模式(中)——13.狀態(tài)模式
- JavaScript 設(shè)計(jì)模式(中)——14.適配器模式
- JavaScript 設(shè)計(jì)模式(下)——設(shè)計(jì)原則
- JavaScript 設(shè)計(jì)模式練習(xí)代碼