設計模式---訂閱發布模式(Subscribe/Publish)


訂閱發布模式定義了一種一對多的依賴關系,讓多個訂閱者對象同時監聽某一個主題對象。這個主題對象在自身狀態變化時,會通知所有訂閱者對象,使它們能夠自動更新自己的狀態。

       將一個系統分割成一系列相互協作的類有一個很不好的副作用,那就是需要維護相應對象間的一致性,這樣會給維護、擴展和重用都帶來不便。當一個對象的改變需要同時改變其他對象,而且它不知道具體有多少對象需要改變時,就可以使用訂閱發布模式了。

       一個抽象模型有兩個方面,其中一方面依賴於另一方面,這時訂閱發布模式可以將這兩者封裝在獨立的對象中,使它們各自獨立地改變和復用。訂閱發布模式所做的工作其實就是在解耦合。讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響另一邊的變化。

在我們日常寫程序時,經常遇到下面這種情況:

public void 有告警信息產生() {     刷新界面();     更新數據庫();     給管理員發Mail();     ……………………………… }

當有告警信息產生時,依次要去執行:刷新界面()、更新數據庫()、給管理員發Mail()等操作。表面上看代碼寫得很工整,其實這里面問題多多:

  • 首先,這完全是面向過程開發,根本不適合大型項目。
  • 第二,代碼維護量太大。設想一下,如果產生告警后要執行10多個操作,那這將是個多么大,多少復雜的類呀,時間一長,可能連開發者自己都不知道如何去維護了。
  • 第三,擴展性差。如果產生告警后,要增加一個聲音提示()功能,怎么辦呢?沒錯,只能加在有告警信息產生()這個函數中,這樣一來,就違反了“開放-關閉原則”。而且修改了原有的函數,那么在測試時,除了要測新增功能外,還要做原功能的回歸測試;在一個大型項目中,做一次回歸測試可能要花費大約兩周左右的時間,而且前提是新增功能沒有影響原來功能及產生新的bug。

那么如何把有告警信息產生()函數同其他函數進行解耦合呢?別着急,下面就介紹今天的主角----訂閱發布模式。見下圖:

訂閱發布模式1

上面的流程就是對有告警信息產生()這個函數的描述。我們要做的,就是把產生告警和它需要通知的事件進行解耦,讓它們之間沒有相互依賴的關系,解耦合圖如下:

訂閱發布模式2

事件觸發者被抽象出來,稱為消息發布者,即圖中的P。事件接受都被抽象出來,稱為消息訂閱者,即圖中的S。P與S之間通過S.P(即訂閱器)連接。這樣就實現了P與S的解耦。首先,P就把消息發送到指定的訂閱器上,從始至終,它並不知道也不關心要把消息發向哪個S。S如果想接收消息,就要向訂閱器進行訂閱,訂閱成功后,S就可以接收來自S.P的消息了,從始至終,S並不知道也不關心消息來源於哪個具體的P。同理,S還可以向S.P進行退訂操作,成功退訂后,S就無法接收到來自指定S.P的消息了。這樣就完美的解決了P與S之間的解耦。

等等,好像還有一個問題。從圖上看,雖然P與S之間完成了解耦,但是P與S.P,S與S.P之間不就又耦合上了嗎?其實這個問題好解決,想想我們上篇的裝飾模式是怎么解耦的?對,就接口。這里我們同樣使用接口來解決P與S.P和S與S.P之間的解耦,同時,使用delegate來解決多訂閱多發布的機制。

下面給出實現代碼。由於訂閱發布模式涉及P, S.P和S三部份內容,所以代碼比較多,也比較長。請大家耐心閱讀。

首先,為了實現P與S.P, S與S.P之間的解耦,我們需要定義兩個接口文件

ISubscribe.cs
namespace TJVictor.DesignPattern.SubscribePublish
{
    //定義訂閱事件
    public delegate void SubscribeHandle(string str);
    //定義訂閱接口
    public interface ISubscribe
    {
        event SubscribeHandle SubscribeEvent;
    }
}


IPublish
namespace TJVictor.DesignPattern.SubscribePublish
{
    //定義發布事件
    public delegate void PublishHandle(string str);
    //定義發布接口
    public interface IPublish
    {
        event PublishHandle PublishEvent; 

        void Notify(string str);
    }
}

