設計模式---訂閱釋出模式(Subscribe/Publish)
轉載自:http://blog.csdn.net/tjvictor/article/details/5223309
訂閱釋出模式定義了一種一對多的依賴關係,讓多個訂閱者物件同時監聽某一個主題物件。這個主題物件在自身狀態變化時,會通知所有訂閱者物件,使它們能夠自動更新自己的狀態。
將一個系統分割成一系列相互協作的類有一個很不好的副作用,那就是需要維護相應物件間的一致性,這樣會給維護、擴充套件和重用都帶來不便。當一個物件的改變需要同時改變其他物件,而且它不知道具體有多少物件需要改變時,就可以使用訂閱釋出模式了。
一個抽象模型有兩個方面,其中一方面依賴於另一方面,這時訂閱釋出模式可以將這兩者封裝在獨立的物件中,使它們各自獨立地改變和複用。訂閱釋出模式所做的工作其實就是在解耦合。讓耦合的雙方都依賴於抽象,而不是依賴於具體,從而使得各自的變化都不會影響另一邊的變化。
在我們日常寫程式時,經常遇到下面這種情況:
public void 有告警資訊產生()
{
重新整理介面();
更新資料庫();
給管理員發Mail();
………………………………
}
當有告警資訊產生時,依次要去執行:重新整理介面()、更新資料庫()、給管理員發Mail()等操作。表面上看程式碼寫得很工整,其實這裡面問題多多:
- 首先,這完全是面向過程開發,根本不適合大型專案。
- 第二,程式碼維護量太大。設想一下,如果產生告警後要執行10多個操作,那這將是個多麼大,多少複雜的類呀,時間一長,可能連開發者自己都不知道如何去維護了。
- 第三,擴充套件性差。如果產生告警後,要增加一個聲音提示()功能,怎麼辦呢?沒錯,只能加在有告警資訊產生()這個函式中,這樣一來,就違反了“開放-關閉原則”。而且修改了原有的函式,那麼在測試時,除了要測新增功能外,還要做原功能的迴歸測試;在一個大型專案中,做一次迴歸測試可能要花費大約兩週左右的時間,而且前提是新增功能沒有影響原來功能及產生新的bug。
那麼如何把有告警資訊產生()函式同其他函式進行解耦合呢?彆著急,下面就介紹今天的主角----訂閱釋出模式。見下圖:
上面的流程就是對有告警資訊產生()這個函式的描述。我們要做的,就是把產生告警和它需要通知的事件進行解耦,讓它們之間沒有相互依賴的關係,解耦合圖如下:
事件觸發者被抽象出來,稱為訊息釋出者,即圖中的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; }
}
#endregionpublic 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
執行結果圖: