事件總線知多少(2)

[源碼路徑:Github-EventBus:https://github.com/yanshengjie/EventBus] [事件總線知多少(1):http://www.jianshu.com/p/22fbe7a7c120] [事件總線知多少(2):http://www.jianshu.com/p/61042d36b010]

1.引言

之前的一篇文章[事件總線知多少(1):http://www.jianshu.com/p/22fbe7a7c120],介紹了什麼是事件總線,並通過發佈訂閱模式一步一步的分析重構,形成了事件總線的Alpha版本,這篇文章也得到了大家的肯定和積極的反饋和建議,在此謝謝大家。本着繼續學習和回饋大家的思想,我決定繼續完善。本文將繼續延續上一篇循序漸進的寫作風格,來完成對事件總線的分析和優化。

2.回顧事件總線

在進行具體分析之前,我們還是先對我們實現的事件總線進行一個簡單的回顧:

  1. 針對事件源,抽象 IEventData接口;

  2. 針對事件處理,抽象 IEventHandler<TEventData>接口,定義唯一事件處理方法 voidHandleEvent(IEventDataeventData)

  3. 事件總線維護一個事件源和事件處理的類型映射字典 ConcurrentDictionary<Type,List<Type>>_eventAndHandlerMapping

  4. 通過單例模式,確保事件總線的唯一入口;

  5. 利用反射完成事件源與事件處理的動態初始化綁定;

  6. 提供入口支持事件的手動註冊/取消註冊;

  7. 提供統一的事件觸發接口,通過反射動態創建 IEventHandler實例完成具體事件處理邏輯的調用。

3.發現反射問題

基於以上的簡單回顧,我們可以發現Alpha版本事件總線的成功離不開反射的支持。從動態綁定到動態觸發,都是反射在默默的處理着業務邏輯。如果我們只是簡單學習瞭解事件總線,使用反射無可厚非。但如果在實際的項目中,使用反射卻不是一個很明智的行爲,因爲其性能問題。尤其是事件總線要集中處理整個應用程序的所有事件,更易導致程序性能瓶頸。 既然說到了反射性能,那就順便解釋下爲什麼反射性能差?

  1. 類型綁定(元數據字符串匹配)

  2. 參數校驗

  3. 安全校驗

  4. 基於運行時

  5. 反射產生大量臨時對象,增加GC負擔

那既然反射有性能瓶頸,我們該如何是好呢? 你可能會說,既然反射有問題,那就對反射進行性能優化,比如增加緩存機制。出發點是好的,但最終還是在反射問題的陰影之下。對於反射我們應該持以這樣一種態度:能不用反射,則不用反射。

那既然要推翻反射這條路,那如何解決動態綁定和動態觸發的問題呢? 辦法總比問題多。額,啊,嗯。就不饒圈子了,咱們上IOC。

4.使用IOC解除依賴

先看下面一張圖,來了解下DIP、IOC、DI與SL之間的關係,詳細可參考[Asp.net mvc 知多少(十):http://www.jianshu.com/p/96947ec3e508]。

下面我們就以[Castle Windsor:https://github.com/castleproject/Windsor]作爲我們的IOC容器爲例,來講解下如何解除依賴。

4.1. 瞭解Castle Windsor

使用Castle Windsor主要包含以下幾步:

  1. 初始化容器: varcontainer=newWindsorContainer();

  2. 使用WindsorInstallers從執行程序集添加和配置所有組件: container.Install(FromAssembly.This());

  3. 實現 IWindsorInstaller自定義安裝器:

public class RepositoriesInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly()
                            .Where(Component.IsInSameNamespaceAs<King>())
                            .WithService.DefaultInterfaces()
                            .LifestyleTransient());
    }
}
  1. 註冊和解析依賴

  2. 程序退出時,釋放容器

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容器替換了反射,在程序的易用性和性能上都有所提升。但很顯然,用例不夠完善且存在一些潛在問題,比如:

  1. 支持Action EventHandler的綁定和觸發

  2. 異步觸發

  3. 觸發指定的EventHandler

  4. 線程安全

  5. 等等等

下面我們就來先一一完善以上幾個問題。

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:https://github.com/yanshengjie/EventBus])。至此,事件總線進入Beta版本。但很顯然還有許多細節有待完善,比如異常處理等,後續就不再繼續這個系列,我會直接維護Github的源碼,感興趣的可自行參閱。


參考資料: [ABP EventBus:https://github.com/aspnetboilerplate/aspnetboilerplate/tree/dev/src/Abp/Events/Bus] [[c#] 反射真的很可怕嗎?:http://www.cnblogs.com/lwhkdash/archive/2012/09/28/2707549.html]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章