一、單一職責原則描述程序員
單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP。這個設計原則備受爭議,只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的。若是你是老大,看到一個接口或類是這樣或那樣設計的,你就問一句:「你設計的類符合SRP原則嗎?」,保準對方立馬「萎縮」掉,並且還一臉崇拜地看着你,心想:「老大確實英明」。這個原則存在爭議之處在哪裏呢?就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。咱們先舉個例子來講明什麼是單一職責原則。編程
只要作過項目,確定要接觸到用戶、機構、角色管理這些模塊,基本上使用的都是RBAC模型,確實是很好的一個解決辦法。咱們今天要講的是用戶管理、修改用戶的信息、增長機構(一我的屬於多個機構)、增長角色等,用戶有這麼的信息和行爲要維護,咱們就把這些寫到一個接口中,都是用戶管理類嘛,咱們先來看它的類圖,如1-1所示。ide
圖1-1 用戶信息維護類圖spa
太Easy的類圖了,我相信,即便是一個初級的程序員也能夠看出這個接口設計得有問題,用戶的屬性(Property)和用戶的行爲(Behavior)沒有分開,這是一個嚴重的錯誤!很是正確,這個接口確實設計得一團糟,應該把用戶的信息抽取成一個BO(Bussiness Object,業務對象),把行爲抽取成一個BIZ(Business Logic,業務邏輯),按照這個思路對類圖進行修正,如圖1-2所示。設計
圖1-2 職責劃分後的類圖3d
從新拆封成兩個接口,IUserBO負責用戶的屬性,簡單地說,IUserBO的職責就是收集和反饋用戶的屬性信息;IUserBiz負責用戶的行爲,完成用戶信息的維護和變動。各位可能要說了,這個與我實際工做中用到的User類仍是有差異的呀!彆着急,咱們先來看一看分拆成兩個接口怎麼使用。OK,咱們如今是面向接口編程,因此產生了這個UserInfo對象以後,固然能夠把它當IUserBO接口使用。固然,也能夠當IUserBiz接口使用,這要看你在什麼地方使用了。要得到用戶信息,就當是IUserBO的實現類;要是但願維護用戶的信息,就把它看成IUserBiz的實現類就成了,如代碼清單1-1所示。code
代碼清單1-1 分清職責後的代碼示例對象
....... IUserBiz userInfo = new UserInfo(); //我要賦值了,我就認爲它是一個純粹的BO IUserBO userBO = (IUserBO)userInfo; userBO.setPassword("abc"); //我要執行動做了,我就認爲是一個業務邏輯類 IUserBiz userBiz = (IUserBiz)userInfo; userBiz.deleteUser(); .......
確實能夠如此,問題也解決了,可是咱們來回想一下咱們剛纔的動做,爲何要把一個接口拆分紅兩個呢?其實,在實際的使用中,咱們更傾向於使用兩個不一樣的類或接口:一個是IUserBO, 一個是IUserBiz,類圖應該如圖1-3所示blog
圖1-3 項目中常常採用的SRP類圖接口
以上咱們把一個接口拆分紅兩個接口的動做,就是依賴了單一職責原則,那什麼是單一職責原則呢?單一職責原則的定義是:應該有且僅有一個緣由引發類的變動。
解釋到這裏,估計你已經很不屑了,「切!這麼簡單的東西還要講?!弱智!」好,咱們來說點複雜的。SRP的原話解釋是:There should never be more than one reason for a class to change。這句話初中生都能看懂,很少說,可是看懂是一碼事,實施就是另一碼事了。上面講的例子很好理解,在實際項目中你們已經都是這麼作了,那咱們再來看下面這個例子是否好理解。電話這玩意,是現代人都離不了,電話通話的時候有四個過程發生:撥號、通話、迴應、掛機,那咱們寫一個接口,其類圖應該如圖1-4所示。
圖1-4 電話類圖
咱們來看一個這個過程的代碼,如代碼清單1-2所示。
代碼清單1-2 電話過程
public interface IPhone { //撥通電話 public void dial(String phoneNumber); //通話 public void chat(Object o); //迴應,只有本身說話而沒有迴應,那算啥?! public void answer(Object o); //通話完畢,掛電話 public void huangup(); }
實現類也比較簡單,我就再也不寫了,你們看看這個接口有沒有問題?我相信大部分的讀者都會說這個沒有問題呀,之前我就是這麼作的呀,某某書上也是這麼寫的呀,還有什麼什麼的源碼也是這麼寫的!是的,這個接口接近於完美,看清楚了,是「接近」!單一職責原則要求一個接口或類只有一個緣由引發變化,也就是一個接口或類只有一個職責,它就負責一件事情,看看上面的接口只負責一件事情嗎?是隻有一個緣由引發變化嗎?好像不是!
IPhone這個接口可不是隻有一個職責,它包含了兩個職責:一個是協議管理,一個是數據傳送。diag()和huangup()兩個方法實現的是協議管理,分別負責撥號接通和掛機;chat()和answer()是數據的傳送,把咱們說的話轉換成模擬信號或數字信號傳遞到對方,而後再把對方傳遞過來的信號還原成咱們聽得懂語言。咱們能夠這樣考慮這個問題,協議接通的變化會引發這個接口或實現類的變化嗎?會的!那數據傳送(想一想看,電話不只僅能夠通話,還能夠上網)的變化會引發這個接口或實現類的變化嗎?會的!那就很簡單了,這裏有兩個緣由都引發了類的變化,並且這兩個職責會相互影響嗎?電話撥號,我只要能接通就成,甭管是電信的仍是網通的協議;電話鏈接後還關心傳遞的是什麼數據嗎?不關心,你要是樂意使用56K的小貓傳遞一個高清的片子,那也沒有問題(頂多有人說你13了)。經過這樣的分析,咱們發現類圖上的IPhone接口包含了兩個職責,並且這兩個職責的變化不相互影響,那就考慮拆開成兩個接口,其類圖如圖1-5所示
圖1-5 職責分明的電話類圖
這個類圖看着有點複雜了,徹底知足了單一職責原則的要求,每一個接口職責分明,結構清晰,可是我相信你在設計的時候確定不會採用這種方式,一個手機類要把ConnectionManager和DataTransfer組合在一塊才能使用。組合是一種強耦合關係,你和我都有共同的生命期,這樣的強耦合關係還不如使用接口實現的方式呢,並且還增長了類的複雜性,多了兩個類。通過這樣的思考後,咱們再修改一下類圖,如圖1-6所示。
圖1-6 簡潔清晰、職責分明的電話類圖
這樣的設計纔是完美的,一個類實現了兩個接口,把兩個職責融合在一個類中。你會以爲這個Phone有兩個緣由引發變化了呀,是的是的,可是別忘記了咱們是面向接口編程,咱們對外公佈的是接口而不是實現類。並且,若是真要實現類的單一職責,這個就必須使用上面的組合模式了,這會引發類間耦合太重、類的數量增長等問題,人爲的增長了設計的複雜性。
經過上面的例子,咱們來總結一下單一職責原則有什麼好處:
看過電話這個例子後,是否是有點反思了,我之前的設計是否是有點的問題了?不,不是的,不要懷疑本身的技術能力,單一職責原則最難劃分的就是職責。一個職責一個接口,但問題是「職責」是一個沒有量化的標準,一個類到底要負責那些職責?這些職責該怎麼細化?細化後是否都要有一個接口或類?這些都須要從實際的項目去考慮,從功能上來講,定義一個IPhone接口也沒有錯,實現了電話的功能,並且設計還很簡單,僅僅一個接口一個實現類,實際的項目我想你們都會這麼設計。項目要考慮可變因素和不可變因素,以及相關的收益成本比率,所以設計一個IPhone接口也多是沒有錯的。可是,若是純從「學究」理論上分析就有問題了,有兩個能夠變化的緣由放到了一個接口中,這就爲之後的變化帶來了風險。若是之後模擬電話升級到數字電話,咱們提供的接口IPhone是否是要修改了?接口修改對其餘的Invoker類是否是有很大影響?!
注意單一職責原則提出了一個編寫程序的標準,用「職責」或「變化緣由」來衡量接口或類設計得是否有優良,可是「職責」和「變化緣由」都是不可度量的,因項目而異,因環境而異。