一、原理介紹
1、官方定義
開閉原則,英文縮寫OCP,全稱Open Closed Principle。
原始定義:Software entities (classes, modules, functions) should be open for extension but closed for modification。
字面翻譯:軟體實體(包括類、模組、功能等)應該對擴充套件開放,但是對修改關閉。
2、自己理解
2.1、原理解釋
- 對擴充套件開放。模組對擴充套件開放,就意味著需求變化時,可以對模組擴充套件,使其具有滿足那些改變的新行為。換句話說,模組通過擴充套件的方式去應對需求的變化。
- 對修改關閉。模組對修改關閉,表示當需求變化時,關閉對模組原始碼的修改,當然這裡的“關閉”應該是儘可能不修改的意思,也就是說,應該儘量在不修改原始碼的基礎上面擴充套件元件。
2.2、為什麼要“開”和“閉”
一般情況,我們接到需求變更的通知,通常方式可能就是修改模組的原始碼,然而修改已經存在的原始碼是存在很大風險的,尤其是專案上線執行一段時間後,開發人員發生變化,這種風險可能就更大。所以,為了避免這種風險,在面對需求變更時,我們一般不修改原始碼,即所謂的對修改關閉。不允許修改原始碼,我們如何應對需求變更呢?答案就是我們下面要說的對擴充套件開放。
通過擴充套件去應對需求變化,就要求我們必須要面向介面程式設計,或者說面向抽象程式設計。所有引數型別、引用傳遞的物件必須使用抽象(介面或者抽象類)的方式定義,不能使用實現類的方式定義;通過抽象去界定擴充套件,比如我們定義了一個介面A的引數,那麼我們的擴充套件只能是介面A的實現類。總的來說,開閉原則提高系統的可維護性和程式碼的重用性。
二、場景示例
1、對實現類程式設計,你死得很慘
下面就結合之前博主在園子裡面看到的一個使用場景來一步步呈現使用實現類程式設計的弊端。
場景說明:馬上中秋節了, **公司希望研發部門研發一套工具,實現給公司所有員工傳送祝福郵件。
接到開發需求,研發部立刻開會成立研發小組,進入緊張的開發階段,經過1個月的艱苦奮戰,系統順利上線。程式碼實現如下:
1.1 EmailMessage工具類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
namespace Utility { //傳送郵件的類 public class EmailMessage { //裡面是大量的SMTP傳送郵件的邏輯 //傳送郵件的方法 public void SendMessage(string strMsg) { Console.WriteLine("Email節日問候:" + strMsg); } } } |
1.2 MessageService服務
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace Service { public class MessageService { private EmailMessage emailHelper = null; public MessageService() { emailHelper = new EmailMessage(); } //節日問候 public void Greeting(string strMsg) { emailHelper.SendMessage(strMsg); } } } |
1.3 業務呼叫模組
1 2 3 4 5 6 7 8 9 10 |
class Program { static void Main(string[] args) { Service.MessageService oService = new Service.MessageService(); oService.Greeting("祝大家中秋節快樂。"); Console.ReadKey(); } } |
一切都很順利,系統也得到公司好評。
日復一日,年復一年,隨著時間的推移,公司發現郵件推送的方式也存在一些弊病,比如某些網路不發達地區不能正常地收到郵件,並且在外出差人員有時不能正常收到郵件。這個時候公司領導發現簡訊推送是較好的解決辦法。於是乎,需求變更來了:增加簡訊推送節日祝福的功能,對於行政部等特殊部門保留郵件傳送的方式。
研發部的同事們雖然已有微言,但是沒辦法,也只有咬著牙忙了,於是程式碼變成了這樣。
1.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 |
namespace Utility { //傳送郵件的類 public class EmailMessage { //裡面是大量的SMTP傳送郵件的邏輯 //傳送郵件的方法 public void SendMessage(string strMsg) { Console.WriteLine("Email節日問候:" + strMsg); } } //傳送簡訊的類 public class PhoneMessage { //手機端傳送簡訊的業務邏輯 //傳送簡訊的方法 public void SendMessage(string strMsg) { Console.WriteLine("簡訊節日問候:" + strMsg); } } } |
1.2 MessageService服務裡面增加了一個列舉型別MessageType判斷是哪種推送方式
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 |
namespace Service { public enum MessageType { Email, Phone } public class MessageService { private EmailMessage emailHelper = null; private PhoneMessage phoneHelper = null; private MessageType m_oType; public MessageService(MessageType oType) { m_oType = oType; if (oType == MessageType.Email) { emailHelper = new EmailMessage(); } else if (oType == MessageType.Phone) { phoneHelper = new PhoneMessage(); } } //節日問候 public void Greeting(string strMsg) { if (m_oType == MessageType.Email) { emailHelper.SendMessage(strMsg); } else if (m_oType == MessageType.Phone) { phoneHelper.SendMessage(strMsg); } } } } |
1.3 業務呼叫模組
1 2 3 4 5 6 7 8 9 10 11 12 |
class Program { static void Main(string[] args) { Service.MessageService oEmaliService = new Service.MessageService(Service.MessageType.Email); oEmaliService.Greeting("祝大家中秋節快樂。"); Service.MessageService oPhoneService = new Service.MessageService(Service.MessageType.Phone); oPhoneService.Greeting("祝大家中秋節快樂。"); Console.ReadKey(); } } |
經過一段時間的加班、趕進度。終於大功告成。
隨著公司的不斷髮展,很多產品、平臺都融入了微信的功能,於是乎公司領導又希望在保證原有功能的基礎上增加微信的推送方式。這個時候研發部的同事們就怨聲載道了,這樣一年改一次,何時是個頭?並且隨著時間的推移,研發部員工可能發生過多次變換,現在維護這個系統的員工早已不是當初的開發者,在別人的程式碼上面改功能,做過開發的應該都知道,簡直苦不堪言,因為你不知道別人哪裡會給你埋一個“坑”。並且在現有程式碼上面改,也存在很大的風險,即使做好之後所有的功能都必須重新經過嚴格的測試。
事情發展到這裡,就可以看出使用實現類去程式設計,你會因為需求變更而死得很慘,這個時候我們就能看出遵守開閉原則的重要性了,如果這個系統設計之初就能考慮這個原則,所有的可變變數使用抽象去定義,可能效果截然不同。
2、對抽象程式設計,就是這麼靈活
如果專案設計之初我們定義一個ISendable介面,我們看看效果怎樣呢?
2.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 33 34 35 36 37 38 39 40 41 42 43 44 |
namespace IHelper { public interface ISendable { void SendMessage(string strMsg); } } namespace Utility {//傳送郵件的類 public class EmailMessage:ISendable { //裡面是大量的SMTP傳送郵件的邏輯 //傳送郵件的方法 public void SendMessage(string strMsg) { Console.WriteLine("Email節日問候:" + strMsg); } } //傳送簡訊的類 public class PhoneMessage:ISendable { //手機端傳送簡訊的業務邏輯 //傳送簡訊的方法 public void SendMessage(string strMsg) { Console.WriteLine("簡訊節日問候:" + strMsg); } } //傳送微信的類 public class WeChatMessage:ISendable { //微信訊息推送業務邏輯 //傳送微信訊息的方法 public void SendMessage(string strMsg) { Console.WriteLine("簡訊節日問候:" + strMsg); } } } |
2.2 MessageService服務
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
namespace Service { public class MessageService { private ISendable m_oSendHelper = null; public MessageService(ISendable oSendHelper) { m_oSendHelper = oSendHelper; } //節日問候 public void Greeting(string strMsg) { m_oSendHelper.SendMessage(strMsg); } } } |
2.3 業務呼叫模組
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Program { static void Main(string[] args) { var strMsg = "祝大家中秋節快樂。"; ISendable oEmailHelper = new EmailMessage(); Service.MessageService oEmaliService = new Service.MessageService(oEmailHelper); oEmaliService.Greeting(strMsg); ISendable oPhoneHelper = new PhoneMessage(); Service.MessageService oPhoneService = new Service.MessageService(oPhoneHelper); oPhoneService.Greeting(strMsg); ISendable oWeChatHelper = new WeChatMessage(); Service.MessageService oWeChatService = new Service.MessageService(oWeChatHelper); oWeChatService.Greeting(strMsg); Console.ReadKey(); } } |
設計分析:在MessageService服務類中,我們定義了ISendable的介面變數m_oSendHelper,通過這個介面變數,我們就能很方便的通過擴充套件去應對需求的變化,而不必修改原來的程式碼。比如,我們現在再增加一種新的推送方式,對於我們的MessageService服務類來說,不用做任何修改,只需要擴充套件新的推送訊息的工具類即可。從需要抽象的角度來說,開閉原則和依賴倒置原則也有一定的相似性,不過博主覺得,開閉原則更加偏向的是使用抽象來避免修改原始碼,主張通過擴充套件去應對需求變更,而依賴倒置更加偏向的是層和層之間的解耦。當然,我們也不必分得那麼細,往往,一個好的設計肯定是遵循了多個設計原則的。
上面的設計,很好的解決了MessageService服務類中的問題,但是對於呼叫方(比如上文中的Main函式裡面),很顯然是違背了依賴倒置原則的,因為它既依賴介面層ISendable,又依賴介面實現層EmailMessage、PhoneMessage等。這肯定是不合適的。我們引入MEF,稍作修改。
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 |
namespace Utility { //傳送郵件的類 [Export("Email", typeof(ISendable))] public class EmailMessage:ISendable { //裡面是大量的SMTP傳送郵件的邏輯 //傳送郵件的方法 public void SendMessage(string strMsg) { Console.WriteLine("Email節日問候:" + strMsg); } } //傳送簡訊的類 [Export("Phone", typeof(ISendable))] public class PhoneMessage:ISendable { //手機端傳送簡訊的業務邏輯 //傳送簡訊的方法 public void SendMessage(string strMsg) { Console.WriteLine("簡訊節日問候:" + strMsg); } } //傳送微信的類 [Export("WeChat", typeof(ISendable))] public class WeChatMessage:ISendable { //微信訊息推送業務邏輯 //傳送微信訊息的方法 public void SendMessage(string strMsg) { Console.WriteLine("簡訊節日問候:" + strMsg); } } } |
Main函式裡面
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 |
class Program { [Import("Email",typeof(ISendable))] public ISendable oEmailHelper { get; set; } [Import("Phone", typeof(ISendable))] public ISendable oPhoneHelper { get; set; } [Import("WeChat", typeof(ISendable))] public ISendable oWeChatHelper { get; set; } static void Main(string[] args) { //使用MEF裝配元件 var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); var oProgram = new Program(); container.ComposeParts(oProgram); var strMsg = "祝大家中秋節快樂。"; Service.MessageService oEmaliService = new Service.MessageService(oProgram.oEmailHelper); oEmaliService.Greeting(strMsg); Service.MessageService oPhoneService = new Service.MessageService(oProgram.oPhoneHelper); oPhoneService.Greeting(strMsg); Service.MessageService oWeChatService = new Service.MessageService(oProgram.oWeChatHelper); oWeChatService.Greeting(strMsg); Console.ReadKey(); } } |
如果你使用Unity,直接用配置檔案注入的方式更加簡單。
三、總結
至此開閉原則的示例就基本完了。文中觀點有不對的地方,歡迎指出,博主在此多謝了。
軟體設計原則系列文章