事件源应该至少包含事件发生的时间和触发事件的对象。
我们提取IEventData接口来封装事件源:
自然我们应该给一个默认的实现EventData:
/// <summary> /// 事件源:描述事件信息,用于参数传递 /// </summary> public class EventData : IEventData { /// <summary> /// 事件发生的时间 /// </summary> public DateTime EventTime { get; set; } /// <summary> /// 触发事件的对象 /// </summary> public Object EventSource { get; set; } public EventData() { EventTime = DateTime.Now; } }针对Demo,扩展事件源如下:
public class FishingEventData : EventData { public FishType FishType { get; set; } public FishingMan FisingMan { get; set; } }完成后,我们就可以去把在FishingRod声明的委托参数类型改为FishingEventData类型了,即public delegate void FishingHandler(FishingEventData eventData); //声明委托;
然后修改FishingMan的Update方法按委托定义的参数类型修改即可,代码我就不放了,大家自行脑补。
到这一步我们就统一了事件源的定义方式。
3.2.提取事件处理器事件源统一了,那事件处理也得加以限制。比如如果随意命名事件处理方法名,那在进行事件注册的时候还要去按照委托定义的参数类型去匹配,岂不麻烦。
我们提取一个IEventHandler接口:
/// <summary> /// 定义事件处理器公共接口,所有的事件处理都要实现该接口 /// </summary> public interface IEventHandler { }事件处理要与事件源进行绑定,所以我们再来定义一个泛型接口:
/// <summary> /// 泛型事件处理器接口 /// </summary> /// <typeparam name="TEventData"></typeparam> public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData { /// <summary> /// 事件处理器实现该方法来处理事件 /// </summary> /// <param name="eventData"></param> void HandleEvent(TEventData eventData); }你可能会纳闷,为什么先定义了一个空接口?这里就留给自己思考吧。
至此我们就完成了事件处理的抽象。我们再继续去改造我们的Demo。我们让FishingMan实现IEventHandler接口,然后修改场景类中将fishingRod.FishingEvent += jeff.Update;改为fishingRod.FishingEvent += jeff.HandleEvent;即可。代码改动很简单,同样在此略去。
至此你可能觉得我们完成了对Demo的改造。但事实上呢,我们还要弄清一个问题——如果这个FishingMan订阅的有其他的事件,我们该如何处理?
聪颖如你,你立马想到了可以通过事件源来进行区分处理。
至此,这个模式实现到这个地步基本已经可以通用了。
4. 实现事件总线通用的发布订阅模式不是我们的目的,我们的目的是一个集中式的事件处理机制,且各个模块之间相互不产生依赖。那我们如何做到呢?同样我们还是一步一步的进行分析改造。
4.1.分析问题思考一下,每次为了实现这个模式,都要完成以下三步:
虽然只有三步,但这三步已经很繁琐了。而且事件发布方和事件订阅方还存在着依赖(体现在订阅者要显示的进行事件的注册和注销上)。而且当事件过多时,直接在订阅者中实现IEventHandler接口处理多个事件逻辑显然不太合适,违法单一职责原则。这里就暴露了三个问题:
带着问题思考,我们就会更接近真相。
想要精简步骤,那我们需要寻找共性。共性就是事件的本质,也就是我们针对事件源和事件处理提取出来的两个接口。
想要解除依赖,那就要在发布方和订阅方之间添加一个中介。
想要避免订阅者同时处理过多事件逻辑,那我们就把事件逻辑的处理提取到订阅者外部。
思路有了,下面我们就来实施吧。
4.2.解决问题本着先易后难的思想,我们下面就来解决以上问题。
4.2.1. 实现IEventHandler我们先解决上面的第三个问题:如何避免在订阅者中同时处理多个事件逻辑?
自然是针对不同的事件源IEventData实现不同的IEventHandler。改造后的钓鱼事件处理逻辑如下:
/// <summary> /// 钓鱼事件处理 /// </summary> public class FishingEventHandler : IEventHandler<FishingEventData> { public void HandleEvent(FishingEventData eventData) { eventData.FishingMan.FishCount++; Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!", eventData.FishingMan.Name, eventData.FishingMan.FishCount, eventData.FishType); } }这时我们就可以移除在FishingMan中实现的IEventHandler接口了。
然后将事件注册改为fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;即可。
上一个问题的解决,有助于我们解决第一个问题:如何精简流程?
为什么呢,因为我们是根据事件源定义相应的事件处理的。也就是我们之前说的可以根据事件源来区分事件。
然后呢?反射,我们可以通过反射来进行事件的统一注册。
在FishingRod的构造函数中使用反射,统一注册实现了IEventHandler<FishingEventData>类型的实例方法HandleEvent:
这样,我们就可以移出场景类中的显示注册代码fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;。
4.2.3. 解除依赖