学习自事件总线知多少(1)
事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。

处理流程

事件之事件总线 - 图2
事件是由事件源触发并由事件处理消费(An event is raised by an event source and consumed by an event handler)。

发布订阅模式

:::warning 定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。 ——发布订阅模式 ::: 发布订阅模式主要有两个角色:

  • 发布方(Publisher):也称为被观察者,当状态改变时负责通知所有订阅者。
  • 订阅方(Subscriber):也称为观察者,订阅事件并对接收到的事件进行处理。

发布订阅模式有两种实现方式:

  • 简单的实现方式:由Publisher维护一个订阅者列表,当状态改变时循环遍历列表通知订阅者。
  • 委托的实现方式:由Publisher定义事件委托,Subscriber实现委托。

    发布订阅模式流程
    事件之事件总线 - 图3

    钓鱼示例

    事件是由事件源和事件处理组成。
    钓鱼:鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩。 ```csharp

namespace EventBus.Demo;

///

/// 鱼的品类枚举 /// public enum FishType { 鲫鱼, 鲤鱼, 黑鱼, 青鱼, 草鱼, 鲈鱼 }

  1. <a name="dXwgY"></a>
  2. ### 提取事件源
  3. 事件源应该至少包含事件发生的时间和触发事件的对象。<br />我们提取IEventData接口来封装事件源:
  4. ```csharp
  5. namespace EventBus.Demo;
  6. /// <summary>
  7. /// 定义事件源接口,所有的事件源都要实现该接口
  8. /// </summary>
  9. public interface IEventData
  10. {
  11. /// <summary>
  12. /// 事件发生的时间
  13. /// </summary>
  14. DateTime EventTime { get; set; }
  15. /// <summary>
  16. /// 触发事件的对象
  17. /// </summary>
  18. object EventSource { get; set; }
  19. }

自然我们应该给一个默认的事件源实现EventData:

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 默认的事件源实现
  4. /// 事件源:描述事件信息,用于参数传递
  5. /// </summary>
  6. public class EventData : IEventData
  7. {
  8. /// <summary>
  9. /// 事件发生的时间
  10. /// </summary>
  11. public DateTime EventTime { get; set; }
  12. /// <summary>
  13. /// 触发事件的对象
  14. /// </summary>
  15. public Object EventSource { get; set; }
  16. public EventData()
  17. {
  18. EventTime = DateTime.Now;
  19. }
  20. }

针对钓鱼Demo,扩展事件源如下:

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 扩展事件源-钓鱼
  4. /// </summary>
  5. public class FishingEventData : EventData
  6. {
  7. public FishType FishType { get; set; }
  8. public FishingMan FishingMan { get; set; }
  9. }

鱼竿的实现,这里用随机数模拟鱼儿咬钩

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 鱼竿(被观察者)
  4. /// </summary>
  5. public class FishingRod
  6. {
  7. public delegate void FishingHandler(FishingEventData type); //声明委托
  8. public event FishingHandler FishingEvent; //声明事件
  9. public void ThrowHook(FishingMan man)
  10. {
  11. Console.WriteLine("开始下钩!");
  12. //用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
  13. if (new Random().Next() % 2 == 0)
  14. {
  15. var type = (FishType)new Random().Next(0, 5);
  16. //鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩
  17. Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了");
  18. var eventData = new FishingEventData() { FishType = type, FishingMan = man };
  19. FishingEvent?.Invoke(eventData);
  20. }
  21. }
  22. }

垂钓者实现观察者接口

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 垂钓者(观察者)
  4. /// </summary>
  5. public class FishingMan
  6. {
  7. public FishingMan(string name)
  8. {
  9. Name = name;
  10. }
  11. public string Name { get; set; }
  12. public int FishCount { get; set; }
  13. /// <summary>
  14. /// 垂钓者自然要有鱼竿啊
  15. /// </summary>
  16. public FishingRod FishingRod { get; set; }
  17. public void Fishing()
  18. {
  19. this.FishingRod.ThrowHook(this);
  20. }
  21. /// <summary>
  22. /// 鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩
  23. /// </summary>
  24. /// <param name="type"></param>
  25. public void Update(FishingEventData type)
  26. {
  27. FishCount++;
  28. Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!", Name, FishCount, type);
  29. }
  30. }

