設(shè)計(jì)原則之單一職責(zé)原則

就一個(gè)類而言,應(yīng)該僅有一個(gè)引起它變化的原因。在JavaScript中,需要用到類的場景并不太多,單一職責(zé)原則更多地是被運(yùn)用在對象或者方法級別上,因此本節(jié)我們的討論大多基于對象和方法。

單一職責(zé)原則(SRP)的職責(zé)被定義為“引起變化的原因”。如果我們有兩個(gè)動機(jī)去改寫一個(gè)方法,那么這個(gè)方法就具有兩個(gè)職責(zé)。每個(gè)職責(zé)都是變化的一個(gè)軸線,如果一個(gè)方法承擔(dān)了過多的職責(zé),那么在需求的變遷過程中,需要改寫這個(gè)方法的可能性就越大。

此時(shí),這個(gè)方法通常是一個(gè)不穩(wěn)定的方法,修改代碼總是一件危險(xiǎn)的事情,特別是當(dāng)兩個(gè)職責(zé)耦合在一起的時(shí)候,一個(gè)職責(zé)發(fā)生變化可能會影響到其他職責(zé)的實(shí)現(xiàn),造成意想不到的破壞,這種耦合性得到的是低內(nèi)聚和脆弱的設(shè)計(jì)。

因此,SRP原則體現(xiàn)為:一個(gè)對象(方法)只做一件事情。

設(shè)計(jì)模式中的SRP原則

SRP原則在很多設(shè)計(jì)模式中都有著廣泛的運(yùn)用,例如代理模式、迭代器模式、單例模式和裝飾者模式。

1. 代理模式

我們在之前已經(jīng)見過這個(gè)圖片預(yù)加載的例子了。通過增加虛擬代理的方式,把預(yù)加載圖片的職責(zé)放到代理對象中,而本體僅僅負(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ù)加載完成之后把請求交給本體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' );

把添加img標(biāo)簽的功能和預(yù)加載圖片的職責(zé)分開放到兩個(gè)對象中,這兩個(gè)對象各自都只有一個(gè)被修改的動機(jī)。在它們各自發(fā)生改變的時(shí)候,也不會影響另外的對象。

2. 迭代器模式

我們有這樣一段代碼,先遍歷一個(gè)集合,然后往頁面中添加一些div,這些div的innerHTML分別對應(yīng)集合里的元素:

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 ] );

這其實(shí)是一段很常見的代碼,經(jīng)常用于ajax請求之后,在回調(diào)函數(shù)中遍歷ajax請求返回的數(shù)據(jù),然后在頁面中渲染節(jié)點(diǎn)。

appendDiv函數(shù)本來只是負(fù)責(zé)渲染數(shù)據(jù),但是在這里它還承擔(dān)了遍歷聚合對象data的職責(zé)。我們想象一下,如果有一天cgi返回的data數(shù)據(jù)格式從array變成了object,那我們遍歷data的代碼就會出現(xiàn)問題,必須改成 for ( var i in data )的方式,這時(shí)候必須去修改appendDiv里的代碼,否則因?yàn)楸闅v方式的改變,導(dǎo)致不能順利往頁面中添加div節(jié)點(diǎn)。

我們有必要把遍歷data的職責(zé)提取出來,這正是迭代器模式的意義,迭代器模式提供了一種方法來訪問聚合對象,而不用暴露這個(gè)對象的內(nèi)部表示。

當(dāng)把迭代聚合對象的職責(zé)單獨(dú)封裝在each函數(shù)中后,即使以后還要增加新的迭代方式,我們只需要修改each函數(shù)即可,appendDiv函數(shù)不會受到牽連,代碼如下:

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對象
              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} );

3. 單例模式

之前曾實(shí)現(xiàn)過一個(gè)惰性單例,最開始的代碼是這樣的:

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;
    }
})();

現(xiàn)在我們把管理單例的職責(zé)和創(chuàng)建登錄浮窗的職責(zé)分別封裝在兩個(gè)方法里,這兩個(gè)方法可以獨(dú)立變化而互不影響,當(dāng)它們連接在一起的時(shí)候,就完成了創(chuàng)建唯一登錄浮窗的功能,下面的代碼顯然是更好的做法:

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

4. 裝飾者模式

