單一職責原則

轉載自:https://www.cnblogs.com/cbf4life/archive/2009/12/11/1622166.html

 

這篇文章講得真的很好,其實實際項目中對於單一職責的原則使用還是要對照到具體場景中,要儘量讓類的影響因素少一點,這個也是需要不斷迭代和重構的以及隨着業務的發展這些都是會變化的。這是一個指導原則,是我們努力的方向,但是不一定要完美達到,這個完美達到是不可能的,這個原則會讓我們有這個思考的維度,讓大家形成共識,讓後面類的設計對應的影響因素在自己的能做的,以及認知範圍內的儘可能少就可以。運用之妙,在乎一心。

 

1.1 我是“牛”類,我可以擔任多職嗎

     單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP。這個設計原則備受爭議,只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的。如果你是老大,看到一個接口或類是這樣或那樣設計的,你就問一句:“你設計的類符合SRP原則嗎?”,保準對方立馬“萎縮”掉,而且還一臉崇拜地看着你,心想:“老大確實英明”。這個原則存在爭議之處在哪裏呢?就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。我們先舉個例子來說明什麼是單一職責原則。

     只要做過項目,肯定要接觸到用戶、機構、角色管理這些模塊,基本上使用的都是RBAC模型,確實是很好的一個解決辦法。我們今天要講的是用戶管理、修改用戶的信息、增加機構(一個人屬於多個機構)、增加角色等,用戶有這麼的信息和行爲要維護,我們就把這些寫到一個接口中,都是用戶管理類嘛,我們先來看它的類圖,如1-1所示。

clip_image002

圖1-1 用戶信息維護類圖

     太Easy的類圖了,我相信,即使是一個初級的程序員也可以看出這個接口設計得有問題,用戶的屬性(Property)和用戶的行爲(Behavior)沒有分開,這是一個嚴重的錯誤!非常正確,這個接口確實設計得一團糟,應該把用戶的信息抽取成一個BO(Bussiness Object,業務對象),把行爲抽取成一個BIZ(Business Logic,業務邏輯),按照這個思路對類圖進行修正,如圖1-2所示。

clip_image004

圖1-2 職責劃分後的類圖

     重新拆封成兩個接口,IUserBO負責用戶的屬性,簡單地說,IUserBO的職責就是收集和反饋用戶的屬性信息;IUserBiz負責用戶的行爲,完成用戶信息的維護和變更。各位可能要說了,這個與我實際工作中用到的User類還是有差別的呀!彆着急,我們先來看一看分拆成兩個接口怎麼使用。OK,我們現在是面向接口編程,所以產生了這個UserInfo對象之後,當然可以把它當IUserBO接口使用。當然,也可以當IUserBiz接口使用,這要看你在什麼地方使用了。要獲得用戶信息,就當是IUserBO的實現類;要是希望維護用戶的信息,就把它當作IUserBiz的實現類就成了,如代碼清單1-1所示。

代碼清單1-1 分清職責後的代碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

.......

 

IUserBiz userInfo = new UserInfo();

 

//我要賦值了,我就認爲它是一個純粹的BO

 

IUserBO userBO = (IUserBO)userInfo;

 

userBO.setPassword("abc");

 

//我要執行動作了,我就認爲是一個業務邏輯類

 

IUserBiz userBiz = (IUserBiz)userInfo;

 

userBiz.deleteUser();

 

.......

     確實可以如此,問題也解決了,但是我們來回想一下我們剛纔的動作,爲什麼要把一個接口拆分成兩個呢?其實,在實際的使用中,我們更傾向於使用兩個不同的類或接口:一個是IUserBO, 一個是IUserBiz,類圖應該如圖1-3所示。

clip_image006

圖1-3 項目中經常採用的SRP類圖

     以上我們把一個接口拆分成兩個接口的動作,就是依賴了單一職責原則,那什麼是單一職責原則呢?單一職責原則的定義是:應該有且僅有一個原因引起類的變更。

1.2 絕殺技,打破你的傳統思維

     解釋到這裏,估計你已經很不屑了,“切!這麼簡單的東西還要講?!弱智!”好,我們來講點複雜的。SRP的原話解釋是:There should never be more than one reason for a class to change。這句話初中生都能看懂,不多說,但是看懂是一碼事,實施就是另外一碼事了。上面講的例子很好理解,在實際項目中大家已經都是這麼做了,那我們再來看下面這個例子是否好理解。電話這玩意,是現代人都離不了,電話通話的時候有四個過程發生:撥號、通話、迴應、掛機,那我們寫一個接口,其類圖應該如圖1-4所示。

clip_image008

圖1-4 電話類圖

     我不是有意要冒犯IPhone的,同名純屬巧合,我們來看一個這個過程的代碼,如代碼清單1-2所示。

代碼清單1-2 電話過程

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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所示。

clip_image010

圖1-5 職責分明的電話類圖

     這個類圖看着有點複雜了,完全滿足了單一職責原則的要求,每個接口職責分明,結構清晰,但是我相信你在設計的時候肯定不會採用這種方式,一個手機類要把ConnectionManager和DataTransfer組合在一塊才能使用。組合是一種強耦合關係,你和我都有共同的生命期,這樣的強耦合關係還不如使用接口實現的方式呢,而且還增加了類的複雜性,多了兩個類。經過這樣的思考後,我們再修改一下類圖,如圖1-6所示。

clip_image012

