轉自部落格:https://www.cnblogs.com/sheng-jie/p/7063011.html
1.引言
之前的一篇文章事件總線知多少(1),介紹了什麼是事件總線,并通過釋出訂閱模式一步一步的分析重構,形成了事件總線的Alpha版本,這篇文章也得到了大家的肯定和積極的回報和建議,在此謝謝大家。本着繼續學習和回饋大家的思想,我決定繼續完善。本文将繼續延續上一篇循序漸進的寫作風格,來完成對事件總線的分析和優化。
2.回顧事件總線
在進行具體分析之前,我們還是先對我們實作的事件總線進行一個簡單的回顧:
- 針對事件源,抽象
接口;IEventData
- 針對事件處理,抽象
接口,定義唯一事件處理方法IEventHandler<TEventData>
;void HandleEvent(IEventData eventData)
- 事件總線維護一個事件源和事件處理的類型映射字典
ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping
- 通過單例模式,確定事件總線的唯一入口;
- 利用反射完成事件源與事件處理的動态初始化綁定;
- 提供入口支援事件的手動注冊/取消注冊;
- 提供統一的事件觸發接口,通過反射動态建立
執行個體完成具體事件處理邏輯的調用。IEventHandler
3.發現反射問題
基于以上的簡單回顧,我們可以發現Alpha版本事件總線的成功離不開反射的支援。從動态綁定到動态觸發,都是反射在默默的處理着業務邏輯。如果我們隻是簡單學習了解事件總線,使用反射無可厚非。但如果在實際的項目中,使用反射卻不是一個很明智的行為,因為其性能問題。尤其是事件總線要集中處理整個應用程式的所有事件,更易導緻程式性能瓶頸。
既然說到了反射性能,那就順便解釋下為什麼反射性能差?
- 類型綁定(中繼資料字元串比對)
- 參數校驗
- 安全校驗
- 基于運作時
- 反射産生大量臨時對象,增加GC負擔
那既然反射有性能瓶頸,我們該如何是好呢?
你可能會說,既然反射有問題,那就對反射進行性能優化,比如增加緩存機制。出發點是好的,但最終還是在反射問題的陰影之下。對于反射我們應該持以這樣一種态度:能不用反射,則不用反射。
那既然要推翻反射這條路,那如何解決動态綁定和動态觸發的問題呢?
辦法總比問題多。額,啊,嗯。就不饒圈子了,咱們上IOC。
4.使用IOC解除依賴
先看下面一張圖,來了解下DIP、IOC、DI與SL之間的關系,詳細可參考Asp.net mvc 知多少(十)。
下面我們就以Castle Windsor作為我們的IOC容器為例,來講解下如何解除依賴。
4.1. 了解Castle Windsor
使用Castle Windsor主要包含以下幾步:
- 初始化容器:
var container = new WindsorContainer();
- 使用WindsorInstallers從執行程式集添加和配置所有元件:
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());
}
}
- 注冊和解析依賴
- 程式退出時,釋放容器
4.2. 使用Castle Windsor
使用IOC容器的目的很明确,一個是在注冊事件時完成依賴的注入,一個是在觸發事件時完成依賴的解析。進而完成事件的動态綁定和觸發。
4.2.1. 初始化容器
要在
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>>();
}
4.2.2.注冊和取消注冊依賴
初始化完容器,我們需要在手動注冊和取消注冊事件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);
}
4.2.3. 動态事件綁定
要實作事件的動态綁定,我們要拿到所有
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());
4.2.4. 動态事件觸發
觸發事件時主要分三步,第一步從事件源與事件處理的字典中取出映射的
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);
}
}
}
}
}
5.用例完善
我們上面使用IOC容器替換了反射,在程式的易用性和性能上都有所提升。但很顯然,用例不夠完善且存在一些潛在問題,比如:
- 支援Action EventHandler的綁定和觸發
- 異步觸發
- 觸發指定的EventHandler
- 線程安全
- 等等等
下面我們就來先一一完善以上幾個問題。
5.1.支援Action事件處理器
如果每一個事件處理都要定義一個類去實作
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());
5.2. 支援異步觸發
異步觸發很簡單直接使用
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));
}
5.3.觸發指定EventHandler
在我們的
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);
}
5.4.線程安全問題
在事件總線中,維護的事件源和事件處理的映射字典是整個程式中的重中之重。我們選擇了使用
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);
}
}
6.單元測試
為了確定重構的正确性和業務的完整性,以上的改進都是基于單元測試進行改進的,使用的是Xunit+Shouldly。雖然不能保證單元測試的覆寫度,但至少確定了正常業務的流轉。
7.總結
這一次,通過單元測試,一步一步的推進事件總線的重構和完善。主要完成了使用IOC替換反射來解耦和一些用例的完善。源碼已上傳至Github(源碼路徑:Github-EventBus)。
至此,事件總線進入Beta版本。但很顯然還有許多細節有待完善,比如異常處理等,後續就不再繼續這個系列,我會直接維護Github的源碼,感興趣的可自行參閱。
參考資料:
ABP EventBus
[c#] 反射真的很可怕嗎?