單一職責原則(Single Responsibility Principle, SRP)是Bob大叔提倡的S.O.L.I.D五大 設計原則中的第一個。其中,職責(Responsibility)被表述為“變化的原因” (reason to change);SRP被表述為“一個類應該有且只有一個變化的原因”。但如果光從字面去理解, SRP很容易讓人望文生義產生誤解。本文希望能闡明SRP 的本質,達到避免誤解和指導設計的 目的。
動機
對於設計原則的理解應該首先從它的動機入手,即遵循這個原則帶來的好處是什麼?對於 SRP來講,如果遵循“一個類應該有且只有一個變化的原因”是因,那麼“任何一個變化只會 影響一個類”就是果。可見SRP的動機主要是系統的可維護性,即降低適應變化的成本。
多功能與單變化
對於單功能的類來講,比較容易遵循SRP,比如:把string轉換為DateTime類型的工具類 。理解SRP的難點在於在多功能類的情形,即不容易理解多功能與單變化的矛盾。讓我們先來 看一個Modem類的例子,Modem具有4個功能:撥號,掛斷,發送數據,接收數據:
class Modem {
public void Dial(string aPno) {…}
public void Hangup() {…}
public void Send(char aData) {…}
public char Receive() {…}
}
我們先來考察一下Modem類的變化點,並將其分為兩類:1.Modem類的內部細節變化,比如 Dial、Hangup、Send、Receive等具體實現發生了變化;2.Modem類整體性的變化,比如Modem 類需要增加Poweron和Poweroff方法。上面的Modem類具有多個變化點,不符合SRP。
抽象與抽象層次
但是,需要注意的是上面的Modem類其實是很符合現實的概念模型的,Modem本身就應該具 有這幾個功能點。難道要把Modem類強行拆開嗎,難道所有類都只應該有一個方法嗎?是什麼 原因導致符合現實概念模型的Modem類設計與SRP不符合呢?問題在於抽象與抽象層次,Modem 類是現實概念模型的抽象沒有錯,但是Modem類缺乏抽象層次,低層概念與高層概念混雜,這 正是問題所在。下面是重構以後的Modem類:
interface IDialable{
void Dial(string aPno);
}
class Dialer : IDialable{
public void Dial(string aPno){…}
}
class Modem{
public void Dial(string aPno) { m_dialer.Dial(aPno); }
private IDialable m_dialer;
… //hangup, send, receive類似
}
上面的設計將各個功能點抽象為單一的接口,將變化封裝在穩定的接口背後,再通過組合 的方式建立起整體的Modem類與局部的功能點的聯系。這樣就避免了低層的變化傳導到高層。
總結
可見,理解SRP的關鍵在於理解類的抽象層次,高層次的類是高層概念的抽象,低層次的 類是低層概念的抽象。低層的變化只影響低層類,高層的變化只影響高層類。對於遵守SRP的 設計,一定具有很好的抽象層次,因此不妨以SRP為指導和檢驗,幫助我們設計出好的類來。 最後,我用幾個關鍵詞梳理SRP的脈絡:
類只有一個變化的原因 >> 一個變化只影響一個類 >> 變化只影響其相應層 次的類