到这一步我们就统一了事件源的定义方式。

提取事件处理器

事件源统一了,那事件处理也得加以限制。比如如果随意命名事件处理方法名,那在进行事件注册的时候还要去按照委托定义的参数类型去匹配,岂不麻烦。
我们提取一个IEventHandler接口:

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 定义事件处理器公共接口,所有的事件处理都要实现该接口
  4. /// </summary>
  5. public interface IEventHandler
  6. {
  7. }

事件处理要与事件源进行绑定,所以我们再来定义一个泛型接口:

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 定义事件处理器公共接口,所有的事件处理都要实现该接口
  4. /// </summary>
  5. public interface IEventHandler
  6. {
  7. }
  8. /// <summary>
  9. /// 泛型事件处理器接口
  10. /// </summary>
  11. /// <typeparam name="TEventData"></typeparam>
  12. public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
  13. {
  14. /// <summary>
  15. /// 事件处理器实现该方法来处理事件
  16. /// </summary>
  17. /// <param name="eventData"></param>
  18. void HandleEvent(TEventData eventData);
  19. }

至此我们就完成了事件处理的抽象。我们再继续去改造我们的Demo。FishingMan实现IEventHandler接口,

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 定义事件处理器公共接口,所有的事件处理都要实现该接口
  4. /// </summary>
  5. public interface IEventHandler
  6. {
  7. }
  8. /// <summary>
  9. /// 泛型事件处理器接口
  10. /// </summary>
  11. /// <typeparam name="TEventData"></typeparam>
  12. public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
  13. {
  14. /// <summary>
  15. /// 事件处理器实现该方法来处理事件
  16. /// </summary>
  17. /// <param name="eventData"></param>
  18. void HandleEvent(TEventData eventData);
  19. }

然后修改场景类中将fishingRod.FishingEvent += jeff.Update;改为fishingRod.FishingEvent += jeff.HandleEvent;即可。

  1. //观察者模式(事件总线实现方式)--钓鱼:鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩。
  2. using EventBus.Demo;
  3. //1、初始化鱼竿
  4. var fishingRod = new FishingRod();
  5. //2、声明垂钓者
  6. var jeff = new FishingMan("圣杰");
  7. //3.分配鱼竿
  8. jeff.FishingRod = fishingRod;
  9. //4、注册观察者
  10. //fishingRod.FishingEvent += jeff.Update;
  11. fishingRod.FishingEvent += jeff.HandleEvent;
  12. //5、循环钓鱼
  13. while (jeff.FishCount < 5)
  14. {
  15. jeff.Fishing();
  16. Console.WriteLine("-------------------");
  17. //睡眠5s
  18. Thread.Sleep(5000);
  19. }

至此,这个模式实现到这个地步基本已经可以通用了。

实现IEventHandler

针对不同的事件源IEventData实现不同的IEventHandler。

  1. namespace EventBus.Demo;
  2. /// <summary>
  3. /// 钓鱼事件处理
  4. /// 针对不同的事件源IEventData实现不同的IEventHandler
  5. /// </summary>
  6. public class FishingEventHandler : IEventHandler<FishingEventData>
  7. {
  8. public void HandleEvent(FishingEventData eventData)
  9. {
  10. eventData.FishingMan.FishCount++;
  11. Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!",
  12. eventData.FishingMan.Name, eventData.FishingMan.FishCount, eventData.FishType);
  13. }
  14. }

