1. 程式人生 > >設計模式---訂閱釋出模式(Subscribe/Publish)

設計模式---訂閱釋出模式(Subscribe/Publish)

轉載自:http://blog.csdn.net/tjvictor/article/details/5223309

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

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

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

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

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