出自書《設計模式之蟬》
單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP圣拄。這個設計原則備受爭議荣刑,只要你想和別人爭執(zhí)、慪氣或者是吵架与柑,這個則是屢試不爽的谤辜。如果你是老大,看到一個接口或類是這樣或那樣設計的价捧,你就問一句:“你設計的類符合SRP原則嗎丑念?”保準對方立馬“萎縮”掉,而且還一臉崇拜地看著你结蟋,心想:“老大確實英明”脯倚。這個原則存在爭議之處在哪里呢?就是對職責的定義嵌屎,什么是類的職責推正,以及怎么劃分類的職責。
我們先舉個例子來說明什么是單一職責原則宝惰。
只要做過項目植榕,肯定要接觸到用戶、機構尼夺、角色管理這些模塊尊残,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的訪問控制淤堵,通過分配和取消角色來完成用戶權限的授予和取消寝衫,使動作主體(用戶)與資源的行為(權限)分離),確實是一個很好的解決辦法拐邪。我們這里要講的是用戶管理慰毅、修改用戶的信息、增加機構(一個人屬于多個機構)扎阶、增加角色等事富,用戶有這么多的信息和行為要維護,我們就把這些寫到一個接口中乘陪,都是用戶管理類嘛统台,我們先來看它的類圖,如圖:
太Easy的類圖了啡邑,我相信贱勃,即使是一個初級的程序員也可以看出這個接口設計得有問題,用戶的屬性和用戶的行為沒有分開谤逼,這是一個嚴重的錯誤贵扰!這個接口確實設計得一團糟,應該把用戶的信息抽取成一個BO(Business Object流部,業(yè)務對象)戚绕,把行為抽取成一個Biz(Business Logic,業(yè)務邏輯)枝冀,按照這個思路對類圖進行修正舞丛,如圖:
重新拆封成兩個接口耘子,IUserBO負責用戶的屬性,簡單地說球切,IUserBO的職責就是收集和反饋用戶的屬性信息谷誓;IUserBiz負責用戶的行為,完成用戶信息的維護和變更吨凑。各位可能要說了捍歪,這個與我實際工作中用到的User類還是有差別的呀!別著急鸵钝,我們先來看一看分拆成兩個接口怎么使用糙臼。OK,我們現在是面向接口編程恩商,所以產生了這個UserInfo對象之后弓摘,當然可以把它當IUserBO接口使用。也可以當IUserBiz接口使用痕届,這要看你在什么地方使用了韧献。要獲得用戶信息,就當是IUserBO的實現類研叫;要是希望維護用戶的信息锤窑,就把它當作IUserBiz的實現類就成了,如代碼所示:
......
IUserInfo userInfo = new UserInfo();//我要賦值了嚷炉,我就認為它是一個純粹的BO
IUserBO userBO = (IUserBO)userInfo;userBO.setPassword("abc");//我要執(zhí)行動作了渊啰,我就認為是一個業(yè)務邏輯類
IUserBiz userBiz = (IUserBiz)userInfo;userBiz.deleteUser();
......
確實可以如此,問題也解決了申屹,但是我們來分析一下剛才的動作绘证,為什么要把一個接口拆分成兩個呢?其實哗讥,在實際的使用中嚷那,我們更傾向于使用兩個不同的類或接口:一個是IUserBO,一個是IUserBiz杆煞,類圖1所示:
以上我們把一個接口拆分成兩個接口的動作魏宽,就是依賴了單一職責原則,那什么是單一職責原則呢决乎?
單一職責原則的定義是:應該有且僅有一個原因引起類的變更队询。
解釋到這里,估計你已經很不屑了构诚,“切蚌斩!這么簡單的東西還要講?范嘱!”好送膳,我們來講點復雜的员魏。SRP的原話解釋是:
There should never be more than one reason for a class to change.
這句話初中生都能看懂,不多說肠缨,但是看懂是一碼事逆趋,實施就是另外一碼事了盏阶。上面講的例子很好理解晒奕,在實際項目中大家都已經這么做了,那我們再來看看下面這個例子是否好理解名斟。電話這玩意脑慧,是現代人都離不了,電話通話的時候有4個過程發(fā)生:撥號砰盐、通話闷袒、回應、掛機岩梳,那我們寫一個接口囊骤,其類圖2所示。
我不是有意要冒犯IPhone的冀值,同名純屬巧合也物,我們來看一個這個過程的代碼:
public interface IPhone {
????//撥通電話
????public void dial(String phoneNumber);
????//通話
????public void chat(Object o);
????//通話完畢,掛電話
????public void hangup();
}
實現類也比較簡單列疗,我就不再寫了滑蚯,大家看看這個接口有沒有問題?我相信大部分的讀者都會說這個沒有問題呀抵栈,以前我就是這么做的呀告材,某某書上也是這么寫的呀,還有什么什么的源碼也是這么寫的古劲!是的斥赋,這個接口接近于完美,看清楚了产艾,是“接近”灿渴!單一職責原則要求一個接口或類只有一個原因引起變化,也就是一個接口或類只有一個職責胰舆,它就負責一件事情骚露,看看上面的接口只負責一件事情嗎?是只有一個原因引起變化嗎缚窿?好像不是棘幸!IPhone這個接口可不是只有一個職責,它包含了兩個職責:一個是協(xié)議管理倦零,一個是數據傳送误续。dial()和hangup()兩個方法實現的是協(xié)議管理吨悍,分別負責撥號接通和掛機;chat()實現的是數據的傳送蹋嵌,把我們說的話轉換成模擬信號或數字信號傳遞到對方育瓜,然后再把對方傳遞過來的信號還原成我們聽得懂的語言。我們可以這樣考慮這個問題栽烂,協(xié)議接通的變化會引起這個接口或實現類的變化嗎躏仇?會的!那數據傳送(想想看腺办,電話不僅僅可以通話焰手,還可以上網)的變化會引起這個接口或實現類的變化嗎?會的怀喉!那就很簡單了书妻,這里有兩個原因都引起了類的變化。這兩個職責會相互影響嗎躬拢?電話撥號躲履,我只要能接通就成,甭管是電信的還是網通的協(xié)議聊闯;電話連接后還關心傳遞的是什么數據嗎工猜?通過這樣的分析,我們發(fā)現類圖上的IPhone接口包含了兩個職責馅袁,而且這兩個職責的變化不相互影響域慷,那就考慮拆分成兩個接口,其下圖所示:
這個類圖看上去有點復雜了汗销,完全滿足了單一職責原則的要求犹褒,每個接口職責分明,結構清晰弛针,但是我相信你在設計的時候肯定不會采用這種方式叠骑,一個手機類要把ConnectionManager和DataTransfer組合在一塊才能使用。組合是一種強耦合關系削茁,你和我都有共同的生命期宙枷,這樣的強耦合關系還不如使用接口實現的方式呢,而且還增加了類的復雜性茧跋,多了兩個類慰丛。經過這樣的思考后,我們再修改一下類圖瘾杭,如下圖:
這樣的設計才是完美的,一個類實現了兩個接口,把兩個職責融合在一個類中贤笆。你會覺得這個Phone有兩個原因引起變化了呀蝇棉,是的,但是別忘記了我們是面向接口編程芥永,我們對外公布的是接口而不是實現類篡殷。而且,如果真要實現類的單一職責埋涧,這個就必須使用上面的組合模式了板辽,這會引起類間耦合過重、類的數量增加等問題飞袋,人為地增加了設計的復雜性戳气。通過上面的例子链患,我們來總結一下單一職責原則有什么好處:
● 類的復雜性降低巧鸭,實現什么職責都有清晰明確的定義;
● 可讀性提高麻捻,復雜性降低纲仍,那當然可讀性提高了;
● 可維護性提高贸毕,可讀性提高,那當然更容易維護了;
● 變更引起的風險降低潦闲,變更是必不可少的牵啦,如果接口的單一職責做得好,一個接口修改只對相應的實現類有影響摊腋,對其他的接口無影響沸版,這對系統(tǒng)的擴展性、維護性都有非常大的幫助兴蒸。
看過電話這個例子后视粮,是不是想反思一下了,我以前的設計是不是有點問題了橙凳?不蕾殴,不是的,不要懷疑自己的技術能力岛啸,單一職責原則最難劃分的就是職責钓觉。一個職責一個接口,但問題是“職責”沒有一個量化的標準坚踩,一個類到底要負責那些職責荡灾?這些職責該怎么細化?細化后是否都要有一個接口或類?這些都需要從實際的項目去考慮卧晓,從功能上來說芬首,定義一個IPhone接口也沒有錯,實現了電話的功能逼裆,而且設計還很簡單郁稍,僅僅一個接口一個實現類,實際的項目我想大家都會這么設計胜宇。項目要考慮可變因素和不可變因素耀怜,以及相關的收益成本比率,因此設計一個IPhone接口也可能是沒有錯的桐愉。但是财破,如果純從“學究”理論上分析就有問題了,有兩個可以變化的原因放到了一個接口中从诲,這就為以后的變化帶來了風險左痢。如果以后模擬電話升級到數字電話,我們提供的接口IPhone是不是要修改了系洛?接口修改對其他的Invoker類是不是有很大影響俊性?
注意 !C璩丁定页!
單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否優(yōu)良绽诚,但是“職責”和“變化原因”都是不可度量的典徊,因項目而異,因環(huán)境而異恩够。
對于單一職責原則卒落,我的建議是接口一定要做到單一職責,類的設計盡量做到只有一個原因引起變化玫鸟。