一個類所對應的需求功能越多,引起變化的可能性就越大,單一職責原則(Single Responsibility Principle,簡稱SRP)就是要求我們的接口(或類)盡可能保持單一,它的定義是說「一個類有且僅有一個變化的原因(There should never be more than one reason for a class to change)」,那問題是什麼是職責呢?
職責是一個接口(或類)要承擔的業務含義,或是接口(或類)表現出的意圖,例如一個User類可以包含寫入用戶信息到數據庫、刪除用戶、修改用戶密碼等職責,而一個密碼工具類則可以包含解密職責和加密職責。明白了什麼是類的職責單一,再來看看它有什麼好處。單一職責有以下三個優點:
(1)類的複雜性降低
職責單一,在實現什麼職責時都有清晰明確的定義,那麼接口(或類)的代碼量就會減少,複雜度也就會減少。當然,接口(或類)的數量會增加上去,相互間的關係也會更複雜,這就需要適當把握了。
(2)可讀性和可維護性提高
職責單一,會讓類中的代碼量減少,我們可以一眼看穿該類的實現方式,有助於提供代碼的可讀性,這也間接提升了代碼的可維護性。
(3)降低變更風險
變更是必不可少的,如果接口(或類)的單一職責做得好,一個接口修改只對相應的實現類有影響,對其他的接口無影響,那就會對系統的擴展性、維護性都有非常大的幫助。
既然單一職責有這麼多的優點,那我們該如何應用到設計和編碼中呢?下面以電話通信為例子來說明如何實施單一職責原則:
(1)分析職責
一次電話通信包含四個過程:撥號、通話、回應、掛機,我們來思考一下該如何劃分職責,這四個過程包含了兩個職責:一個是撥號和掛機表示的是通信協議的鏈接和關閉,另外一個是通話和回應所表示的數據交互。
問題是我們依靠什麼來劃分職責呢?依靠變化因素,我們可以這樣考慮該問題:
通信協議的變化會引起數據交換的變化嗎?會的!你能在3G網絡視頻聊天,但你很難在2G網絡上實現。
數據交互的變化會引起通信協議的變化嗎?會的!傳輸2KB的文件和20GB的文件需要的不可能是同一種網絡,用56KB的「小貓」傳輸一個20GB的高清影視那是不可行的。
(2)設計接口
職責分析確定了兩個職責,首先不要考慮實現類是如何設計的,我們首先應該通過兩個接口來實現這兩個職責。接口的定義如下。
//通信協議
interface Connection{
//撥通電話
public void dial();
//通話完畢,掛電話
public void hangup();
}
//數據傳輸
interface Transfer{
//通話
public void chat();
}
(3)合併實現
接口定義了兩個職責,難道實現類就一定要分別實現這兩個接口嗎?這樣做確實完全滿足了單一職責原則的要求:每個接口和類職責分明,結構清晰,但是我相信讀者在設計的時候肯定不會採用這種方式,因為一個電話類要把ConnectionManager和DataTransfer的實現類組合起來才能使用。組合是一種強耦合關係,你和我都有共同的生命期,這樣的強耦合關係還不如使用接口實現的方式呢!而且這還增加了類的複雜性,多出了兩個類。
通常的做法是一個實現類實現多個職責,也就是實現多個接口,代碼如下:
//電話
class Phone implements Connection, Transfer{
//實現各個接口
}
這樣的設計才是完美的,一個類實現了兩個接口,把兩個職責融合在一個類中。你會覺得這個Phone有兩個原因引起了變化,是的,但是別忘記了我們是面向接口編程的,我們對外公佈的是接口而不是實現類。而且,如果真要實現類的單一職責,就必須使用上面的組合模式,這會引起類間的耦合過重、類的數量增加等問題,人為地增加了設計的複雜性。
對於單一職責原則,我建議接口一定要做到職責單一,類的設計盡量做到只有一個原因引起變化。
注意 接口職責一定要單一,實現類職責盡量單一。