六大設計原則——單一職責原則【Single Responsibility Principle】

聲明:本文內容是從網絡書籍整理而來,並非原創。

用戶管理的例子

  1. 先看一張用戶管理的類圖:
    這裏寫圖片描述
  2. 再看一眼上面的圖,思考:這樣合理嗎?

  3. 這個接口是一個很糟糕的設計! 用戶的屬性和行爲竟然混合在一起!!!

  4. 正確的做法是把用戶的信息抽取成一個業務對象(Bussiness Object,簡稱 BO),把行爲抽取成另外一個接口中,我們把這個類圖重新畫一下:
    這裏寫圖片描述

  5. 這樣劃分成了兩個接口,IUserBO 負責用戶的屬性,IUserBiz 負責用戶的行爲,因爲是面向的接口編程,所有當產生了這個 UserInfo 對象之後,既可以把它當 IUserBO 接口使用,也可以當 IUserBiz 接口使用,類似下面代碼:

    IUserBiz userInfo = new UserInfo(); 
    
    //我要賦值了,我就認爲它是一個純粹的BO 
    IUserBO userBO = (IUserBO)userInfo; 
    userBO.setPassword("abc"); 
    
    //我要執行動作了,我就認爲是一個業務邏輯類 
    IUserBiz userBiz = (IUserBiz)userInfo; 
    userBiz.deleteUser();
  6. 問題解決了,但實際中我們更傾向於使用兩個不同的類或接口, 一個就是IUserBO,一個是IUserBiz,如下圖:
    這裏寫圖片描述

  7. 以上我們把一個接口拆分成兩個接口的動作,就是依賴了單一職責原則,那什麼是單一職責原則呢?

    單一職責原則:應該有且僅有一個原因引起類的變更
    SRP 的原話解釋是:There should never be more than one reason for a class to change。

打電話的例子

  1. 電話通話的時候有四過程發生:撥號、通話、迴應、掛機,那麼看下接口圖:
    這裏寫圖片描述
    接口代碼:

    public interface IPhone { 
    
      //撥通電話 
      public void dial(String phoneNumber); 
    
      //通話 
      public void chat(Object o); 
    
      //迴應,只有自己說話而沒有迴應,那算啥?! 
      public void answer(Object o); 
    
      //通話完畢,掛電話 
      public void huangup(); 
    } 
    
  2. 想想,符合單一職責原則嗎?

  3. 其實它有兩個職責:一個是協議管理,一個是數據傳送,diag()和 huangup()兩個方法實現的是協議管理,撥號接通和關閉;chat()和answer()是數據的傳送,把我們說的話轉換成模擬信號或者是數字信號傳遞到對方,然後再把對方傳遞過來的信號還原成我們聽的懂人話。這兩個職責互不影響,所以考慮拆開,如下圖:
    這裏寫圖片描述

  4. 這種設計完全滿足類和接口的單一職責要求,但是一個手機類要把 ConnectionManager 和DataTransfer 組合在一塊才能使用,組合是一種強耦合關係,都是有共同的生命期,這樣的強耦合關係還不如使用接口實現的方式呢,而且還增加了類的複雜性,多了兩個類呀,好,我們修改一下類圖:
    這裏寫圖片描述

  5. 這樣設計才爲完美,一個手機實現了兩個接口,把兩個職責融合一個類中,你會覺得這個 Phone有兩個原因引起變化了呀,是的是的,但是別忘記了我們是面向接口編程,我們對外公佈的是接口而不是實現類;而且如果真要實現類的單一職責的話,這個就必須使用了上面組合的方式了,那這個會引起類間耦合過重的問題。

    所以,對於接口,我們在設計的時候一定要做到單一,但是對於實現類就需要多方面考慮了,生搬硬套單一職責原則會引起類的劇增,給維護帶來非常多的麻煩;而且過分的細分類的職責也會人爲的製造系統的複雜性,本來一個類可以實現的行爲非要拆成兩個,然後使用聚合或組合的方式再耦合在一起,這個是人爲製造了系統的複雜性,所以原則是死的,人是活的,這句話是非常好的。

單一職責的好處:

  1. 類的複雜性降低,實現什麼職責都有清晰明確的定義;
  2. 可讀性提高,複雜性降低,那當然可讀性提高了;
  3. 可維護性提高,那當然了,可讀性提高,那當然更容易維護了;
  4. 變更引起的風險降低,變更是必不可少的,接口的單一職責做的好的話,一個接口修改只對相應的實現類有影響,與其他的接口無影響,這個是對項目有非常大的幫助。

    單一職責原則最難劃分的就是職責,一個職責一個接口,但是問題是“職責”是一個沒有量化的標準,一個類到底要負責那些職責?這些職責怎麼細化?細化後是否都要有一個接口或類?這個都是需要從實際的項目區考慮的,從功能上來說,定義一個 IPhone 接口也沒有錯,實現了電話的功能呀,而且設計還很簡單,就一個接口一個實現類,真正的項目我想大家一般都是會這麼設計的,從設計原則上來看就有問題了,有兩個可以變化的原因放到了一個接口中了,這就爲以後的變化帶來了風險,我從 2G 通訊協議修改到 3G 通訊,你看看你提供出的接口 IPhone 是不是要修改了?接口修改對其他的 Invoker 是不是有很大影響?!

方法的單一職責原則

單一職責使用於接口、類,同時也使用方法,什麼意思呢?一個方法儘可能做一件事情,比如一個方法修改用戶密碼,別把這個方法放到“修改用戶信息”方法中,這個方法的顆粒度很粗,比如這樣一個方法:
這裏寫圖片描述

在 IUserManager 中定義了一個方法叫 changeUser,根據傳遞的 type 不同,把可變長度參數changeOptions 修改到 userBo 這個對象上,並調用持久層的方法保存到數據庫中。在我的項目組中如果有人寫了這樣一個方法,我不管他寫了多上程序化了多少工夫,一律重寫!原因是:方法職責不清晰,不單一,一般方法設計成這樣的:
這裏寫圖片描述
你要修改用戶名稱,就調用 changeUserName 方法,你要修改家庭地址就調用 changeHomeAddress,你要修改單位單戶就調用 changeOfficeTel 方法,每個方法的職責就非常清晰,這也是一個良好的設計習慣。
所以,不管是對接口、類、方法使用了單一規則原則,那麼快樂的就不僅僅是你了,還有你項目的成員,你的板,減少了因爲變更引起的工作量呀,加官進爵等着你幺!

疑惑

你看到這裏,就會問我,你寫是類的設計原則嗎?你通篇都在說接口的單一職責,類的單一職責你都違背了呀,呵呵,這個還真是的,我的本意是想把這個原則講清楚,類的單一職責嘛,這個很簡單,但當我回頭寫的時候,發覺纔不是這麼回事,翻看了以前一些設計和代碼,基本上拿的出手的類設計都是和單一職責向違背的,靜下心來回憶,發覺每一個類這樣設計都是有原因的。這幾天我查閱了 wikipedia、oodesign 等幾個網站,專家和我也有類似的經驗,基本上類的單一職責都用了類似的一句話來說“This is sometimes hard to see” ,這句話翻譯過來就是“這個有時候很難說” ,是的,類的單一職責確實受非常多的因素制約,純理論的來講,這個原則是非常優秀的,但是現實有現實難處,你必須去考慮項目工期、成本、人員技術水平、硬件情況、網絡情況甚至有時候還要考慮政府政策、壟斷協議等等原因。

所以,對於單一職責原則,我的建議是接口一定要做到單一職責,類設計儘量只有一個原因引起變化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章