这时我们就可以移除在FishingMan中实现的IEventHandler接口了。
然后将事件注册改为fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;即可。

  1. //观察者模式(事件总线实现方式)--钓鱼:鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩。
  2. using EventBus.Demo;
  3. //1、初始化鱼竿
  4. var fishingRod = new FishingRod();
  5. //2、声明垂钓者
  6. var jeff = new FishingMan("圣杰");
  7. //3.分配鱼竿
  8. jeff.FishingRod = fishingRod;
  9. //4、注册观察者
  10. //fishingRod.FishingEvent += jeff.Update;
  11. //fishingRod.FishingEvent += jeff.HandleEvent;
  12. fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;
  13. //5、循环钓鱼
  14. while (jeff.FishCount < 5)
  15. {
  16. jeff.Fishing();
  17. Console.WriteLine("-------------------");
  18. //睡眠5s
  19. Thread.Sleep(5000);
  20. }

统一注册事件

我们可以通过反射来进行事件的统一注册。
在FishingRod的构造函数中使用反射,统一注册实现了IEventHandler类型的实例方法HandleEvent:

  1. using System.Reflection;
  2. namespace EventBus.Demo;
  3. /// <summary>
  4. /// 鱼竿(被观察者)
  5. /// </summary>
  6. public class FishingRod
  7. {
  8. public delegate void FishingHandler(FishingEventData type); //声明委托
  9. public event FishingHandler FishingEvent; //声明事件
  10. /// <summary>
  11. /// 统一注册事件
  12. /// 在FishingRod的构造函数中使用反射,统一注册实现了IEventHandler<FishingEventData>类型的实例方法HandleEvent
  13. /// </summary>
  14. public FishingRod()
  15. {
  16. Assembly assembly = Assembly.GetExecutingAssembly();
  17. foreach (var type in assembly.GetTypes())
  18. {
  19. if (typeof(IEventHandler).IsAssignableFrom(type))//判断当前类型是否实现了IEventHandler接口
  20. {
  21. Type handlerInterface = type.GetInterface("IEventHandler`1");//获取该类实现的泛型接口
  22. Type eventDataType = handlerInterface?.GetGenericArguments()[0]; // 获取泛型接口指定的参数类型
  23. //如果参数类型是FishingEventData,则说明事件源匹配
  24. if (eventDataType!=null&&eventDataType.Equals(typeof(FishingEventData)))
  25. {
  26. //创建实例
  27. var handler = Activator.CreateInstance(type) as IEventHandler<FishingEventData>;
  28. //注册事件
  29. FishingEvent += handler.HandleEvent;
  30. }
  31. }
  32. }
  33. }
  34. public void ThrowHook(FishingMan man)
  35. {
  36. Console.WriteLine("开始下钩!");
  37. //用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
  38. if (new Random().Next() % 2 == 0)
  39. {
  40. var type = (FishType)new Random().Next(0, 5);
  41. //鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩
  42. Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了");
  43. var eventData = new FishingEventData() { FishType = type, FishingMan = man };
  44. FishingEvent?.Invoke(eventData);
  45. }
  46. }
  47. }

这样,我们就可以移出场景类中的显示注册代码fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;。

解除依赖

