1. 程式人生 > >1.單一職責原則

1.單一職責原則

.1 我是“牛”類,我可以擔任多職嗎?

單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP。這個設計原則備受爭議,只要你想和別人爭執、慪氣或者是吵架,這個原則是屢試不爽的。如果你是老大,看到一個介面或類是這樣或那樣設計的,你就問一句:“你設計的類符合SRP原則嗎?”保準對方立馬“萎縮”掉,而且還一臉崇拜地看著你,心想:“老大確實英明”。這個原則存在爭議之處在哪裡呢?就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。我們先舉個例子來說明什麼是單一職責原則。只要做過專案,肯定要接觸到使用者、機構、角色管理這些模組,基本上使用的都是RBAC模型(Role-Based Access Control,基於角色的訪問控制,通過分配和取消角色來完成使用者許可權的授予和取消,使動作主體(使用者)與資源的行為(許可權)分離),確實是一個很好的解決辦法。我們這裡要講的是使用者管理、修改使用者的資訊、增加機構(一個人屬於多個機構)、增加角色等,使用者有這麼多的資訊和行為要維護,我們就把這些寫到一個介面中,都是使用者管理類嘛,我們先來看它的類圖,如圖所示。

太Easy的類圖了,我相信,即使是一個初級的程式設計師也可以看出這個介面設計得有問題,使用者的屬性和使用者的行為沒有分開,這是一個嚴重的錯誤!這個介面確實設計得一團
糟,應該把使用者的資訊抽取成一個BO(Business Object,業務物件),把行為抽取成一個Biz(Business Logic,業務邏輯),按照這個思路對類圖進行修正,如圖所示。


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

IUserInfo userInfo = new UserInfo();
//我要賦值了,我就認為它是一個純粹的BO
IUserBO userBO = (IUserBO)userInfo;
userBO.setPassword("abc");
//我要執行動作了,我就認為是一個業務邏輯類
IUserBiz userBiz = (IUserBiz)userInfo

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

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

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

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


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

public interface IPhone {
//撥通電話
public void dial(String phoneNumber);
//通話
public void chat(Object o);
//通話完畢,掛電話
public void hangup();
}

實現類也比較簡單,我就不再寫了,大家看看這個介面有沒有問題?我相信大部分的讀者都會說這個沒有問題呀,以前我就是這麼做的呀,某某書上也是這麼寫的呀,還有什麼什麼的原始碼也是這麼寫的!是的,這個介面接近於完美,看清楚了,是“接近”!單一職責原則要求一個介面或類只有一個原因引起變化,也就是一個介面或類只有一個職責,它就負責一件事情,看看上面的介面只負責一件事情嗎?是隻有一個原因引起變化嗎?好像不是!IPhone這個介面可不是隻有一個職責,它包含了兩個職責:一個是協議管理,一個是資料傳送。dial()和hangup()兩個方法實現的是協議管理,分別負責撥號接通和掛機;chat()實現的是資料的傳送,把我們說的話轉換成模擬訊號或數字訊號傳遞到對方,然後再把對方傳遞過來的訊號還原成我們聽得懂的語言。我們可以這樣考慮這個問題,協議接通的變化會引起這個介面或實現類的變化嗎?會的!那資料傳送(想想看,電話不僅僅可以通話,還可以上網)的變化會引起這個介面或實現類的變化嗎?會的!那就很簡單了,這裡有兩個原因都引起了類的變化。這兩個職責會相互影響嗎?電話撥號,我只要能接通就成,甭管是電信的還是網通的協議;電話連線後還關心傳遞的是什麼資料嗎?通過這樣的分析,我們發現類圖上的IPhone介面包含了兩個職責,而且這兩個職責的變化不相互影響,那就考慮拆分成兩個介面,其類圖如圖1-5所示。