使用裝飾者模式的時(shí)候,我們通常讓類或者對象一開始只具有一些基礎(chǔ)的職責(zé),更多的職責(zé)在代碼運(yùn)行時(shí)被動態(tài)裝飾到對象上面。裝飾者模式可以為對象動態(tài)增加職責(zé),從另一個(gè)角度來看,這也是分離職責(zé)的一種方式。

下面是之前曾提到的例子,我們把數(shù)據(jù)上報(bào)的功能單獨(dú)放在一個(gè)函數(shù)里,然后把這個(gè)函數(shù)動態(tài)裝飾到業(yè)務(wù)函數(shù)上面:

<html>
    <body>
        <button tag="login" id="button">點(diǎn)擊打開登錄浮層</button>
    </body>

<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( '上報(bào)標(biāo)簽為: ' + this.getAttribute( 'tag' ) );
};

document.getElementById( 'button' ).onclick = showLogin.after( log );
    // 打開登錄浮層之后上報(bào)數(shù)據(jù)

</script>
</html>

SRP原則的應(yīng)用難點(diǎn)就是如何去分離職責(zé),下面我們將開始討論這點(diǎn)。

何時(shí)應(yīng)該分離職責(zé)

SRP原則是所有原則中最簡單也是最難正確運(yùn)用的原則之一。

要明確的是,并不是所有的職責(zé)都應(yīng)該一一分離。

一方面,如果隨著需求的變化,有兩個(gè)職責(zé)總是同時(shí)變化,那就不必分離他們。比如在ajax請求的時(shí)候,創(chuàng)建xhr對象和發(fā)送xhr請求幾乎總是在一起的,那么創(chuàng)建xhr對象的職責(zé)和發(fā)送xhr請求的職責(zé)就沒有必要分開。

另一方面,職責(zé)的變化軸線僅當(dāng)它們確定會發(fā)生變化時(shí)才具有意義,即使兩個(gè)職責(zé)已經(jīng)被耦合在一起,但它們還沒有發(fā)生改變的征兆,那么也許沒有必要主動分離它們,在代碼需要重構(gòu)的時(shí)候再進(jìn)行分離也不遲。

違反SRP原則

在人的常規(guī)思維中,總是習(xí)慣性地把一組相關(guān)的行為放到一起,如何正確地分離職責(zé)不是一件容易的事情。

我們也許從來沒有考慮過如何分離職責(zé),但這并不妨礙我們編寫代碼完成需求。對于SRP原則,許多專家委婉地表示“This is sometimes hard to see.”。

一方面,我們受設(shè)計(jì)原則的指導(dǎo),另一方面,我們未必要在任何時(shí)候都一成不變地遵守原則。在實(shí)際開發(fā)中,因?yàn)榉N種原因違反SRP的情況并不少見。比如jQuery的attr等方法,就是明顯違反SRP原則的做法。jQuery的attr是個(gè)非常龐大的方法,既負(fù)責(zé)賦值,又負(fù)責(zé)取值,這對于jQuery的維護(hù)者來說,會帶來一些困難,但對于jQuery的用戶來說,卻簡化了用戶的使用。

在方便性與穩(wěn)定性之間要有一些取舍。具體是選擇方便性還是穩(wěn)定性,并沒有標(biāo)準(zhǔn)答案,而是要取決于具體的應(yīng)用環(huán)境。比如如果一個(gè)電視機(jī)內(nèi)置了DVD機(jī),當(dāng)電視機(jī)壞了的時(shí)候,DVD機(jī)也沒法正常使用,那么一個(gè)DVD發(fā)燒友通常不會選擇這樣的電視機(jī)。但如果我們的客廳本來就小得夸張,或者更在意DVD在使用上的方便,那讓電視機(jī)和DVD機(jī)耦合在一起就是更好的選擇。

SRP原則的優(yōu)缺點(diǎn)

SRP原則的優(yōu)點(diǎn)是降低了單個(gè)類或者對象的復(fù)雜度,按照職責(zé)把對象分解成更小的粒度,這有助于代碼的復(fù)用,也有利于進(jìn)行單元測試。當(dāng)一個(gè)職責(zé)需要變更的時(shí)候,不會影響到其他的職責(zé)。

但SRP原則也有一些缺點(diǎn),最明顯的是會增加編寫代碼的復(fù)雜度。當(dāng)我們按照職責(zé)把對象分解成更小的粒度之后,實(shí)際上也增大了這些對象之間相互聯(lián)系的難度。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容