如何解除依赖呢?其实答案就在本文的两张图上,仔细对比我们可以很直观的看到,Event Bus就相当于一个介于Publisher和Subscriber中间的桥梁。它隔离了Publlisher和Subscriber之间的直接依赖,接管了所有事件的发布和订阅逻辑,并负责事件的中转。
Event Bus终于要粉墨登场了!!!
分析一下,如果EventBus要接管所有事件的发布和订阅,那它则需要有一个容器来记录事件源和事件处理。那又如何触发呢?有了事件源,我们就自然能找到绑定的事件处理逻辑,通过反射触发。代码如下:

  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. namespace EventBus.Demo;
  9. /// <summary>
  10. /// 事件总线
  11. /// </summary>
  12. public class EventBus
  13. {
  14. public static EventBus Default => new EventBus();
  15. /// <summary>
  16. /// 定义线程安全集合
  17. /// </summary>
  18. private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping;
  19. public EventBus()
  20. {
  21. _eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>();
  22. MapEventToHandler();
  23. }
  24. /// <summary>
  25. ///通过反射,将事件源与事件处理绑定
  26. /// </summary>
  27. private void MapEventToHandler()
  28. {
  29. Assembly assembly = Assembly.GetEntryAssembly();
  30. foreach (var type in assembly.GetTypes())
  31. {
  32. if (typeof(IEventHandler).IsAssignableFrom(type))//判断当前类型是否实现了IEventHandler接口
  33. {
  34. Type handlerInterface = type.GetInterface("IEventHandler`1");//获取该类实现的泛型接口
  35. if (handlerInterface != null)
  36. {
  37. Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 获取泛型接口指定的参数类型
  38. if (_eventAndHandlerMapping.ContainsKey(eventDataType))
  39. {
  40. List<Type> handlerTypes = _eventAndHandlerMapping[eventDataType];
  41. handlerTypes.Add(type);
  42. _eventAndHandlerMapping[eventDataType] = handlerTypes;
  43. }
  44. else
  45. {
  46. var handlerTypes = new List<Type> { type };
  47. _eventAndHandlerMapping[eventDataType] = handlerTypes;
  48. }
  49. }
  50. }
  51. }
  52. }
  53. /// <summary>
  54. /// 手动绑定事件源与事件处理
  55. /// </summary>
  56. /// <typeparam name="TEventData"></typeparam>
  57. /// <param name="eventHandler"></param>
  58. public void Register<TEventData>(Type eventHandler)
  59. {
  60. List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)];
  61. if (!handlerTypes.Contains(eventHandler))
  62. {
  63. handlerTypes.Add(eventHandler);
  64. _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;
  65. }
  66. }
  67. /// <summary>
  68. /// 手动解除事件源与事件处理的绑定
  69. /// </summary>
  70. /// <typeparam name="TEventData"></typeparam>
  71. /// <param name="eventHandler"></param>
  72. public void UnRegister<TEventData>(Type eventHandler)
  73. {
  74. List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)];
  75. if (handlerTypes.Contains(eventHandler))
  76. {
  77. handlerTypes.Remove(eventHandler);
  78. _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;
  79. }
  80. }
  81. /// <summary>
  82. /// 根据事件源触发绑定的事件处理
  83. /// </summary>
  84. /// <typeparam name="TEventData"></typeparam>
  85. /// <param name="eventData"></param>
  86. public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
  87. {
  88. List<Type> handlers = _eventAndHandlerMapping[eventData.GetType()];
  89. if (handlers != null && handlers.Count > 0)
  90. {
  91. foreach (var handler in handlers)
  92. {
  93. MethodInfo methodInfo = handler.GetMethod("HandleEvent");
  94. if (methodInfo != null)
  95. {
  96. object obj = Activator.CreateInstance(handler);
  97. methodInfo.Invoke(obj, new object[] { eventData });
  98. }
  99. }
  100. }
  101. }
  102. }

事件总线主要定义三个方法,注册、取消注册、事件触发。还有一点就是我们在构造函数中通过反射去进行事件源和事件处理的绑定。
代码注释已经很清楚了,这里就不过多解释了。
下面我们就来修改Demo,修改FishingRod的事件触发:

  1. public void ThrowHook(FishingMan man)
  2. {
  3. Console.WriteLine("开始下钩!");
  4. //用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
  5. if (new Random().Next() % 2 == 0)
  6. {
  7. var type = (FishType)new Random().Next(0, 5);
  8. //鱼儿咬钩,鱼竿通过铃铛通知垂钓者收钩
  9. Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了");
  10. var eventData = new FishingEventData() { FishType = type, FishingMan = man };
  11. //FishingEvent(eventData);//不再需要通过事件委托触发
  12. EventBus.Default.Trigger<FishingEventData>(eventData);//直接通过事件总线触发即可
  13. }
  14. }

事件总线的总结

通过上面一步一步的分析和实践,发现事件总线也不是什么高深的概念,只要我们自己善于思考,勤于动手,也能实现自己的事件总线。
根据我们的实现,大概总结出以下几条:

  1. 事件总线维护一个事件源与事件处理的映射字典;
  2. 通过单例模式,确保事件总线的唯一入口;
  3. 利用反射完成事件源与事件处理的初始化绑定;
  4. 提供统一的事件注册、取消注册和触发接口。

    继续完善

    请参考事件总线知多少(2)