1.定義html
單一職責原則概念::規定一個類應該只有一個發生變化的緣由。編程
There should never be more than one reason for a class to change.
spa
2.單一職責理解:設計
從單一職責原則概念,咱們將職責定義爲一個變化的因素,如可這個類有多個變化因素,這個類就違背了單一職責原則。code
在設計類的時候須要將不一樣的職責分離到單獨的類中。一個類只須要專注實現自身的職責,Do one thing and do it well。專一能保證對象的高內聚和細粒度,有利於對象的重用。htm
3.問題和解決方案:對象
例1blog
類T負責兩個不一樣的職責:職責P1、職責P2。當因爲職責P1需求發生改變而須要修改類T時,有可能會致使原來運行的職責P2功能發生故障。解決方法:分別創建兩個類完成對應的功能。接口
只要作過項目,確定要接觸到用戶、機構、角色管理這些模塊,基本上使用的都是RBAC模型,確實是很好的一個解決辦法。咱們今天要講的是用戶管理、修改用戶的信息、增長機構(一我的屬於多個機構)、增長角色等,用戶有這麼的信息和行爲要維護,咱們就把這些寫到一個接口中,都是用戶管理類嘛,咱們先來看它的類圖:遊戲
太Easy的類圖了, 這個接口設計得有問題,用戶的屬性(Property)和用戶的行爲(Behavior)沒有分開,這是一個嚴重的錯誤!很是正確,這個接口確實設計得一團糟,應該把用戶的信息抽取成一個BO(Bussiness Object,業務對象),把行爲抽取成一個BIZ(Business Logic,業務邏輯),按照這個思路對類圖進行修正,以下圖:
從新拆封成兩個接口,IUserBO負責用戶的屬性,簡單地說,IUserBO的職責就是收集和反饋用戶的屬性信息;IUserBiz負責用戶的行爲,完成用戶信息的維護和變動。各位可能要說了,這個與我實際工做中用到的User類仍是有差異的呀!彆着急,咱們先來看一看分拆成兩個接口怎麼使用。OK,咱們如今是面向接口編程,因此產生了這個UserInfo對象以後,固然能夠把它當IUserBO接口使用。固然,也能夠當IUserBiz接口使用,這要看你在什麼地方使用了。要得到用戶信息,就當是IUserBO的實現類;要是但願維護用戶的信息,就把它看成IUserBiz的實現類就成了,代碼清單1-1所示。
IUserBiz userInfo = new UserInfo(); //我要賦值了,我就認爲它是一個純粹的BO IUserBO userBO = (IUserBO)userInfo; userBO.setPassword("abc"); //我要執行動做了,我就認爲是一個業務邏輯類 IUserBiz userBiz = (IUserBiz)userInfo; userBiz.deleteUser(); .......
確實能夠如此,問題也解決了,可是咱們來回想一下咱們剛纔的動做,爲何要把一個接口拆分紅兩個呢?其實,在實際的使用中,咱們更傾向於使用兩個不一樣的類或接口:一個是IUserBO, 一個是IUserBiz,類圖應該以下圖所示。
上圖就是項目中經常使用的SPR(There should never be more than one reason for a class to change)類圖;
以上咱們把一個接口拆分紅兩個接口的動做,就是依賴了單一職責原則,那什麼是單一職責原則呢?單一職責原則的定義是:應該有且僅有一個緣由引發類的變動。
實現類也比較簡單,我就再也不寫了,你們看看這個接口有沒有問題?我相信大部分的讀者都會說這個沒有問題呀,之前我就是這麼作的呀,某某書上也是這麼寫的呀,還有什麼什麼的源碼也是這麼寫的!是的,這個接口接近於完美,看清楚了,是「接近」!單一職責原則要求一個接口或類只有一個緣由引發變化,也就是一個接口或類只有一個職責,它就負責一件事情,看看上面的接口只負責一件事情嗎?是隻有一個緣由引發變化嗎?好像不是!
IPhone這個接口可不是隻有一個職責,它包含了兩個職責:一個是協議管理,一個是數據傳送。diag()和huangup()兩個方法實現的是協議管理,分別負責撥號接通和掛機;chat()和answer()是數據的傳送,把咱們說的話轉換成模擬信號或數字信號傳遞到對方,而後再把對方傳遞過來的信號還原成咱們聽得懂語言。咱們能夠這樣考慮這個問題,協議接通的變化會引發這個接口或實現類的變化嗎?會的!那數據傳送(想一想看,電話不只僅能夠通話,還能夠上網)的變化會引發這個接口或實現類的變化嗎?會的!那就很簡單了,這裏有兩個緣由都引發了類的變化,並且這兩個職責會相互影響嗎?電話撥號,我只要能接通就成,甭管是電信的仍是網通的協議;電話鏈接後還關心傳遞的是什麼數據嗎?不關心,你要是樂意使用56K的小貓傳遞一個高清的片子,那也沒有問題(頂多有人說你13了)。經過這樣的分析,咱們發現類圖上的IPhone接口包含了兩個職責,並且這兩個職責的變化不相互影響,那就考慮拆開成兩個接口,其類圖以下圖所示。
這個類圖看着有點複雜了,徹底知足了單一職責原則的要求,每一個接口職責分明,結構清晰,可是我相信你在設計的時候確定不會採用這種方式,一個手機類要把ConnectionManager和DataTransfer組合在一塊才能使用。組合是一種強耦合關係,你和我都有共同的生命期,這樣的強耦合關係還不如使用接口實現的方式呢,並且還增長了類的複雜性,多了兩個類。通過這樣的思考後,咱們再修改一下類圖,以下圖所示。
簡潔清晰、職責分明的電話類圖
這樣的設計纔是完美的,一個類實現了兩個接口,把兩個職責融合在一個類中。你會以爲這個Phone有兩個緣由引發變化了呀,是的是的,可是別忘記了咱們是面向接口編程,咱們對外公佈的是接口而不是實現類。並且,若是真要實現類的單一職責,這個就必須使用上面的組合模式了,這會引發類間耦合太重、類的數量增長等問題,人爲的增長了設計的複雜性。
看過電話這個例子後,是否是有點反思了,我之前的設計是否是有點的問題了?不,不是的,不要懷疑本身的技術能力,單一職責原則最難劃分的就是職責。一個職責一個接口,但問題是「職責」是一個沒有量化的標準,一個類到底要負責那些職責?這些職責該怎麼細化?細化後是否都要有一個接口或類?這些都須要從實際的項目去考慮,從功能上來講,定義一個IPhone接口也沒有錯,實現了電話的功能,並且設計還很簡單,僅僅一個接口一個實現類,實際的項目我想你們都會這麼設計。項目要考慮可變因素和不可變因素,以及相關的收益成本比率,所以設計一個IPhone接口也多是沒有錯的。可是,若是純從「學究」理論上分析就有問題了,有兩個能夠變化的緣由放到了一個接口中,這就爲之後的變化帶來了風險。若是之後模擬電話升級到數字電話,咱們提供的接口IPhone是否是要修改了?接口修改對其餘的Invoker類是否是有很大影響?!
注意 單一職責原則提出了一個編寫程序的標準,用「職責」或「變化緣由」來衡量接口或類設計得是否有優良,可是「職責」和「變化緣由」都是不可度量的,因項目而異,因環境而異。
例2
再好比:生產手機
假定如今有以下場景:國際手機運營商那裏定義了生產手機必需要實現的接口,接口裏面定義了一些手機的屬性和行爲,手機生產商若是要生產手機,必需要實現這些接口。
咱們首先以手機做爲單一職責去設計接口,方案以下。
/// <summary> /// 充電電源類 /// </summary> public class ElectricSource { }
public interface IMobilePhone { //運行內存 string RAM { get; set; } //手機存儲內存 string ROM { get; set; } //CPU主頻 string CPU { get; set; } //屏幕大小 int Size { get; set; } //手機充電接口 void Charging(ElectricSource oElectricsource); //打電話 void RingUp(); //接電話 void ReceiveUp(); //上網 void SurfInternet(); }
而後咱們的手機生產商去實現這些接口
//具體的手機示例 public class MobilePhone:IMobilePhone { public string RAM { get {throw new NotImplementedException();} set{ throw new NotImplementedException();} } public string ROM { get{throw new NotImplementedException();} set{ throw new NotImplementedException();} } public string CPU { get{ throw new NotImplementedException();} set{ throw new NotImplementedException();} } public int Size { get{throw new NotImplementedException();} set{throw new NotImplementedException();} } public void Charging(ElectricSource oElectricsource) { throw new NotImplementedException(); } public void RingUp() { throw new NotImplementedException(); } public void ReceiveUp() { throw new NotImplementedException(); } public void SurfInternet() { throw new NotImplementedException(); } }
這種設計有沒有問題呢?這是一個頗有爭議的話題。單一職責原則要求一個接口或類只有一個緣由引發變化,也就是一個接口或類只有一個職責,它就負責一件事情,原則上來講,咱們以手機做爲單一職責去設計,也是有必定的道理的,由於咱們接口裏面都是定義的手機相關屬性和行爲,引發接口變化的緣由只多是手機的屬性或者行爲發生變化,從這方面考慮,這種設計是有它的合理性的,若是你能保證需求不會變化或者變化的可能性比較小,那麼這種設計就是合理的。但實際狀況咱們知道,現代科技突飛猛進,科技的進步促使着人們不斷在手機原有基礎上增長新的屬性和功能。好比有一天,咱們給手機增長了攝像頭,那麼須要新增一個像素的屬性,咱們的接口和實現就得改吧,又有一天,咱們增長移動辦公的功能,那麼咱們的接口實現是否是也得改。因爲上面的設計沒有細化到必定的粒度,致使任何一個細小的改動都會引發從上到下的變化,有一種「牽一髮而動全身」的感受。因此須要細化粒度,下面來看看咱們如何變動設計。
二次設計 變動:
咱們將接口細化
//手機屬性接口 public interface IMobilePhoneProperty { //運行內存 string RAM { get; set; } //手機存儲內存 string ROM { get; set; } //CPU主頻 string CPU { get; set; } //屏幕大小 int Size { get; set; } //攝像頭像素 string Pixel { get; set; } } //手機功能接口 public interface IMobilePhoneFunction { //手機充電接口 void Charging(ElectricSource oElectricsource); //打電話 void RingUp(); //接電話 void ReceiveUp(); //上網 void SurfInternet(); //移動辦公 void MobileOA(); }
實現類
//手機屬性實現類 public class MobileProperty:IMobilePhoneProperty { public string RAM { get{ throw new NotImplementedException();} set{ throw new NotImplementedException();} } public string ROM { get{ throw new NotImplementedException();} set{ throw new NotImplementedException();} } public string CPU { get{ throw new NotImplementedException();} set{throw new NotImplementedException();} } public int Size { get{throw new NotImplementedException();} set{throw new NotImplementedException();} } public string Pixel { get{throw new NotImplementedException();} set{throw new NotImplementedException();} } } //手機功能實現類 public class MobileFunction:IMobilePhoneFunction { public void Charging(ElectricSource oElectricsource) { throw new NotImplementedException(); } public void RingUp() { throw new NotImplementedException(); } public void ReceiveUp() { throw new NotImplementedException(); } public void SurfInternet() { throw new NotImplementedException(); } public void MobileOA() { throw new NotImplementedException(); } } //具體的手機實例 public class HuaweiMobile { private IMobilePhoneProperty m_Property; private IMobilePhoneFunction m_Func; public HuaweiMobile(IMobilePhoneProperty oProperty, IMobilePhoneFunction oFunc) { m_Property = oProperty; m_Func = oFunc; } }
對於上面題的問題,這種設計可以比較方便的解決,若是是增長屬性,只須要修改IMobilePhoneProperty和MobileProperty便可;若是是增長功能,只須要修改IMobilePhoneFunction和MobileFunction便可。貌似完勝第一種解決方案。那麼是否這種解決方案就完美了呢?答案仍是看狀況。原則上,咱們將手機的屬性和功能分開了,使得職責更加明確,全部的屬性都由IMobilePhoneProperty接口負責,全部的功能都由IMobilePhoneFunction接口負責,若是是需求的粒度僅僅到了屬性和功能這一級,這種設計確實是比較好的。反之,若是粒度再細小一些呢,那咱們這種職責劃分是否完美呢?好比咱們普通的老人機只須要一些最基礎的功能,好比它只須要充電、打電話、接電話的功能,可是按照上面的設計,它也要實現IMobilePhoneFunction接口,某一天,咱們增長了一個新的功能玩遊戲,那麼咱們就須要在接口上面增長一個方法PlayGame()。但是咱們老人機根本用不着實現這個功能,但是因爲它實現了該接口,它的內部實現也得從新去寫。從這點來講,以上的設計仍是存在它的問題。那麼,咱們如何繼續細化接口粒度呢?
最終設計
接口細化粒度設計以下
//手機基礎屬性接口 public interface IMobilePhoneBaseProperty { //運行內存 string RAM { get; set; } //手機存儲內存 string ROM { get; set; } //CPU主頻 string CPU { get; set; } //屏幕大小 int Size { get; set; } } //手機擴展屬性接口 public interface IMobilePhoneExtentionProperty { //攝像頭像素 string Pixel { get; set; } } //手機基礎功能接口 public interface IMobilePhoneBaseFunc { //手機充電接口 void Charging(ElectricSource oElectricsource); //打電話 void RingUp(); //接電話 void ReceiveUp(); } //手機擴展功能接口 public interface IMobilePhoneExtentionFunc { //上網 void SurfInternet(); //移動辦公 void MobileOA(); //玩遊戲 void PlayGame(); }
實現類和上面相似
//手機基礎屬性實現 public class MobilePhoneBaseProperty : IMobilePhoneBaseProperty { public string RAM { get{throw new NotImplementedException();} set{throw new NotImplementedException();} } public string ROM { get{throw new NotImplementedException();} set {throw new NotImplementedException();} } public string CPU { get{throw new NotImplementedException();} set{ throw new NotImplementedException();} } public int Size { get{ throw new NotImplementedException();} set{ throw new NotImplementedException();} } } //手機擴展屬性實現 public class MobilePhoneExtentionProperty : IMobilePhoneExtentionProperty { public string Pixel { get{ throw new NotImplementedException();} set{ throw new NotImplementedException();} } } //手機基礎功能實現 public class MobilePhoneBaseFunc : IMobilePhoneBaseFunc { public void Charging(ElectricSource oElectricsource) { throw new NotImplementedException(); } public void RingUp() { throw new NotImplementedException(); } public void ReceiveUp() { throw new NotImplementedException(); } } //手機擴展功能實現 public class MobilePhoneExtentionFunc : IMobilePhoneExtentionFunc { public void SurfInternet() { throw new NotImplementedException(); } public void MobileOA() { throw new NotImplementedException(); } public void PlayGame() { throw new NotImplementedException(); } }
此種設計能解決上述問題,細分到此粒度,這種方案基本算比較完善了。能不能算完美?這個得另說。接口的粒度要設計到哪一步,取決於需求的變動程度,或者說取決於需求的複雜度
由於每個職責都是變化的中心。當需求變動時,這個變化將經過更改職責相關的類來實現。若是一個類擁有多個職責,那麼這個類在變動的時候可能會影響到其餘的類,產生沒法預期的破壞。因此單一職責原則有利於對象的穩定,讓多個對象負責各自的職責,而後對象之間進行協做要比一個對象負責多個職責強的多,方法之間也是這樣。
4.優勢:
類的複雜性下降,實現什麼職責都有清晰明確的定義;
可讀性提升,複雜性下降,那固然可讀性提升了;
可維護性提升,那固然了,可讀性提升,那固然更容易維護了;
變動引發的風險下降,變動是必不可少的,若是接口的單一職責作得好,一個接口修改只對相應的實現類有影響,對其餘的接口無影響,這對系統的擴展性、維護性都有很是大幫助。
5.難點:職責的劃分
類的設計儘可能作到只有一個緣由引發變化。單一職責原則是一個很是簡單的原則,但一般也是最難作的正確的一個原則。職責的聯合是在實踐中常常碰到的事情。理論是理論,實踐是實踐。要考慮相關因素和收益等。
6.總結 :
以上經過一個應用場景簡單介紹了下單一職責原則的使用,類的設計儘可能作到只有一個緣由引發變化。上面三種設計,沒有最合理,只有最合適。理解單一職責原則,最重要的就是理解職責的劃分,職責劃分的粒度取決於需求的粒度,沒有最好的設計,只有最適合的設計。
Do one thing,and do it well~.
參考連接:
https://zhuanlan.zhihu.com/p/24198903
https://www.cnblogs.com/cbf4life/archive/2009/12/11/1622166.html