事件总线知多少(2)

1.引言

之前的一篇文章事件总线知多少(1),介绍了什么是事件总线,并通过发布订阅模式一步一步的分析重构,形成了事件总线的Alpha版本,这篇文章也得到了大家的肯定和积极的反馈和建议,在此谢谢大家。本着继续学习和回馈大家的思想,我决定继续完善。本文将继续延续上一篇循序渐进的写作风格,来完成对事件总线的分析和优化。

2.回顾事件总线

在进行具体分析之前,我们还是先对我们实现的事件总线进行一个简单的回顾:

  1. 针对事件源,抽象IEventData接口;

  2. 针对事件处理,抽象IEventHandler<TEventData>接口,定义唯一事件处理方法void HandleEvent(IEventData eventData)

  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 知多少(十)。

下面我们就以Castle Windsor作为我们的IOC容器为例,来讲解下如何解除依赖。

4.1. 了解Castle Windsor

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

  1. 初始化容器:var container = new WindsorContainer();

  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());
    }
    }
  4. 注册和解析依赖

  5. 程序退出时,释放容器

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容器中获取注册的所有IEventHandlervar 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 {    //获取所有映射的EventHandlerList<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.构造ActionEventHandlervar 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>
p
rivate 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。虽然不能保证单元测试的覆盖度,但至少确保了正常业务的流转。

frameborder="0" scrolling="no" style="border-width: initial; border-style: none; width: 840px; height: 318px;">

7.总结

这一次,通过单元测试,一步一步的推进事件总线的重构和完善。主要完成了使用IOC替换反射来解耦和一些用例的完善。源码已上传至Github(源码路径:Github-EventBus)。

至此,事件总线进入Beta版本。但很显然还有许多细节有待完善,比如异常处理等,后续就不再继续这个系列,我会直接维护Github的源码,感兴趣的可自行参阅。


参考资料:
ABP EventBus
[c#] 反射真的很可怕吗?

原文地址:http://www.cnblogs.com/sheng-jie/p/7063011.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/324850.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

记录程序人生2020.8.11

1.晚休的时间总是短暂的&#xff0c;甚至你都没有来得及闭眼呢就需要将它睁开。上眼皮与下眼皮一直恋恋不舍的分开&#xff0c;天花板渐渐的呈现出来&#xff0c;该起了&#xff01; 2.5点一刻准时坐在外面的水泥台阶上&#xff0c;飞速打开屏幕中的背单词软件&#xff0c;%……

DDD理论学习系列(4)-- 领域模型

1.引言 我们还是先来拆词理解&#xff0c;领域模型可以拆为“领域”和“模型”二词。 领域&#xff1a;按照我们之前的文章的理解&#xff0c;DDD中的领域是指软件系统要解决的问题&#xff0c;如我们的办公设备公众号在线商城就是为了解决电商问题&#xff0c;对应的就是电商…

Ajax判断用户名是否可用

Ajax的简介 01展示效果并认识Ajax 方案1&#xff1a;传统方案 提交表单&#xff0c;服务器端处理&#xff0c;错误后跳转到注册页面&#xff0c;同时显示错误信息。返回客户端的是整个注册页面。 缺点&#xff1a;较大的网络流量&#xff0c;用户体验不好 方案2&#xff1a;使…

学习分布式不得不会的ACP理论

转载自 学习分布式不得不会的ACP理论 2000年7月&#xff0c;加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后&#xff0c;麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP。之后&#xff0c;CAP理论正式成为分布式计算领域的公认定理。 无…

浅谈我的读书史

点击上方蓝字关注我们本文是【雄雄的小课堂】原创的第 137 篇文章昨日晚间&#xff0c;一个朋友突然问我&#xff1a;“读书真的会有所收获吗&#xff1f;”“读书&#xff0c;真的会改变一个人吗&#xff1f;”刚看到这个问题时&#xff0c;我有点愕然&#xff0c;想着为啥会突…

.NetCore+Jexus代理+Redis模拟秒杀商品活动

开篇叙 &#xff0c;顺手点个推荐也不错&#xff1b; a. 秒杀流程 b. 封装StackExchange.Redis的使用类 c. Ubuntu16.04上使用Jexus搭建代理完成分布式部署 d. NetCore写实时监控队列服务 秒杀架构设计图︿(&#xffe3;︶&#xffe3;)︿三幅 1. 一般业务性架构 2. 后端…

如何快速搭建一个免费的,无限流量的Blog

转载自 如何快速搭建一个免费的&#xff0c;无限流量的Blog 喜欢写Blog的人&#xff0c;会经历三个阶段。 第一阶段&#xff0c;刚接触Blog&#xff0c;觉得很新鲜&#xff0c;试着选择一个免费空间来写。 第二阶段&#xff0c;发现免费空间限制太多&#xff0c;就自己购买域…

切记!构造函数里面别一定不要初始化其他类,踩过坑的都知道

点击上方蓝色关注我们&#xff01;先来看看什么是构造函数&#xff08;方法&#xff09;&#xff1a;是一种特殊的方法&#xff0c;特殊之处就在于它没有返回类型&#xff0c;void也不可以有。且方法名与类名完全相同。主要是用来创建对象时初始化对象&#xff0c;也就是为对象…

线程安全问题解决

方式一(同步代码块) synchronized(同步监视器){ //需要被同步的代码 } 说明&#xff1a;1.操作共享数据的代码&#xff0c;即为需要被同步的代码。 -->不能包含代码多了&#xff0c;也不能包含代码少了。 2.共享数据&#xff1a;多个线程共同操作的变量。比如&#xff1a;…

Chrome DevTools 调研笔记

1 说明 此篇文章针对Chrome DevTools常用功能进行调研分析。描述了每个功能点能实现的功能、应用场景和详细操作。 2 Elements 2.1 功能 检查和实时更新页面的HTML与CSS 在 Elements 面板中检查和实时编辑 DOM 树中的任何元素。在 Styles 窗格中查看和更改应用到任何选…

java中你知道的这四种代码块吗?

点击上方蓝字关注我们大家好&#xff0c;我是雄雄&#xff0c;今天给大家分享的是&#xff1a;java中的四种代码块什么叫代码块&#xff1f;代码块就是将多行代码封装到一个“{}”中&#xff0c;形成一个独立的代码区&#xff0c;这就构成了代码块&#xff0c;一般常见的代码块…

DDD理论学习系列(5)-- 统一建模语言

1.引言 上一节讲解了领域模型&#xff0c;领域模型主要是将业务中涉及到的概念以面向对象的思想进行抽象&#xff0c;抽象出实体对象&#xff0c;确定实体所对应的方法和属性&#xff0c;以及实体之间的关系。然后将这些实体和实体之间的关系以某种形式&#xff08;比如UML、图…

java中你知道这四种代码块吗?

大家好&#xff0c;我是雄雄&#xff0c;今天给大家分享的是&#xff1a;java中构造代码块的用法。 什么叫代码块&#xff1f;代码块将多行代码封装到一个{}中&#xff0c;形成一个独立的代码区&#xff0c;这就够成了代码块&#xff0c;一般常见的代码块是这样的&#xff1a; …

jzoj2152-终极数【堆】

题目&#xff08;复杂&#xff09; 给定一个长度为n的序列a&#xff0c;试求出对于序列a的每一个前缀的终极数x&#xff0c;使得 最小&#xff0c;试求出终极数t&#xff08;如若有多个终极数t&#xff0c;只需输出最小的那个&#xff09; 正解 其实就是求中位数… 输入 …

谈谈准确率(P值)、召回率(R值)及F值

转载自 谈谈准确率&#xff08;P值&#xff09;、召回率&#xff08;R值&#xff09;及F值 谈谈准确率&#xff08;P值&#xff09;、召回率&#xff08;R值&#xff09;及F值 一直总是听说过这几个词&#xff0c;但是很容易记混&#xff0c;在这里记录一下。希望对大家理解…

线程创建两种方式

方式一(继承于Thread类) 创建一个继承于Thread类的子类重写Thread类的run() --> 将此线程执行的操作声明在run()中创建Thread类的子类的对象通过此对象调用start() package com.wdl.java;//1. 创建一个继承于Thread类的子类 class MyThread extends Thread {//2. 重写Thre…

在ASP.NET CORE 2.0使用SignalR技术

一、前言 上次讲SignalR还是在《在ASP.NET Core下使用SignalR技术》文章中提到&#xff0c;ASP.NET Core 1.x.x 版本发布中并没有包含SignalR技术和开发计划中。时间过得很快&#xff0c;MS已经发布了.NET Core 2.0 Preview 2 预览版&#xff0c;距离正式版已经不远了&#xf…

java中常见的几种内部类,你会几个?(未完)

点击上方蓝色关注我们&#xff01;大家好&#xff0c;我是雄雄&#xff0c;今天给大家介绍的是java中的几种内部类。java中常见的几个内部类&#xff0c;你会几个&#xff1f;我会四个&#xff01;在看每个新知识点时&#xff0c;我们不禁有这样或者那样的疑问&#xff0c;比如…

通俗理解信息熵

转载自 通俗理解信息熵 通俗理解信息熵 前段时间德川和我讲解了决策树的相关知识&#xff0c;里面德川说了一下熵&#xff0c;今天整理了一下&#xff0c;记录下来希望对大家理解有帮助~ 1、信息熵的公式 先抛出信息熵公式如下&#xff1a; 其中代表随机事件X为的概率&…