一、定義
一個類應該隻有一個發生變化的原因。
二、為什麼要使用SRC
因為每一個職責都是變化的一個軸線。當需求變化時,這種變化就會反映為類的職責的變化。如果一個類承擔了多于一個的職責,那麼引起它變化的原因就會有多個。
如果一個類承擔的職責過多,就等于把這些職責耦合在了一起。一個職責的變化可能會消弱或抑制這個類完成其他職責的能力。這種耦合會導緻脆弱的設計,當變化發生時,設計會遭到意想不到的破壞。
三、案例示範
考慮圖中的設計。
Rectangle類具有兩個方法,一個方法把矩形繪制到螢幕上,另一個方法則是計算矩形的面積。 現在有兩個應用程式使用Rectangle類。一個是利用Rectangle計算矩形面積,但不需要繪制矩形;另一個應用程式可能會繪制矩形,也可能計算面積。
這個設計違反了單一職責原則。Rectangle類具有兩個職責。第一個職責提供了矩形面積的計算;第二個職責提供了矩形繪制。 對于SRC的違反導緻了一些嚴重的問題。
對于這個違反了SRC的程式有以下幾點問題:
首先,我們必須在左側的程式中包含進GUI代碼,即使我們是不需要的。
其次,如果右側程式的改變導緻了Rectangle的改變,那麼這個改變會迫使我們重新建構、測試和部署左側的程式。
一個較好的設計就是把這兩個職責分離到如下圖所示的兩個完全不同的類中。這個設計把Rectangle類中進行計算的部分移到了GeometricRectangle類中。現在矩形繪制方法的變化不會影響到左側的程式。
四、什麼是職責
在SRC中,我們把職責定義為變化的原因。
如果你能夠想到多于一個的動機去改變一個類,那麼這個類就具有多于一個的職責。
有時,我們很難注意到這一點,我們習慣于以組的形式去考慮職責。 例如,下面的Modem接口,大多數人認為這個接口非常合理。該接口所聲明的4個方法确實是數據機所具有的功能 。
public interface Modem
{
public void Dial(string pno); //連接配接
public void Hangup(); //斷開
public void Send(char c); //發送
public char Recv(); //接收
}
然而,該接口中卻顯示出兩個職責。第一個職責是連接配接管理;第二個職責是資料通信。
這兩個職責需要分開嗎?
這依賴于應用程式變化的方式。如果應用程式的變化會影響到連接配接管理方法的簽名,那麼這個設計就具有僵化性,因為調用send和recv的類必須重新編譯、部署。在這種情況下,這兩個職責應該被分離,如下圖所示。
另一方面,如果應用程式的變化方式總是導緻這兩個職責同時變化,那就不必分離它們。
記住這麼一個結論,僅當變化發生時,變化的軸線才具有實際意義。如果沒有征兆,那麼應用SRP或者任何其它原則都是不明智的。
五、分離耦合的職責
請注意,在上圖中,把兩個職責都耦合進了ModemImplementation類中。這也許不是最好的,但是或許必須得這麼做。常常會有一些和硬體或者作業系統的細節有關的原因,迫使我們把不願意耦合在一起的東西耦合在了一起。然而,對于應用的其餘部分來說,通過分離它們的接口我們已經解耦了。
六、持久化
下圖展示了一種常見的違反SRP的情形。
Employee類包含了業務規則和對于持久化的控制。這個兩個職責在大多數情況下絕不應該混合在一起。業務規則往往會頻繁地變化,而持久化的方式卻不會如此頻繁的變化,并且變化的原因也是完全不同的,它們實在兩個方向上變化,一個是業務方向上變化,另一個是持久化方向上變化。
把業務規則和持久化子系統綁定在一起的做法是自讨苦吃。 測試驅動的開發實踐常常會遠在設計出現臭味之前就迫使我們分離這兩個職責。然而,如果測試沒有迫使這種分離,那麼就應該使用Facade(外觀)、DAO(資料通路)或者Proxy(代理)模式對設計進行重構,分離這兩個職責。
七、結論
SRP是所有原則中最簡單的原則之一,也是最難正确運用的原則之一。我們會不自覺地把職責結合到一起。軟體設計真正要做的許多工作,就是發現職責并把那些職責互相分離。