七种设计原则(二)单一职责原则
2019獨角獸企業重金招聘Python工程師標準>>>
1.定義
單一職責原則概念::規定一個類應該只有一個發生變化的原因。
There should never be more than one reason for a class to change.
?
2.單一職責理解:
從單一職責原則概念,我們將職責定義為一個變化的因素,如可這個類有多個變化因素,這個類就違背了單一職責原則。
在設計類的時候需要將不同的職責分離到單獨的類中。一個類只需要專一實現自身的職責,Do one thing and do it well。專注能保證對象的高內聚和細粒度,有利于對象的重用。
?
3.問題和解決方案:
例1
類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();//我要賦值了,我就認為它是一個純粹的BOIUserBO 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()??墒俏覀兝先藱C根本用不著實現這個功能,可是由于它實現了該接口,它的內部實現也得重新去寫。從這點來說,以上的設計還是存在它的問題。那么,我們如何繼續細化接口粒度呢?
最終設計
接口細化粒度設計如下
//手機基礎屬性接口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
轉載于:https://my.oschina.net/u/3701483/blog/1577552
總結
以上是生活随笔為你收集整理的七种设计原则(二)单一职责原则的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iBATIS In Action:使用映
- 下一篇: [2011山东ACM省赛] Identi