圖1-6 簡潔清晰、職責分明的電話類圖

     這樣的設計纔是完美的,一個類實現了兩個接口,把兩個職責融合在一個類中。你會覺得這個Phone有兩個原因引起變化了呀,是的是的,但是別忘記了我們是面向接口編程,我們對外公佈的是接口而不是實現類。而且,如果真要實現類的單一職責,這個就必須使用上面的組合模式了,這會引起類間耦合過重、類的數量增加等問題,人爲的增加了設計的複雜性。

     通過上面的例子,我們來總結一下單一職責原則有什麼好處:

  • 類的複雜性降低,實現什麼職責都有清晰明確的定義;
  • 可讀性提高,複雜性降低,那當然可讀性提高了;
  • 可維護性提高,那當然了,可讀性提高,那當然更容易維護了;
  • 變更引起的風險降低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改只對相應的實現類有影響,對其他的接口無影響,這對系統的擴展性、維護性都有非常大幫助。

     看過電話這個例子後,是不是有點反思了,我以前的設計是不是有點的問題了?不,不是的,不要懷疑自己的技術能力,單一職責原則最難劃分的就是職責。一個職責一個接口,但問題是“職責”是一個沒有量化的標準,一個類到底要負責那些職責?這些職責該怎麼細化?細化後是否都要有一個接口或類?這些都需要從實際的項目去考慮,從功能上來說,定義一個IPhone接口也沒有錯,實現了電話的功能,而且設計還很簡單,僅僅一個接口一個實現類,實際的項目我想大家都會這麼設計。項目要考慮可變因素和不可變因素,以及相關的收益成本比率,因此設計一個IPhone接口也可能是沒有錯的。但是,如果純從“學究”理論上分析就有問題了,有兩個可以變化的原因放到了一個接口中,這就爲以後的變化帶來了風險。如果以後模擬電話升級到數字電話,我們提供的接口IPhone是不是要修改了?接口修改對其他的Invoker類是不是有很大影響?!

     注意 單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否有優良,但是“職責”和“變化原因”都是不可度量的,因項目而異,因環境而異。

1.3 我單純,所以我快樂

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

     單一職責原則很難在項目中得到體現,非常難,爲什麼?在國內,技術人員的地位和話語權都比較低,因此在項目中需要考慮環境,考慮工作量,考慮人員的技術水平,考慮硬件的資源情況,等等,最終妥協的結果是經常違背單一原則。而且,我們中華文明就有很多屬於混合型的產物,比如筷子,我們可以把筷子當做刀來使用,分割食物;還可以當叉使用,把食物從盤子中移動到口中。而在西方的文化中,刀就是刀,叉就是叉,你去吃西餐的時候這兩樣肯定都是有的,刀就是切割食物,叉就是固定食物或者移動食物,分工很明晰。這種文化的差異是很難一步改造過來,但是我相信隨着技術的深入,單一職責原則必然會深入到項目的設計中去,而且這個原則是那麼的簡單,簡單得不需要我們更加深入地思考,單從字面上大家都應該知道是什麼意思,單一職責嘛!

     單一職責適用於接口、類,同時也適用於方法,什麼意思呢?一個方法儘可能做一件事情,比如一個方法修改用戶密碼,不要把這個方法放到“修改用戶信息”方法中,這個方法的顆粒度很粗,比如圖1-7中所示的方法。

clip_image014

圖1-7 一個方法承擔多個職責

     在IUserManager中定義了一個方法changeUser,根據傳遞的類型不同,把可變長度參數changeOptions修改到userBo這個對象上,並調用持久層的方法保存到數據庫中。在我的項目組中,如果有人寫了這樣一個方法,我不管他寫了多少程序,花了多少工夫,一律重寫!原因很簡單:方法職責不清晰,不單一,不要讓別人猜測這個方法可能是用來處理什麼邏輯。比較好的設計如圖1-8所示。

clip_image016

圖1-8 一個方法承擔一個職責

     通過上面的類圖,如果要修改用戶名稱,就調用changeUserName方法;要修改家庭地址,就調用changeHomeAddress方法;要修改單位電話,就調用changeOfficeTel方法。每個方法的職責非常清晰明確,不僅開發簡單,而且日後的維護也非常容易,大家可以逐漸養成這樣的習慣。

     所以,如果對接口、類、方法使用了單一職責原則,那麼快樂的就不僅僅是你了,還有你的項目組成員,大家可以輕鬆而又愉快地進行開發;還有你的老闆,減少了因爲變更引起的工作量,減少了無爲人員和資金消耗。當然,最快樂的也許就是你了,因爲加官進爵可能等着你喲!

1.4 最佳實踐

     你閱讀到這裏,可能就會問我,你寫的是類的設計原則嗎?你通篇都在說接口的單一職責,類的單一職責你都違背了呀!呵呵,這個還真是的,我的本意是想把這個原則講清楚,類的單一職責嘛,這個很簡單,但當我回頭寫的時候,發覺並不是這麼回事,翻看了以前的一些設計和代碼,基本上拿得出手的類設計都是與單一職責相違背的。靜下心來回憶,發覺每一個類這樣設計都是有原因的。這幾天我查閱了Wikipedia、OODesign等幾個網站,專家和我也有類似的經驗,基本上類的單一職責都用了類似的一句話來說“This is sometimes hard to see”,這句話翻譯過來就是“這個有時候很難說”。是的,類的單一職責確實受非常多因素的制約,純理論地來講,這個原則是非常優秀的,但是現實有現實的難處,你必須去考慮項目工期、成本、人員技術水平、硬件情況、網絡情況甚至有時候還要考慮政府政策、壟斷協議等原因。比如,2004年我就做過一個項目,做加密處理的,甲方就甩過來一句話,你什麼都不用管,調用這個API就可以了,不用考慮什麼傳輸協議、異常處理、安全連接等。所以,我們就直接使用了JNI與加密廠商提供的API通信,什麼單一職責原則,根本就不用考慮,因爲對方不公佈通信接口、異常判斷。

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

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