設計模式教程-開閉原則OCP

 

原文地址:http://www.hello-code.com/blog/design/201603/6001.html

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工具類

namespace Utility
{
    //發送郵件的類
    public class EmailMessage
    {
        //裏面是大量的SMTP發送郵件的邏輯

        //發送郵件的方法
        public void SendMessage(string strMsg)
        {
            Console.WriteLine("Email節日問候:" + strMsg);
        }
    }
}

1.2 MessageService服務

namespace Service
{
    public class MessageService
    {
        private EmailMessage emailHelper = null;
        public MessageService()
        {
            emailHelper = new EmailMessage();
        }

        //節日問候
        public void Greeting(string strMsg)
        {
            emailHelper.SendMessage(strMsg);
        }
    }
}

1.3 業務調用模塊

    class Program
    {
        static void Main(string[] args)
        {
            Service.MessageService oService = new Service.MessageService();
            oService.Greeting("祝大家中秋節快樂。");

            Console.ReadKey();
        }
    }

一切都很順利,系統也得到公司好評。

日復一日,年復一年,隨着時間的推移,公司發現郵件推送的方式也存在一些弊病,比如某些網絡不發達地區不能正常地收到郵件,並且在外出差人員有時不能正常收到郵件。這個時候公司領導發現短信推送是較好的解決辦法。於是乎,需求變更來了:增加短信推送節日祝福的功能,對於行政部等特殊部門保留郵件發送的方式。

研發部的同事們雖然已有微言,但是沒辦法,也只有咬着牙忙了,於是代碼變成了這樣。

1.1 工具類裏面增加了發送短信的幫助類

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判斷是哪種推送方式

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 業務調用模塊

    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 工具類

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服務

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 業務調用模塊

    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,稍作修改。

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函數裏面

    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,直接用配置文件注入的方式更加簡單。

發佈了56 篇原創文章 · 獲贊 12 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章