[TOC]
# 單一職責
就一個類而言,應該僅有一個引起它變化的原因。
SRP原則體現為:一個對象(方法)只做一件事情。
## 設計模式中的SRP原則
### 代理模式
圖片預加載,通過增加虛擬代理的方式,把預加載圖片的職責放到代理對象中,而本體僅僅負責往頁面添加img標簽。
```javascript
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src){
imgNode.src = src;
}
}
})();
// 創建proxyImage
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc(this.src);
};
return {
setSrc: function(src){
myImage.setSrc('file://xxx');
img.src = src;
}
}
})();
proxyImage.setSrc('http://sxxx');
```
把添加img標簽的功能和預加載圖片的職責分開放到兩個對象中,這兩個對象各自都只有一個被修改的動機。在它們各自發生改變的時候,也不會影響另外的對象。
### 迭代器模式
```javascript
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]);
```
`appendChild`函數本來只是負責渲染數據,有必要吧遍歷data的職責提取出來。
```javascript
var appendDiv = function(data){
each(data, function(i, n){ // each 未實現
var div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div);
})
};
appendDiv([1,2,3,4,5,6]);
```
### 單例模式
```javascript
var createLoginLayer = (function(){
var div;
return function(){
if(!div){
div = docuement.createElement('div');
div.innerHTML = 'login';
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})();
```
應該要把管理單例的職責和創建登錄浮窗的職責分別封裝在兩個方法里。
```javascript
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();
```
### 裝飾者模式
通常讓類或者對象一開始只具有一些基礎的職責,更多的職責在代碼運行時被動態裝飾到對象上面。裝飾者模式可以為對象動態增加職責,從另一個角度來看,這也是分離職責的一種方式。
```javascript
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('open');
};
var log = function(){
console.log('log');
};
document.getElementById('button').onclick = showLogin.after(log);
```
## 何時應該分離職責
要明確,并不是所有的職責都應該一一分離。
如果隨著需求的變化,有兩個職責總是同時變化,那就不必分離他們。
職責的變化軸線僅當它們確定會發生變化時才具有意義,即使兩個職責已經被耦合在一起,但它們還沒有發生改變的征兆,那么也許沒有必要主動分離它們,在代碼需要重構的時候再進行分離也不遲。
## 違法SRP原則
*This is sometime hard to see*,未必要在任何時候都一成不變地遵守原則。在實際開發中,因為種種原因違法SRP的情況并不少見。在方便性與穩定性之間要有一些取舍。
## SRP原則的優缺點
優點:
- 降低了單個類或者對象的復雜度,按照職責把對象分解成更小的粒度,這有助于代碼的復用,也有利于進行單元測試。當一個職責需要變更的時候,不會影響到其它的職責。
缺點:
- 最明顯的是會增加編寫代碼的復雜度。當我們按照職責把對象分解成更小的粒度之后,實際上也增大了這些對象之間相互聯系的難度。
# 最少知識原則
最少知識原則(LKP)說的是一個軟件實體應當盡可能少地與其他實體發生相互作用。也叫迪米特法則。
## 減少對象之間的聯系
最少知識原則要求我們在設計程序時,應當盡量減少對象之間的交互。如果兩個對象之間不必彼此直接通信,那么這兩個對象就不要發生直接的相互聯系。常見的做法是引入一個第三者對象,來承擔這些對象之間的通信作用。如果一些對象需要向另一些對象發起請求,可以通過第三者來轉發這些請求。
## 設計模式中的最少知識原則
### 中介者模式
中介者模式很好滴體現了最少知識原則。通過增加一個中介者對象,讓所有的相關對象都通過中介者對象來通信,而不是互相引用。所以,當一個對象發生改變時,只需要通知中介者對象即可。
### 外觀模式
外觀模式的作用是對客戶屏蔽一組子系統的復雜性。外觀模式對客戶提供一個簡單易用的高層接口,高層接口會把客戶的請求轉發給子系統來完成具體的功能實現。大多數客戶都可以通過請求外觀接口來達到訪問子系統的目的。
```javascript
var A = function(){
a1();
a2();
};
var B = function(){
b1();
b2();
};
var facade = function(){
A();
B();
};
facade();
```
外觀模式的作用主要有兩點:
- 為一組子系統提供一個簡單便利的訪問入口。
- 隔離客戶與復雜子系統之間的聯系,客戶不用去了解子系統的細節。
外觀模式是符合最少知識原則的。
## 封裝在最少知識原則中的體現
封裝在很大程度上表達的是數據的隱藏。一個模塊或者對象可以將內部的數據或者實現細節隱藏起來,只暴露必要的接口API供外界訪問。對象之間難免產生聯系,當一個對象必須引用另外一個對象的時候,我們可以讓對象只暴露必要的接口,讓對象之間的聯系限制在最小的范圍之內。
把變量的可見性限制在一個盡可能小的范圍內,這個變量對其他不相關模塊的影響就越小,變量被改寫和發生沖突的機會也越小。這也是廣義的最少原則的一種體現。
# 開放-封閉原則
在面向對象的程序設計中,開放-封閉原則(OCP)是最重要的一條原則。很多時候,一個程序具有良好的設計,往往說明它是符合開放-封閉原則的。軟件實體(類、模塊、函數)等應該是可以擴展的,但是不可修改的。
## 拓展window.onload函數
```javascript
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(){...});
```
通過動態裝飾函數的方式,我們完全不用理會從前`window.onload`函數的內部實現。
## 開放和封閉
開放-封閉原則的思想:當需要改變一個程序的功能或者給這個程序增加新功能的時候,可以使用增加代碼的方式,但是不允許改動程序的源代碼。
## 用對象的多態性消除條件分支
利用多態的思想,可以把程序中不變的部分隔離出來(動物都會叫),然后把可變的部分封裝起來(不同類型的動物發出不同的叫聲),這樣一來程序就具有了可擴展性。
```javascript
var makeSound = function(animal){
animal.sound();
};
var Duck = function(){};
Duck.prototype.sound = function(){...};
var Dog = function(){};
Dog.prototype.sound = function(){...};
makeSound(new Duck);
makeSound(new Dog);
```
## 找出變化的地方
最明顯的就是找出程序中將要發生變化的地方,然后把變化封裝起來。
通過封裝的方式可以把系統中穩定不變的部分和容易變化的部分隔離開來。在系統的演變過程中,我們只需要替換那些容易變化的部分,如果這些部分是已經封裝好的,那么替換起來也相對容易。而變化部分之外的就是穩定的部分。在系統的演變過程中,穩定的部分是不需要改變的。
### 放置掛鉤
放置掛鉤(hook)也是分離變化的一種方式。我們在程序有可能發生變化的地方放置一個掛鉤,掛鉤的返回結果決定了程序的下一步走向。
### 使用回調函數
回調函數是一種特殊的掛鉤。可以把一部分易于變化的邏輯封裝在回調函數里,然后把回調函數當作參數傳入一個穩定和封閉的函數中。當回調函數被執行的時候,程序就可以因為回調函數的內部邏輯不通,而產生不同的結果。
## 設計模式中的開放-封閉原則
開放-封閉原則是編寫一個好程序的目標,其它設計原則都是達到這個目標的過程。
### 發布-訂閱模式
發布-訂閱模式用來降低多個對象之間的依賴關系,它可以取代對象之間硬編碼的通知機制,一個對象不用再顯式地調用另外一個對象的某個接口。當有新的訂閱者出現時,發布者的代碼不需要進行任何修改;同樣當發布者需要改變時,也不會影響到之前的訂閱者。
### 模板方法模式
模板方法是一種典型的通過封裝變化來提高系統擴展性的設計模式。在一個運用了模板方法模式的程序中,子類的方法種類和執行順序都是不變的,所以我們把這部分邏輯抽出來放到父類的模板方法里面;而子類的方法具體怎么實現則是可變的,于是把這部分變化的邏輯封裝到子類中。通過增加新的子類,便能給系統增加系統新的功能,并不需要改動抽象父類以及其他的子類,這也是符合開發-封閉原則的。
### 策略模式
策略模式和模板方法模式是一對競爭者。在大多數情況下,它們可以相互替換使用。模板方法模式基于繼承的思想,而策略模式則偏重于組合和委托。
策略模式將各種算法都封裝成單獨的策略類,這些策略類可以被交換使用。策略和使用策略的客戶代碼可以分別獨立進行修改而互不影響。我們增加一個新的策略類也非常方便,完全不用修改之前的代碼。
### 代理模式
拿預加載圖片舉例,我們現在已有一個給圖片設置`src`的函數`myImage`,當我們想為它增加圖片預加載功能時,一種做法是改動`myImage`函數內部的代碼,更好的做法是提供一個代理函數`proxyMyImage`,代理函數負責圖片預加載,在圖片預加載完成之后,再將請求轉交給原來的`myImage`函數,`myImage`函數在這個過程中不需要任何改動。
預加載圖片的功能和給圖片設置`src`的功能被隔離在兩個函數里,它們可以單獨改變而互不影響。`myImage`不知曉代理的存在,它可以繼續專注于自己的職責---給圖片設置`src`。
### 職責鏈模式
一個例子,把一個巨大的訂單函數分別拆成了500元訂單、200元訂單以及普通訂單的3個函數。這3個函數通過職責鏈連接在一起,客戶的請求會在這條鏈條里面依次傳遞:
```javascript
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(orderNoraml);
order500yuan.passRequest(1,true,10);
```
## 開放-封閉原則的相對性
讓程序符合開放-封閉原則的代價是引入更多的抽象層次,更多的抽象有可能會增大代碼的復雜度。
- 挑選出最容易發生變化的地方,然后構造抽象來封閉這些變化。
- 在不可避免發生修改的時候,盡量修改那些相對容易修改的地方。那一個開源庫來說,修改它提供的配置文件,總比修改它的源代碼來得簡單。
## 接受第一次愚弄
一方面,我們需要盡快知道程序在哪些地方會發生變化,這要求我們有一些“未卜先知”的能力。另一方面,留給程序員的需求排期并不是無限的,所以我們可以說服自己去接受不合理的代碼帶來的第一次愚弄。先假設變化永遠不會發生,這有利于我們迅速完成需求。當變化發生并且對我們接下來工作造成影響的時候,可以再回頭來封裝這些變化的地方。
# 接口和面向接口編程
不關注對象的具體類型,而僅僅針對超類型中的“契約方法”來編寫程序,可以產生可靠性高的程序,也可以極大地減少子系統實現之間的相互依賴關系。這就是面向接口編程,而不是面向實現編程。
## JavaScript語言是否需要抽象類和interface
抽象類和`interface`的作用主要都是以下兩點:
- 通過向上轉型來隱藏對象的真正類型,以表現對象的多態性。
- 約定類與類之間的一些契約行為。
接口在`JavaScript`中的最大作用就退化到了檢查代碼的規范性。
## 用鴨子類型進行接口檢查
*如果它走起來像鴨子,叫起來也是鴨子,那么它就是鴨子。* 鴨子類型是動態類型語言面向對象設計中的一個重要概念,利用鴨子類型的思想,不必借助超類型的幫助,就能在動態類型語言中輕松地實現設計原則:面向接口編程,而不是面向實現編程。
用鴨子類型來判斷一個對象是否為數組
```javascript
var isArray = function(obj){
return obj &&
typeof obj === 'object' &&
typeof obj.length === 'number' &&
typeof obj.splice === 'function'
};
```
# 代碼重構
模式和重構之間有著一種與生俱來的關系。從某種角度來看,設計模式的目的就是為了許多重構行為提供目標。
1. 提煉函數
2. 合并重復的代碼片段
3. 把條件分支語句提煉成函數
4. 合理使用循環
5. 提前讓函數退出代替嵌套條件分支
6. 傳遞對象參數代替過長的參數列表
7. 盡量減少參數數量
8. 少用三目運算符
9. 合理使用鏈式調用
10. 分解大型類
11. 用return退出多重循環