然后我們來設計訂閱器。顯然訂閱器要實現雙向解耦,就一定要繼承上面兩個接口,這也是我為什么用接口不用抽象類的原因(類是單繼承)。


namespace TJVictor.DesignPattern.SubscribePublish
{
    public class SubPubComponet : ISubscribe, IPublish
    {
        private string _subName;
        public SubPubComponet(string subName)
        {
            this._subName = subName;
            PublishEvent += new PublishHandle(Notify);
        } 

        #region ISubscribe Members
        event SubscribeHandle subscribeEvent;
        event SubscribeHandle ISubscribe.SubscribeEvent
        {
            add { subscribeEvent += value; }
            remove { subscribeEvent -= value; }
        }
        #endregion 

        #region IPublish Members
        public PublishHandle PublishEvent; 

        event PublishHandle IPublish.PublishEvent
        {
            add { PublishEvent += value; }
            remove { PublishEvent -= value; }
        }
        #endregion 

        public void Notify(string str)
        {
            if (subscribeEvent != null)
                subscribeEvent.Invoke(string.Format("消息來源{0}:消息內容:{1}", _subName, str));
        }
    }
}

接下來是設計訂閱者S。S類中使用了ISubscribe來與S.P進行解耦。代碼如下: 


namespace TJVictor.DesignPattern.SubscribePublish
{
    public class Subscriber
    {
        private string _subscriberName; 

        public Subscriber(string subscriberName)
        {
            this._subscriberName = subscriberName;
        } 

        public ISubscribe AddSubscribe { set { value.SubscribeEvent += Show; } }
        public ISubscribe RemoveSubscribe { set { value.SubscribeEvent -= Show; } } 

        private void Show(string str)
        {
            Console.WriteLine(string.Format("我是{0},我收到訂閱的消息是:{1}", _subscriberName, str));
        }
    }
}

最后是發布者P,繼承IPublish來對S.P發布消息通知。 


namespace TJVictor.DesignPattern.SubscribePublish
{
    public class Publisher:IPublish
    {
        private string _publisherName; 

        public Publisher(string publisherName)
        {
            this._publisherName = publisherName;
        } 

        private event PublishHandle PublishEvent;
        event PublishHandle IPublish.PublishEvent
        {
            add { PublishEvent += value; }
            remove { PublishEvent -= value; }
        } 

        public void Notify(string str)
        {
            if (PublishEvent != null)
                PublishEvent.Invoke(string.Format("我是{0},我發布{1}消息", _publisherName, str));
        }
    }
}

至此,一個簡單的訂閱發布模式已經完成了。下面是調用代碼及運行結果。調用代碼模擬了圖2中的訂閱發布關系,大家可以從代碼,運行結果和示例圖三方面對照着看。


#region TJVictor.DesignPattern.SubscribePublish
//新建兩個訂閱器
SubPubComponet subPubComponet1 = new SubPubComponet("訂閱器1");
SubPubComponet subPubComponet2 = new SubPubComponet("訂閱器2");
//新建兩個發布者
IPublish publisher1 = new Publisher("TJVictor1");
IPublish publisher2 = new Publisher("TJVictor2");
//與訂閱器關聯
publisher1.PublishEvent += subPubComponet1.PublishEvent;
publisher1.PublishEvent += subPubComponet2.PublishEvent;
publisher2.PublishEvent += subPubComponet2.PublishEvent;
//新建兩個訂閱者
Subscriber s1 = new Subscriber("訂閱人1");
Subscriber s2 = new Subscriber("訂閱人2");
//進行訂閱
s1.AddSubscribe = subPubComponet1;
s1.AddSubscribe = subPubComponet2;
s2.AddSubscribe = subPubComponet2;
//發布者發布消息
publisher1.Notify("博客1");
publisher2.Notify("博客2");
//發送結束符號
Console.WriteLine("".PadRight(50,'-'));
//s1取消對訂閱器2的訂閱
s1.RemoveSubscribe = subPubComponet2;
//發布者發布消息
publisher1.Notify("博客1");
publisher2.Notify("博客2");
//發送結束符號
Console.WriteLine("".PadRight(50, '-'));
#endregion 

#region Console.ReadLine();
Console.ReadLine();
#endregion

 

 運行結果圖:

訂閱發布模式3

 

 

如需轉載,請注明本文原創自CSDN TJVictor專欄:http://blog.csdn.net/tjvictor


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM