第一章.設計原則-----單一職責原則

單一職責原則

就一個類來說,應該僅有一個引起它變化的原因。在 JavaScript 中,單一職責原則更多地是被運用在對象或者方法級別上。

如果我們有兩個動機去改寫一 個方法,那麼這個方法就具有兩個職責。每個職責都是變化,需求的變遷過程中,需要改寫這個方法的可能性就越大。此時,這個方法通常是一個不穩定的方法,修改代碼總是一件危險的事情,特別是當兩個職責耦合在一起的時候,一個職責發生變化可能會影響到其他職責的實現,造成意想不到的破壞, 這種耦合性得到的是低內聚和脆弱的設計。

SRP 原則體現爲:一個對象(方法)只做一件事情。

1. 設計模式中的SRP原則

SRP 原則在很多設計模式中都有着廣泛的運用,例如代理模式、迭代器模式、單例模式和裝 飾者模式。這些模式我們會在以後進行詳細的講解。

1.1. 代理模式

我們使用圖片預加載的例子。通過增加虛擬代理的方式,把預加載圖
片的職責放到代理對象中,而本體僅僅負責往頁面中添加 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:// /C:/Users/svenzeng/Desktop/loading.gif');
        img.src = src;
      }
    }
  })();
  proxyImage.setSrc('http:// imgcache.qq.com/music/photo/000GGDys0yA0Nk.jpg');

把添加 img 標籤的功能和預加載圖片的職責分開放到兩個對象中,這兩個對象各自都只有一
個被修改的動機。在它們各自發生改變的時候,也不會影響另外的對象。

1.2. 迭代器模式

我們有這樣一段代碼,先遍歷一個集合,然後往頁面中添加一些 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 ( var i in data )的方式,這時候必須去修改 appendDiv 裏的 代碼,否則因爲遍歷方式的改變,導致不能順利往頁面中添加 div 節點。

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

當把迭代聚合對象的職責單獨封裝在 each 函數中後,即使以後還要增加新的迭代方式,我
們只需要修改 each 函數即可,appendDiv 函數不會受到牽連,代碼如下

var each = function (obj, callback) {
      var value,
        i = 0,
        length = obj.length,
        isArray = isArraylike(obj);    // isArraylike 函數未實現,可以翻閱 jQuery 源代碼
      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 });

1.3. 單例模式

有一個惰性單例,最開始的代碼是這樣的:

 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

1.4. 裝飾者模式

使用裝飾者模式的時候,我們通常讓類或者對象一開始只具有一些基礎的職責,更多的職責 在代碼運行時被動態裝飾到對象上面。裝飾者模式可以爲對象動態增加職責,從另一個角度來看, 這也是分離職責的一種方式。

我們把數據上報的功能單獨放在一個函數裏,然後把這個函數動態裝飾到業務函數上面:

<html>

  <body>
    <button tag="login" id="button">點擊打開登錄浮層</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('上報標籤爲: ' + this.getAttribute('tag')); 
    };
    document.getElementById('button').onclick = showLogin.after(log); // 打開登錄浮層之後上報數據
  </script>

  </html>

2 何時應該分離職責

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

要明確的是,並不是所有的職責都應該一一分離。

一方面,如果隨着需求的變化,有兩個職責總是同時變化,那就不必分離他們。比如在 ajax 請求的時候,創建 xhr 對象和發送 xhr 請求幾乎總是在一起的,那麼創建 xhr 對象的職責和發送 xhr 請求的職責就沒有必要分開。

另一方面,兩個職責已經被耦 合在一起,但它們還沒有發生改變的徵兆,那麼也許沒有必要主動分離它們,在代碼需要重構的時候再進行分離也不遲。

3 違反SRP原則

在人的常規思維中,總是習慣性地把一組相關的行爲放到一起,如何正確地分離職責不是一
件容易的事情。

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

一方面,我們受設計原則的指導,另一方面,我們未必要在任何時候都一成不變地遵守原則。 在實際開發中,因爲種種原因違反 SRP 的情況並不少見。比如 jQuery 的 attr 等方法,就是明顯 違反 SRP 原則的做法。jQuery 的 attr 是個非常龐大的方法,既負責賦值,又負責取值,這對於 jQuery 的維護者來說,會帶來一些困難,但對於 jQuery 的用戶來說,卻簡化了用戶的使用。

在方便性與穩定性之間要有一些取捨。具體是選擇方便性還是穩定性,並沒有標準答案,而 是要取決於具體的應用環境。比如如果一個電視機內置了 DVD 機,當電視機壞了的時候,DVD 機也沒法正常使用,那麼一個 DVD 發燒友通常不會選擇這樣的電視機。但如果我們的客廳本來 就小得誇張,或者更在意 DVD 在使用上的方便,那讓電視機和 DVD 機耦合在一起就是更好的 選擇。

4 SRP 原則的優缺點

優點 :
是降低了單個類或者對象的複雜度,按照職責把對象分解成更小的粒度, 這有助於代碼的複用,也有利於進行單元測試。當一個職責需要變更的時候,不會影響到其他 的職責。

缺點 :
最明顯的是會增加編寫代碼的複雜度。當我們按照職責把對象 分解成更小的粒度之後,實際上也增大了這些對象之間相互聯繫的難度。

參考資料

JavaScript設計模式與開發實踐----曾探

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章