這個類圖看上去有點複雜了,完全滿足了單一職責原則的要求,每個介面職責分明,結構清晰,但是我相信你在設計的時候肯定不會採用這種方式,一個手機類要把
ConnectionManager和DataTransfer組合在一塊才能使用。組合是一種強耦合關係,你和我都有共同的生命期,這樣的強耦合關係還不如使用介面實現的方式呢,而且還增加了類的複雜性,多了兩個類。經過這樣的思考後,我們再修改一下類圖,如圖1-6所示。這樣的設計才是完美的,一個類實現了兩個介面,把兩個職責融合在一個類中。你會覺得這個Phone有兩個原因引起變化了呀,是的,但是別忘記了我們是面向介面程式設計,我們對外公佈的是介面而不是實現類。而且,如果真要實現類的單一職責,這個就必須使用上面的組合模式了,這會引起類間耦合過重、類的數量增加等問題,人為地增加了設計的複雜性。
通過上面的例子,我們來總結一下單一職責原則有什麼好處:
● 類的複雜性降低,實現什麼職責都有清晰明確的定義;
● 可讀性提高,複雜性降低,那當然可讀性提高了;
● 可維護性提高,可讀性提高,那當然更容易維護了;
● 變更引起的風險降低,變更是必不可少的,如果介面的單一職責做得好,一個介面修
改只對相應的實現類有影響,對其他的介面無影響,這對系統的擴充套件性、維護性都有非常大的幫助。看過電話這個例子後,是不是想反思一下了,我以前的設計是不是有點問題了?不,不是的,不要懷疑自己的技術能力,單一職責原則最難劃分的就是職責。一個職責一個介面,但問題是“職責”沒有一個量化的標準,一個類到底要負責那些職責?這些職責該怎麼細化?細化後是否都要有一個介面或類?這些都需要從實際的專案去考慮,從功能上來說,定義一個IPhone介面也沒有錯,實現了電話的功能,而且設計還很簡單,僅僅一個介面一個實現類,實際的專案我想大家都會這麼設計。專案要考慮可變因素和不可變因素,以及相關的收益成本比率,因此設計一個IPhone介面也可能是沒有錯的。但是,如果純從“學究”理論上分析就有問題了,有兩個可以變化的原因放到了一個介面中,這就為以後的變化帶來了風險。如果以後模擬電話升級到數字電話,我們提供的介面IPhone是不是要修改了?介面修改對其他的Invoker類是不是有很大影響?

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

3 我單純,所以我快樂

對於介面,我們在設計的時候一定要做到單一,但是對於實現類就需要多方面考慮了。生搬硬套單一職責原則會引起類的劇增,給維護帶來非常多的麻煩,而且過分細分類的職責也會人為地增加系統的複雜性。本來一個類可以實現的行為硬要拆成兩個類,然後再使用聚合或組合的方式耦合在一起,人為製造了系統的複雜性。所以原則是死的,人是活的,這句話很有道理。單一職責原則很難在專案中得到體現,非常難,為什麼?在國內,技術人員的地位和話語權都比較低,因此在專案中需要考慮環境,考慮工作量,考慮人員的技術水平,考慮硬體的資源情況,等等,最終妥協的結果是經常違背單一職責原則。而且,我們中華文明就有很多屬於混合型的產物,比如筷子,我們可以把筷子當做刀來使用,分割食物;還可以當叉使用,把食物從盤子中移動到口中。而在西方的文化中,刀就是刀,叉就是叉,你去吃西餐的時候這兩樣肯定都是有的,刀就是切割食物,叉就是固定食物或者移動食物,分工很明晰。這種文化的差異很難一步改造過來,但是我相信隨著技術的深入,單一職責原則必然會深入到專案的設計中,而且這個原則是那麼的簡單,簡單得不需要我們更加深入地思考,單從字面上大家都應該知道是什麼意思,單一職責嘛!單一職責適用於介面、類,同時也適用於方法,什麼意思呢?一個方法儘可能做一件事情,比如一個方法修改使用者密碼,不要把這個方法放到“修改使用者資訊”方法中,這個方法的顆粒度很粗,比如圖1-7中所示的方法


在IUserManager中定義了一個方法changeUser,根據傳遞的型別不同,把可變長度引數changeOptions修改到userBO這個物件上,並呼叫持久層的方法儲存到資料庫中。在我的專案組中,如果有人寫了這樣一個方法,我不管他寫了多少程式,花了多少工夫,一律重寫!原因很簡單:方法職責不清晰,不單一,不要讓別人猜測這個方法可能是用來處理什麼邏輯的。比較好的設計如圖1-8所示。通過類圖可知,如果要修改使用者名稱稱,就呼叫changeUserName方法;要修改家庭地址,就呼叫changeHomeAddress方法;要修改單位電話,就呼叫changeOfficeTel方法。每個方法的職責非常清晰明確,不僅開發簡單,而且日後的維護也非常容易,大家可以逐漸養成這樣的習慣。1.4 最佳實踐

4 最佳實踐

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