前言:上篇小話設計模式原則之(1):依賴倒置原則DIP簡單介紹了下依賴倒置的由來以及使用,中間插了兩篇WebApi的文章,這篇還是迴歸正題,繼續來寫寫設計模式另一個重要的原則:單一職責原則。
一、原理介紹
1、官方定義
單一職責原則,英文縮寫SRP,全稱Single Responsibility Principle。
原始定義:There should never be more than one reason for a class to change。
官方翻譯:應該有且僅有一個原因引起類的變更。簡單點說,一個類,最好只負責一件事,只有一個引起它變化的原因。
2、自己理解
2.1、原理解釋
上面的定義不難理解,引起類變化的原因不能多於一個。也就是說每一個類只負責自己的事情,此所謂單一職責。
我們知道,在OOP裡面,高內聚、低耦合是軟體設計追求的目標,而單一職責原則可以看做是高內聚、低耦合的引申,將職責定義為引起變化的原因,以提高內聚性,以此來減少引起變化的原因。職責過多,可能引起變化的原因就越多,這將是導致職責依賴,相互之間就產生影響,從而極大的損傷其內聚性和耦合度。單一職責通常意味著單一的功能,因此不要為類實現過多的功能點,以保證實體只有一個引起它變化的原因。
不管是從官方定義,還是對“單一職責”名稱的解釋,都能很好的理解單一職責原則的意義。其實在軟體設計中,要真正用好單一職責原則並不簡單,因為遵循這一原則最關鍵的地方在於職責的劃分,博主的理解是職責的劃分是根據需求定的,同一個類(介面)的設計,在不同的需求裡面,可能職責的劃分並不一樣,為什麼這麼說呢?我們來看下面的例子。
二、場景示例
關於單一職責原則的原理,我們就不做過多的解釋了。重點是職責的劃分!重點是職責的劃分!重點是職責的劃分!重要的事情說三遍。下面根據一個示例場景來看看如何劃分職責。
假定現在有如下場景:國際手機運營商那裡定義了生產手機必須要實現的介面,介面裡面定義了一些手機的屬性和行為,手機生產商如果要生成手機,必須要實現這些介面。
1、初始設計——初稿
我們首先以手機作為單一職責去設計介面,方案如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/// /// 充電電源 /// 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(); } |
然後我們的手機生產商去實現這些介面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
//具體的手機示例 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(); } } |
這種設計有沒有問題呢?這是一個很有爭議的話題。單一職責原則要求一個介面或類只有一個原因引起變化,也就是一個介面或類只有一個職責,它就負責一件事情,原則上來說,我們以手機作為單一職責去設計,也是有一定的道理的,因為我們介面裡面都是定義的手機相關屬性和行為,引起介面變化的原因只可能是手機的屬性或者行為發生變化,從這方面考慮,這種設計是有它的合理性的,如果你能保證需求不會變化或者變化的可能性比較小,那麼這種設計就是合理的。但實際情況我們知道,現代科技日新月異,科技的進步促使著人們不斷在手機原有基礎上增加新的屬性和功能。比如有一天,我們給手機增加了攝像頭,那麼需要新增一個畫素的屬性,我們的介面和實現就得改吧,又有一天,我們增加移動辦公的功能,那麼我們的介面實現是不是也得改。由於上面的設計沒有細化到一定的粒度,導致任何一個細小的改動都會引起從上到下的變化,有一種“牽一髮而動全身”的感覺。所以需要細化粒度,下面來看看我們如何變更設計。
2、二次設計——變更
我們將介面細化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//手機屬性介面 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(); } |
實現類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
//手機屬性實現類 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()。可是我們老人機根本用不著實現這個功能,可是由於它實現了該介面,它的內部實現也得重新去寫。從這點來說,以上的設計還是存在它的問題。那麼,我們如何繼續細化介面粒度呢?
3、最終設計——成型
介面細化粒度設計如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//手機基礎屬性介面 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(); } |
實現類和上面類似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
//手機基礎屬性實現 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(); } } |
此種設計能解決上述問題,細分到此粒度,這種方案基本算比較完善了。能不能算完美?這個得另說。介面的粒度要設計到哪一步,取決於需求的變更程度,或者說取決於需求的複雜度。
三、總結
以上通過一個應用場景簡單介紹了下單一職責原則的使用,上面三種設計,沒有最合理,只有最合適。理解單一職責原則,最重要的就是理解職責的劃分,職責劃分的粒度取決於需求的粒度,最後又回到了那句話:沒有最好的設計,只有最適合的設計。
軟體設計原則系列文章