以前的一篇文章事件總線知多少(1),介紹了什麼是事件總線,並經過發佈訂閱模式一步一步的分析重構,造成了事件總線的Alpha版本,這篇文章也獲得了你們的確定和積極的反饋和建議,在此謝謝你們。本着繼續學習和回饋你們的思想,我決定繼續完善。本文將繼續延續上一篇按部就班的寫做風格,來完成對事件總線的分析和優化。
git
在進行具體分析以前,咱們仍是先對咱們實現的事件總線進行一個簡單的回顧:github
IEventData
接口;IEventHandler<TEventData>
接口,定義惟一事件處理方法void HandleEvent(IEventData eventData)
;ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping
;IEventHandler
實例完成具體事件處理邏輯的調用。基於以上的簡單回顧,咱們能夠發現Alpha版本事件總線的成功離不開反射的支持。從動態綁定到動態觸發,都是反射在默默的處理着業務邏輯。若是咱們只是簡單學習瞭解事件總線,使用反射無可厚非。但若是在實際的項目中,使用反射卻不是一個很明智的行爲,由於其性能問題。尤爲是事件總線要集中處理整個應用程序的全部事件,更易致使程序性能瓶頸。
既然說到了反射性能,那就順便解釋下爲何反射性能差?c#
那既然反射有性能瓶頸,咱們該如何是好呢?
你可能會說,既然反射有問題,那就對反射進行性能優化,好比增長緩存機制。出發點是好的,但最終仍是在反射問題的陰影之下。對於反射咱們應該持以這樣一種態度:能不用反射,則不用反射。緩存
那既然要推翻反射這條路,那如何解決動態綁定和動態觸發的問題呢?
辦法總比問題多。額,啊,嗯。就不饒圈子了,我們上IOC。安全
先看下面一張圖,來了解下DIP、IOC、DI與SL之間的關係,詳細可參考Asp.net mvc 知多少(十)。
性能優化
下面咱們就以Castle Windsor做爲咱們的IOC容器爲例,來說解下如何解除依賴。mvc
使用Castle Windsor主要包含如下幾步:app
var container = new WindsorContainer();
container.Install(FromAssembly.This());
IWindsorInstaller
自定義安裝器:public class RepositoriesInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register(Classes.FromThisAssembly() .Where(Component.IsInSameNamespaceAs<King>()) .WithService.DefaultInterfaces() .LifestyleTransient()); } }
使用IOC容器的目的很明確,一個是在註冊事件時完成依賴的注入,一個是在觸發事件時完成依賴的解析。從而完成事件的動態綁定和觸發。異步
要在EventBus
這個類中完成事件依賴的注入和解析,就須要在本類中持有一個對IWindsorContainer
的引用。
能夠直接定義一個只讀屬性,並在構造函數中進行初始化便可。
public IWindsorContainer IocContainer { get; private set; }//定義IOC容器 private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping; public EventBus() { IocContainer = new WindsorContainer(); _eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>(); }
初始化完容器,咱們須要在手動註冊和取消註冊事件API上分別完成依賴的註冊和取消註冊。由於Castle Windsor在3.0版本取消了UnRegister方法,因此在進行事件註冊時,就再也不手動卸載IOC容器中已註冊的依賴。
/// <summary> /// 手動綁定事件源與事件處理 /// </summary> /// <param name="eventType"></param> /// <param name="handlerType"></param> public void Register(Type eventType, Type handlerType) { //註冊IEventHandler<T>到IOC容器 var handlerInterface = handlerType.GetInterface("IEventHandler`1"); if (!IocContainer.Kernel.HasComponent(handlerInterface)) { IocContainer.Register(Component.For(handlerInterface, handlerType)); } //註冊到事件總線 //省略其餘代碼 } /// <summary> /// 手動解除事件源與事件處理的綁定 /// </summary> /// <typeparam name="TEventData"></typeparam> /// <param name="handlerType"></param> public void UnRegister<TEventData>(Type handlerType) { _eventAndHandlerMapping.GetOrAdd(typeof(TEventData), (type) => new List<Type>()) .RemoveAll(t => t == handlerType); }
要實現事件的動態綁定,咱們要拿到全部IEventHandler<T>
的實現。而遍歷全部類型最好的辦法就是拿到程序集(Assembly)。拿到程序集後就能夠將全部IEventHandler<T>
的實現註冊到IOC容器,而後再基於IOC容器註冊的IEventHandler<T>
動態映射事件源和事件處理。
/// <summary> /// 提供入口支持註冊其它程序集中實現的IEventHandler /// </summary> /// <param name="assembly"></param> public void RegisterAllEventHandlerFromAssembly(Assembly assembly) { //1.將IEventHandler註冊到Ioc容器 IocContainer.Register(Classes.FromAssembly(assembly) .BasedOn(typeof(IEventHandler<>)) .WithService.AllInterfaces() .LifestyleSingleton()); //2.從IOC容器中獲取註冊的全部IEventHandler var handlers = IocContainer.Kernel.GetHandlers(typeof(IEventHandler)); foreach (var handler in handlers) { //循環遍歷全部的IEventHandler<T> var interfaces = handler.ComponentModel.Implementation.GetInterfaces(); foreach (var @interface in interfaces) { if (!typeof(IEventHandler).IsAssignableFrom(@interface)) { continue; } //獲取泛型參數類型 var genericArgs = @interface.GetGenericArguments(); if (genericArgs.Length == 1) { //註冊到事件源與事件處理的映射字典中 Register(genericArgs[0], handler.ComponentModel.Implementation); } } } }
經過這種方式,咱們就能夠再其餘須要使用事件總線的項目中,添加引用後,經過調用如下代碼,來完成程序集中IEventHandler<T>
的動態綁定。
//註冊當前程序集中實現的全部IEventHandler<T> EventBus.Default.RegisterAllEventHandlerFromAssembly(Assembly.GetExecutingAssembly());
觸發事件時主要分三步,第一步從事件源與事件處理的字典中取出映射的IEventHandler
集合,第二步使用IOC容器解析依賴,第三步調用HandleEvent
方法。代碼以下:
/// <summary> /// 根據事件源觸發綁定的事件處理 /// </summary> /// <typeparam name="TEventData"></typeparam> /// <param name="eventData"></param> public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData { //獲取全部映射的EventHandler List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (handlerTypes != null && handlerTypes.Count > 0) { foreach (var handlerType in handlerTypes) { //從Ioc容器中獲取全部的實例 var handlerInterface = handlerType.GetInterface("IEventHandler`1"); var eventHandlers = IocContainer.ResolveAll(handlerInterface); //循環遍歷,僅當解析的實例類型與映射字典中事件處理類型一致時,才觸發事件 foreach (var eventHandler in eventHandlers) { if (eventHandler.GetType() == handlerType) { var handler = eventHandler as IEventHandler<TEventData>; handler.HandleEvent(eventData); } } } } }
咱們上面使用IOC容器替換了反射,在程序的易用性和性能上都有所提高。但很顯然,用例不夠完善且存在一些潛在問題,好比:
下面咱們就來先一一完善以上幾個問題。
若是每個事件處理都要定義一個類去實現IEventHandler<T>
接口,很顯然會形成類急劇膨脹。且在一些簡單場景,定義一個類又大才小用。這時咱們應該馬上想到Action。
使用Action,第一步咱們要對其進行封裝,提供一個公共的ActionEventHandler
來統一處理全部的Action事件處理器。代碼以下:
/// <summary> /// 支持Action的事件處理器 /// </summary> /// <typeparam name="TEventData"></typeparam> internal class ActionEventHandler<TEventData> : IEventHandler<TEventData> where TEventData : IEventData { /// <summary> /// 定義Action的引用,並經過構造函數傳參初始化 /// </summary> public Action<TEventData> Action { get; private set; } public ActionEventHandler(Action<TEventData> handler) { Action = handler; } /// <summary> /// 調用具體的Action來處理事件邏輯 /// </summary> /// <param name="eventData"></param> public void HandleEvent(TEventData eventData) { Action(eventData); } }
有了ActionEventHandler
作封裝,下一步就是注入IOC容器並註冊到事件總線了。
/// <summary> /// 註冊Action事件處理器 /// </summary> /// <typeparam name="TEventData"></typeparam> /// <param name="action"></param> public void Register<TEventData>(Action<TEventData> action) where TEventData : IEventData { //1.構造ActionEventHandler var actionHandler = new ActionEventHandler<TEventData>(action); //2.將ActionEventHandler的實例注入到Ioc容器 IocContainer.Register( Component.For<IEventHandler<TEventData>>() .UsingFactoryMethod(() => actionHandler) .LifestyleSingleton()); //3.註冊到事件總線 Register<TEventData>(actionHandler); }
使用起來就很簡單:
//註冊Action事件處理器 EventBus.Default.Register<EventData>( actionEventData => { Trace.TraceInformation(actionEventData.EventTime.ToLongDateString()); }); //觸發 EventBus.Default.Trigger(new EventData());
異步觸發很簡單直接使用Task.Run
包裝一下就ok了。
/// <summary> /// 異步觸發 /// </summary> /// <typeparam name="TEventData"></typeparam> /// <param name="eventData"></param> /// <returns></returns> public Task TriggerAsync<TEventData>(TEventData eventData) where TEventData : IEventData { return Task.Run(() => Trigger<TEventData>(eventData)); }
在咱們的Trigger
方法中咱們會將某一個事件源綁定的事件處理所有觸發。但在某些場景下,咱們可能並不須要所有觸發,僅須要觸發指定的EventHandler。這個需求很實際,咱們來實現一下。
/// <summary> /// 觸發指定EventHandler /// </summary> /// <param name="eventHandlerType"></param> /// <param name="eventData"></param> public void Trigger<TEventData>(Type eventHandlerType, TEventData eventData) where TEventData : IEventData { //獲取類型實現的泛型接口 var handlerInterface = eventHandlerType.GetInterface("IEventHandler`1"); var eventHandlers = IocContainer.ResolveAll(handlerInterface); //循環遍歷,僅當解析的實例類型與映射字典中事件處理類型一致時,才觸發事件 foreach (var eventHandler in eventHandlers) { if (eventHandler.GetType() == eventHandlerType) { var handler = eventHandler as IEventHandler<TEventData>; handler?.HandleEvent(eventData); } } } /// <summary> /// 異步觸發指定EventHandler /// </summary> /// <param name="eventHandlerType"></param> /// <param name="eventData"></param> /// <returns></returns> public Task TriggerAsycn<TEventData>(Type eventHandlerType, TEventData eventData) where TEventData : IEventData { return Task.Run(() => Trigger(eventHandlerType, eventData)); }
上個測試用例:
[Fact] public async void Should_Call_Specified_Handler_Async() { TestEventBus.Register<TestEventData>(new TestEventHandler()); var count = 0; TestEventBus.Register<TestEventData>( actionEventData => { count++; } ); await TestEventBus.TriggerAsycn<TestEventData> (typeof(TestEventHandler), new TestEventData(999)); TestEventHandler.TestValue.ShouldBe(999); count.ShouldBe(0); }
在事件總線中,維護的事件源和事件處理的映射字典是整個程序中的重中之重。咱們選擇了使用ConcurrentDictionary
線程安全字典來規避線程安全問題。但實際咱們真正作到線程安全了嗎?咱們看下映射字典申明:
/// <summary> /// 定義線程安全集合 /// </summary> private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping;
聰慧如你,咱們的事件源支持綁定多個事件處理,ConcurrentDictionary
確保了對key值(事件源)修改的線程安全,但沒法確保事件處理的列表List<Type>
的線程安全。那咱們就來動手改造吧。一樣代碼很簡單:
/// <summary> /// 定義鎖對象 /// </summary> private static object lockObj= new object(); /// <summary> /// 獲取事件總線映射字典中指定事件源的事件列表 /// 如有,返回列表 /// 若無,構造空列表返回 /// </summary> /// <param name="eventType"></param> /// <returns></returns> private List<Type> GetOrCreateHandlers(Type eventType) { return _eventAndHandlerMapping.GetOrAdd(eventType, (type) => new List<Type>()); } public void Register(Type eventType, Type handlerType) { //省略其餘代碼 //註冊到事件總線 lock (lockObj) { GetOrCreateHandlers(eventType).Add(handlerType); } } public void UnRegister<TEventData>(Type handlerType) { lock (lockObj) { GetOrCreateHandlers(typeof(TEventData)).RemoveAll(t => t == handlerType); } }
爲了確保重構的正確性和業務的完整性,以上的改進都是基於單元測試進行改進的,使用的是Xunit+Shouldly。雖然不能保證單元測試的覆蓋度,但至少確保了正常業務的流轉。
這一次,經過單元測試,一步一步的推動事件總線的重構和完善。主要完成了使用IOC替換反射來解耦和一些用例的完善。源碼已上傳至Github(源碼路徑:Github-EventBus)。
至此,事件總線進入Beta版本。但很顯然還有許多細節有待完善,好比異常處理等,後續就再也不繼續這個系列,我會直接維護Github的源碼,感興趣的可自行參閱。
參考資料:
ABP EventBus
[c#] 反射真的很可怕嗎?