事件總線這個概念對你來講可能很陌生,但提到觀察者(發佈-訂閱)模式,你也許就很熟悉。事件總線是對發佈-訂閱模式的一種實現。它是一種集中式事件處理機制,容許不一樣的組件之間進行彼此通訊而又不須要相互依賴,達到一種解耦的目的。git
咱們來看看事件總線的處理流程:github
瞭解了事件總線的基本概念和處理流程,下面咱們就來分析下如何去實現事件總線。安全
在動手實現事件總線以前,咱們仍是要追本溯源,探索一下事件的本質和發佈訂閱模式的實現機制。併發
咱們先來探討一下事件的概念。都是讀過書的,應該都還記得記敘文的六要素:時間、地點、人物、事件(原由、通過、結果)。app
咱們拿註冊的案例,來解釋一下。
用戶輸入用戶名、郵箱、密碼後,點擊註冊,輸入無誤校驗經過後,註冊成功併發送郵件給用戶,要求用戶進行郵箱驗證激活。dom
這裏面就涉及了兩個主要事件:函數
其實這六要素也適用於咱們程序中事件的處理過程。開發過WinForm程序的都知道,咱們在作UI設計的時候,從工具箱拖入一個註冊按鈕(btnRegister),雙擊它,VS就會自動幫咱們生成以下代碼:工具
void btnRegister_Click(object sender, EventArgs e) { // 事件的處理 }
其中object sender
指代發出事件的對象,這裏也就是button對象;EventArgs e
事件參數,能夠理解爲對事件的描述 ,它們能夠統稱爲事件源。其中的代碼邏輯,就是對事件的處理。咱們能夠統稱爲事件處理。this
說了這麼多,無非是想透過現象看本質:事件是由事件源和事件處理組成。
定義對象間一種一對多的依賴關係,使得每當一個對象改變狀態,則全部依賴於它的對象都會獲得通知並被自動更新。 ——發佈訂閱模式
發佈訂閱模式主要有兩個角色:
發佈訂閱模式有兩種實現方式:
總的來講,發佈訂閱模式中有兩個關鍵字,通知和更新。
被觀察者狀態改變通知觀察者作出相應更新。
解決的是當對象改變時須要通知其餘對象作出相應改變的問題。
若是畫一個圖來表示這個流程的畫,圖形應該是這樣的:
相信經過上面的解釋,對事件和發佈訂閱模式有了一個大概的印象。都說理論要與實踐相結合,因此咱們仍是動動手指敲敲代碼比較好。
我將以『觀察者模式』來釣魚這個例子爲基礎,經過重構的方式來完善一個更加通用的發佈訂閱模式。
先上代碼:
/// <summary> /// 魚的品類枚舉 /// </summary> public enum FishType { 鯽魚, 鯉魚, 黑魚, 青魚, 草魚, 鱸魚 }
釣魚竿的實現:
/// <summary> /// 魚竿(被觀察者) /// </summary> public class FishingRod { public delegate void FishingHandler(FishType type); //聲明委託 public event FishingHandler FishingEvent; //聲明事件 public void ThrowHook(FishingMan man) { Console.WriteLine("開始下鉤!"); //用隨機數模擬魚咬鉤,若隨機數爲偶數,則爲魚咬鉤 if (new Random().Next() % 2 == 0) { var type = (FishType) new Random().Next(0, 5); Console.WriteLine("鈴鐺:叮叮叮,魚兒咬鉤了"); if (FishingEvent != null) FishingEvent(type); } } }
垂釣者:
/// <summary> /// 垂釣者(觀察者) /// </summary> public class FishingMan { public FishingMan(string name) { Name = name; } public string Name { get; set; } public int FishCount { get; set; } /// <summary> /// 垂釣者天然要有魚竿啊 /// </summary> public FishingRod FishingRod { get; set; } public void Fishing() { this.FishingRod.ThrowHook(this); } public void Update(FishType type) { FishCount++; Console.WriteLine("{0}:釣到一條[{2}],已經釣到{1}條魚了!", Name, FishCount, type); } }
場景類也很簡單:
//一、初始化魚竿 var fishingRod = new FishingRod(); //二、聲明垂釣者 var jeff = new FishingMan("聖傑"); //3.分配魚竿 jeff.FishingRod = fishingRod; //四、註冊觀察者 fishingRod.FishingEvent += jeff.Update; //五、循環釣魚 while (jeff.FishCount < 5) { jeff.Fishing(); Console.WriteLine("-------------------"); //睡眠5s Thread.Sleep(5000); }
代碼很簡單,相信你一看就明白。但很顯然這個代碼實現僅適用於當前這個釣魚場景,假若有其餘場景也想使用這個模式,咱們還須要從新定義委託,從新定義事件處理,豈不很累。本着」Don't repeat yourself「的原則,咱們要對其進行重構。
結合咱們對事件本質的探討,事件是由事件源和事件處理組成。針對咱們上面的案例來講,public delegate void FishingHandler(FishType type);
這句代碼就已經說明了事件源和事件處理。事件源就是FishType type
,事件處理天然是註冊到FishingHandler
上面的委託實例。
問題找到了,很顯然是咱們的事件源和事件處理不夠抽象,因此不能通用,下面我們就來動手改造。
事件源應該至少包含事件發生的時間和觸發事件的對象。
咱們提取IEventData
接口來封裝事件源:
/// <summary> /// 定義事件源接口,全部的事件源都要實現該接口 /// </summary> public interface IEventData { /// <summary> /// 事件發生的時間 /// </summary> DateTime EventTime { get; set; } /// <summary> /// 觸發事件的對象 /// </summary> object EventSource { get; set; } }
天然咱們應該給一個默認的實現EventData
:
/// <summary> /// 事件源:描述事件信息,用於參數傳遞 /// </summary> public class EventData : IEventData { /// <summary> /// 事件發生的時間 /// </summary> public DateTime EventTime { get; set; } /// <summary> /// 觸發事件的對象 /// </summary> public Object EventSource { get; set; } public EventData() { EventTime = DateTime.Now; } }
針對Demo,擴展事件源以下:
public class FishingEventData : EventData { public FishType FishType { get; set; } public FishingMan FisingMan { get; set; } }
完成後,咱們就能夠去把在FishingRod
聲明的委託參數類型改成FishingEventData
類型了,即public delegate void FishingHandler(FishingEventData eventData); //聲明委託
;
而後修改FishingMan
的Update
方法按委託定義的參數類型修改便可,代碼我就不放了,你們自行腦補。
到這一步咱們就統一了事件源的定義方式。
事件源統一了,那事件處理也得加以限制。好比若是隨意命名事件處理方法名,那在進行事件註冊的時候還要去按照委託定義的參數類型去匹配,豈不麻煩。
咱們提取一個IEventHandler
接口:
/// <summary> /// 定義事件處理器公共接口,全部的事件處理都要實現該接口 /// </summary> public interface IEventHandler { }
事件處理要與事件源進行綁定,因此咱們再來定義一個泛型接口:
/// <summary> /// 泛型事件處理器接口 /// </summary> /// <typeparam name="TEventData"></typeparam> public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData { /// <summary> /// 事件處理器實現該方法來處理事件 /// </summary> /// <param name="eventData"></param> void HandleEvent(TEventData eventData); }
你可能會納悶,爲何先定義了一個空接口?這裏就留給本身思考吧。
至此咱們就完成了事件處理的抽象。咱們再繼續去改造咱們的Demo。咱們讓FishingMan
實現IEventHandler
接口,而後修改場景類中將fishingRod.FishingEvent += jeff.Update;
改成fishingRod.FishingEvent += jeff.HandleEvent;
便可。代碼改動很簡單,一樣在此略去。
至此你可能以爲咱們完成了對Demo的改造。但事實上呢,咱們還要弄清一個問題——若是這個FishingMan
訂閱的有其餘的事件,咱們該如何處理?
聰穎如你,你立馬想到了能夠經過事件源來進行區分處理。
public class FishingMan : IEventHandler<IEventData> { //省略其餘代碼 public void HandleEvent(IEventData eventData) { if (eventData is FishingEventData) { //do something } if(eventData is XxxEventData) { //do something else } } }
至此,這個模式實現到這個地步基本已經能夠通用了。
通用的發佈訂閱模式不是咱們的目的,咱們的目的是一個集中式的事件處理機制,且各個模塊之間相互不產生依賴。那咱們如何作到呢?一樣咱們仍是一步一步的進行分析改造。
思考一下,每次爲了實現這個模式,都要完成如下三步:
雖然只有三步,但這三步已經很繁瑣了。並且事件發佈方和事件訂閱方還存在着依賴(體如今訂閱者要顯示的進行事件的註冊和註銷上)。並且當事件過多時,直接在訂閱者中實現IEventHandler
接口處理多個事件邏輯顯然不太合適,違法單一職責原則。這裏就暴露了三個問題:
帶着問題思考,咱們就會更接近真相。
想要精簡步驟,那咱們須要尋找共性。共性就是事件的本質,也就是咱們針對事件源和事件處理提取出來的兩個接口。
想要解除依賴,那就要在發佈方和訂閱方之間添加一箇中介。
想要避免訂閱者同時處理過多事件邏輯,那咱們就把事件邏輯的處理提取到訂閱者外部。
思路有了,下面咱們就來實施吧。
本着先易後難的思想,咱們下面就來解決以上問題。
咱們先解決上面的第三個問題:如何避免在訂閱者中同時處理多個事件邏輯?
天然是針對不一樣的事件源IEventData
實現不一樣的IEventHandler
。改造後的釣魚事件處理邏輯以下:
/// <summary> /// 釣魚事件處理 /// </summary> public class FishingEventHandler : IEventHandler<FishingEventData> { public void HandleEvent(FishingEventData eventData) { eventData.FishingMan.FishCount++; Console.WriteLine("{0}:釣到一條[{2}],已經釣到{1}條魚了!", eventData.FishingMan.Name, eventData.FishingMan.FishCount, eventData.FishType); } }
這時咱們就能夠移除在FishingMan
中實現的IEventHandler
接口了。
而後將事件註冊改成fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;
便可。
上一個問題的解決,有助於咱們解決第一個問題:如何精簡流程?
爲何呢,由於咱們是根據事件源定義相應的事件處理的。也就是咱們以前說的能夠根據事件源來區分事件。
而後呢?反射,咱們能夠經過反射來進行事件的統一註冊。
在FishingRod
的構造函數中使用反射,統一註冊實現了IEventHandler<FishingEventData>
類型的實例方法HandleEvent
:
public FishingRod() { Assembly assembly = Assembly.GetExecutingAssembly(); foreach (var type in assembly.GetTypes()) { if (typeof(IEventHandler).IsAssignableFrom(type))//判斷當前類型是否實現了IEventHandler接口 { Type handlerInterface = type.GetInterface("IEventHandler`1");//獲取該類實現的泛型接口 Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 獲取泛型接口指定的參數類型 //若是參數類型是FishingEventData,則說明事件源匹配 if (eventDataType.Equals(typeof(FishingEventData))) { //建立實例 var handler = Activator.CreateInstance(type) as IEventHandler<FishingEventData>; //註冊事件 FishingEvent += handler.HandleEvent; } } } }
這樣,咱們就能夠移出場景類中的顯示註冊代碼fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;
。
如何解除依賴呢?其實答案就在本文的兩張圖上,仔細對比咱們能夠很直觀的看到,Event Bus就至關於一個介於Publisher和Subscriber中間的橋樑。它隔離了Publlisher和Subscriber之間的直接依賴,接管了全部事件的發佈和訂閱邏輯,並負責事件的中轉。
Event Bus終於要粉墨登場了!!!
分析一下,若是EventBus要接管全部事件的發佈和訂閱,那它則須要有一個容器來記錄事件源和事件處理。那又如何觸發呢?有了事件源,咱們就天然能找到綁定的事件處理邏輯,經過反射觸發。代碼以下:
/// <summary> /// 事件總線 /// </summary> public class EventBus { public static EventBus Default => new EventBus(); /// <summary> /// 定義線程安全集合 /// </summary> private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping; public EventBus() { _eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>(); MapEventToHandler(); } /// <summary> ///經過反射,將事件源與事件處理綁定 /// </summary> private void MapEventToHandler() { Assembly assembly = Assembly.GetEntryAssembly(); foreach (var type in assembly.GetTypes()) { if (typeof(IEventHandler).IsAssignableFrom(type))//判斷當前類型是否實現了IEventHandler接口 { Type handlerInterface = type.GetInterface("IEventHandler`1");//獲取該類實現的泛型接口 if (handlerInterface != null) { Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 獲取泛型接口指定的參數類型 if (_eventAndHandlerMapping.ContainsKey(eventDataType)) { List<Type> handlerTypes = _eventAndHandlerMapping[eventDataType]; handlerTypes.Add(type); _eventAndHandlerMapping[eventDataType] = handlerTypes; } else { var handlerTypes = new List<Type> { type }; _eventAndHandlerMapping[eventDataType] = handlerTypes; } } } } } /// <summary> /// 手動綁定事件源與事件處理 /// </summary> /// <typeparam name="TEventData"></typeparam> /// <param name="eventHandler"></param> public void Register<TEventData>(Type eventHandler) { List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (!handlerTypes.Contains(eventHandler)) { handlerTypes.Add(eventHandler); _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes; } } /// <summary> /// 手動解除事件源與事件處理的綁定 /// </summary> /// <typeparam name="TEventData"></typeparam> /// <param name="eventHandler"></param> public void UnRegister<TEventData>(Type eventHandler) { List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (handlerTypes.Contains(eventHandler)) { handlerTypes.Remove(eventHandler); _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes; } } /// <summary> /// 根據事件源觸發綁定的事件處理 /// </summary> /// <typeparam name="TEventData"></typeparam> /// <param name="eventData"></param> public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData { List<Type> handlers = _eventAndHandlerMapping[eventData.GetType()]; if (handlers != null && handlers.Count > 0) { foreach (var handler in handlers) { MethodInfo methodInfo = handler.GetMethod("HandleEvent"); if (methodInfo != null) { object obj = Activator.CreateInstance(handler); methodInfo.Invoke(obj, new object[] { eventData }); } } } } }
事件總線主要定義三個方法,註冊、取消註冊、事件觸發。還有一點就是咱們在構造函數中經過反射去進行事件源和事件處理的綁定。
代碼註釋已經很清楚了,這裏就不過多解釋了。
下面咱們就來修改Demo,修改FishingRod
的事件觸發:
/// <summary> /// 下鉤 /// </summary> public void ThrowHook(FishingMan man) { Console.WriteLine("開始下鉤!"); //用隨機數模擬魚咬鉤,若隨機數爲偶數,則爲魚咬鉤 if (new Random().Next() % 2 == 0) { var a = new Random(10).Next(); var type = (FishType)new Random().Next(0, 5); Console.WriteLine("鈴鐺:叮叮叮,魚兒咬鉤了"); if (FishingEvent != null) { var eventData = new FishingEventData() { FishType = type, FishingMan = man }; //FishingEvent(eventData);//再也不須要經過事件委託觸發 EventBus.Default.Trigger<FishingEventData>(eventData);//直接經過事件總線觸發便可 } } }
至此,事件總線的雛形已經造成!
經過上面一步一步的分析和實踐,發現事件總線也不是什麼高深的概念,只要咱們本身善於思考,勤於動手,也能實現本身的事件總線。
根據咱們的實現,大概總結出如下幾條:
最後,以上事件總線的實現只是一個雛形,還有不少潛在的問題。有興趣的不妨思考完善一下,我也會繼續更新完善,盡情期待!