C# Event Bus (Event Bus)

C# Event Bus (Event Bus)

1 Introduction

The concept of event bus may be unfamiliar to you, but you may be familiar with the observer (publish-subscribe) pattern. The event bus is an implementation of the publish-subscribe pattern. It is a centralized event processing mechanism that allows different components to communicate with each other without interdependence, achieving a decoupling purpose.

2. Back to basics

Before implementing the event bus, we still need to go back to the source and explore the nature of events and the implementation mechanism of the publish-subscribe model.

2.1. The nature of the event

Let's first explore the concept of an event. Those who have read books should all remember the six elements of narrative: time, place, character, and event (cause, process, and result).

Let's take the case of registration to explain.
After the user enters the user name, email, and password, click Register. After the input is correct and verified, the registration is successful and an email is sent to the user, requiring the user to verify and activate the email.

There are two main events involved here:

1. 注册事件:起因是用户点击了注册按钮,经过是输入校验,结果是是否注册成功。

2. 发送邮件事件:起因是用户使用邮箱注册成功需要验证邮箱,经过是邮件发送,结果是邮件是否发送成功。

In fact, these six elements also apply to the processing of events in our program. Anyone who has developed a WinForm program knows that when we are doing UI design, we drag a registration button (btnRegister) from the toolbox, double-click it, and VS will automatically generate the following code for us:

void btnRegister_Click(object sender, EventArgs e){
    
     // 事件的处理}

It object senderrefers to the object that sends the event, which is the button object here; EventArgs ethe event parameters can be understood as the description of the event
, and they can be collectively referred to as the event source . The code logic is the processing of events. We can collectively call it event handling .

Having said so much, I just want to see the essence through the phenomenon: events are composed of event sources and event processing .

2.2 Publish and subscribe mode

Define a one-to-many dependency relationship between objects, so that whenever an object changes state, all objects that depend on it will be notified and automatically updated. ——publish-subscribe mode

The publish-subscribe model has two main roles:

  • Publisher (Publisher): also known as the observer, responsible for notifying all subscribers when the state changes.

  • Subscriber (Subscriber): Also known as an observer, subscribes to events and processes received events.

There are two ways to implement the publish-subscribe mode:

  • Simple implementation: Publisher maintains a list of subscribers, and loops through the list to notify subscribers when the state changes.

  • Implementation method of entrustment: Publisher defines event entrustment, and Subscriber implements entrustment.

In general, there are two keywords in the publish-subscribe model, notification and update.
Observed state changes notify observers to make corresponding updates.
It solves the problem of notifying other objects to make corresponding changes when an object changes.

If you draw a graph to represent the drawing of this process, the graph should look like this:

3. Implement the publish-subscribe model

I believe that through the above explanation, I have a general impression of the event and publish-subscribe model. It is said that theory should be combined with practice, so it is better for us to tap the code with our fingers.
Based on the example of "observer mode" fishing, I will improve a more general publish-subscribe mode through refactoring.
First upload the code:

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

Implementation of the fishing rod:

/// <summary>
/// 鱼竿(被观察者)
/// </summary>
public class FishingRod
{
    
    
    public delegate void FishingHandler(FishType type); //声明委托
    public event FishingHandler FishingEvent; //声明事件

    public void ThrowHook(FishingMan man)
    {
    
    
        Console.WriteLine("开始下钩!");         //用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
        if (new Random().Next() % 2 == 0)
        {
    
    
            var type = (FishType)new Random().Next(0, 5);
            Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了"); if (FishingEvent != null)
                FishingEvent(type);
        }
    }
}

angler:

/// <summary>   
/// 垂钓者(观察者)
/// </summary>
public class FishingMan
{
    
    
    public FishingMan(string name)
    {
    
    
        Name = name;
    }
    public string Name {
    
     get; set; }
    public int FishCount {
    
     get; set; }    /// <summary>
                                            /// 垂钓者自然要有鱼竿啊
                                            /// </summary>
    public FishingRod FishingRod {
    
     get; set; }
    public void Fishing()
    {
    
    
        this.FishingRod.ThrowHook(this);
    }
    public void Update(FishType type)
    {
    
    
        FishCount++;
        Console.WriteLine("{0}:钓到一条[{2}],已经钓到{1}条鱼了!", Name, FishCount, type);
    }
}

The scene class is also very simple:

//1、初始化鱼竿
var fishingRod = new FishingRod();
//2、声明垂钓者
var jeff = new FishingMan("圣杰");
//3.分配鱼竿
jeff.FishingRod = fishingRod;
//4、注册观察者
fishingRod.FishingEvent += jeff.Update;
//5、循环钓鱼
while (jeff.FishCount< 5)
{
    
    
    jeff.Fishing();
    Console.WriteLine("-------------------");    //睡眠5s
    Thread.Sleep(5000);
}

The code is very simple, I believe you can understand it at a glance. But obviously this code implementation is only applicable to the current phishing scenario. If there are other scenarios that want to use this mode, we need to redefine the delegate and redefine the event handling, wouldn't it be very tiring. In line with the principle of "Don't repeat yourself", we want to refactor it.

Combined with our discussion on the nature of events, events are composed of event sources and event processing. For our above case, public delegate void FishingHandler(FishType type);this code has already explained the event source and event processing. The event source is that FishType typethe event processing is naturally registered to FishingHandlerthe above delegate instance.
The problem is found. It is obvious that our event source and event processing are not abstract enough, so they cannot be used universally. Let's do the transformation next.

3.1 Extract event source

The event source should contain at least when the event occurred and the object that triggered the event.
We extract IEventDatathe interface to encapsulate the event source:

/// <summary>
/// 定义事件源接口,所有的事件源都要实现该接口
/// </summary>
public interface IEventData
{
    
    
    /// <summary>
    /// 事件发生的时间
    /// </summary>
    DateTime EventTime {
    
     get; set; }

    /// <summary>
    /// 触发事件的对象
    /// </summary>
    object EventSource {
    
     get; set; }
}

Naturally we should give a default implementation 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;
    }
}

For Demo, the extended event source is as follows:

public class FishingEventData : EventData
{
    
    
    public FishType FishType {
    
     get; set; }
    public FishingMan FisingMan {
    
     get; set; }
}

After the completion, we can change FishingRodthe declared delegate parameter type to FishingEventDatatype, that is public delegate void FishingHandler(FishingEventData eventData); //声明委托;
then FishingManmodify Updatethe method according to the parameter type defined by the delegate. I will not release the code, and everyone can make up their own minds.

At this point, we have unified the definition of event source.

3.2. Extract event handler

The event source is unified, and the event processing must be limited. For example, if you name the event processing method at will, it is troublesome to match the parameter type defined by the delegate when registering the event.

We extract an IEventHandlerinterface:

 /// <summary>
 /// 定义事件处理器公共接口,所有的事件处理都要实现该接口
 /// </summary>
 public interface IEventHandler
 {
    
    
 }

Event processing needs to be bound to the event source, so let's define a generic interface:

 /// <summary>
 /// 泛型事件处理器接口
 /// </summary>
 /// <typeparam name="TEventData"></typeparam>
 public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
 {
    
         /// <summary>
     /// 事件处理器实现该方法来处理事件
     /// </summary>
     /// <param name="eventData"></param>
     void HandleEvent(TEventData eventData);
 }

You may be wondering, why define an empty interface in the first place? Leave it to yourself to think about it here.

So far we have completed the abstraction of event processing. Let's continue to modify our Demo. Let's FishingManimplement IEventHandlerthe interface, and then modify the scene class to fishingRod.FishingEvent += jeff.Update;change fishingRod.FishingEvent += jeff.HandleEvent;it. The code changes are very simple and are also omitted here.

At this point, you may think that we have completed the transformation of the Demo. But in fact, we have to figure out a problem - if FishingManthere are other events in this subscription, how should we deal with it?
As smart as you are, you immediately thought that you can distinguish between event sources .

public class FishingMan : IEventHandler<IEventData>
{
    
        //省略其他代码
    public void HandleEvent(IEventData eventData)
    {
    
    
        if (eventData is FishingEventData)
        {
    
                //do something
        }
        if (eventData is XxxEventData)
        {
    
                //do something else
        }
    }
}

So far, this mode has been implemented to this point and it can basically be used universally.

4. Implement the event bus

The general publish-subscribe model is not our purpose. Our purpose is a centralized event processing mechanism, and each module does not depend on each other. So how do we do it? Similarly, we are still analyzing and transforming step by step.

4.1. Analysis problem

Think about it, in order to implement this pattern every time, the following three steps must be completed:

1. 事件发布方定义事件委托

2. 事件订阅方定义事件处理逻辑

3. 显示的订阅事件

Although there are only three steps, these three steps are already very cumbersome. Moreover, there is also a dependency between the event publisher and the event subscriber (reflected in the registration and deregistration of the event that the subscriber wants to display). IEventHandlerMoreover, when there are too many events, it is obviously inappropriate to directly implement the interface in the subscriber to handle multiple event logic, which violates the principle of single responsibility. Here are three problems:

1. 如何精简步骤?

2. 如何解除发布方与订阅方的依赖?

3. 如何避免在订阅者中同时处理多个事件逻辑?

Thinking with questions brings us closer to the truth.

If we want to streamline the steps, then we need to find commonalities. Commonality is the essence of events, that is, the two interfaces we extracted for event sources and event processing.

To remove dependencies, add an intermediary between the publisher and the subscriber.

To prevent subscribers from processing too much event logic at the same time, we extract the processing of event logic outside the subscriber.

Now that we have an idea, let’s implement it.

4.2. Solve the problem

Based on the idea of ​​first easy and then difficult, we will solve the above problems below.

4.2.1. Implement IEventHandler

Let's solve the third problem above first: How to avoid processing multiple event logics in the subscriber at the same time?

IEventDataNaturally, it is different for different event sources IEventHandler. The modified phishing event processing logic is as follows:

/// <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);
    }
}

At this point we can remove the interface FishingManimplemented in IEventHandler.
Then change the event registration fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;to .

4.2.2. Unified registration event

Solving the previous problem helps us solve the first problem: how to streamline the process?
Why, because we define the corresponding event processing according to the event source. That is to say, we can distinguish events according to the source of the event.
Then what? Reflection, we can perform unified registration of events through reflection.
Using reflection in FishingRodthe constructor, uniformly register IEventHandler<FishingEventData>the instance methods of the implemented types HandleEvent:

public FishingRod()
{
    
    
    Assembly assembly = Assembly.GetExecutingAssembly();

    foreach (var type in assembly.GetTypes())
    {
    
            
        if (typeof(IEventHandler).IsAssignableFrom(type))//判断当前类型是否实现了IEventHandler接口
        {
    
    
            Type handlerInterface = type.GetInterface("IEventHandler`1");//获取该类实现的泛型接口
            Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 获取泛型接口指定的参数类型

            //如果参数类型是FishingEventData,则说明事件源匹配
            if (eventDataType.Equals(typeof(FishingEventData)))
            {
    
       
                //创建实例
                var handler = Activator.CreateInstance(type) as IEventHandler<FishingEventData>;                
                //注册事件
                FishingEvent += handler.HandleEvent;
            }
        }
    }
}

This way, we can move the display registration code out of the scene class fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;.

4.2.3. Removing dependencies

How to remove the dependency? In fact, the answer lies in the two pictures in this article. After careful comparison, we can intuitively see that Event
Bus is equivalent to a bridge between Publisher and Subscriber. It isolates the direct dependencies between Publisher and Subscriber, takes over the publication and subscription logic of all events, and is responsible for the transfer of events.

Event Bus is finally coming to the stage! ! !
To analyze, if EventBus is to take over the publication and subscription of all events, it needs a container to record event sources and event processing. Then how to trigger it? With the event source, we can naturally find the bound event processing logic and trigger it through reflection. code show as below:

/// <summary>
/// 事件总线
/// </summary>
public class EventBus
{
    
    
    public static EventBus Default => new EventBus();
    /// <summary>
    /// 定义线程安全集合
    /// </summary>
    private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping; public EventBus()
    {
    
    
        _eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>();
        MapEventToHandler();
    }    /// <summary>
            ///通过反射,将事件源与事件处理绑定
            /// </summary>
    private void MapEventToHandler()
    {
    
    
        Assembly assembly = Assembly.GetEntryAssembly(); foreach (var type in assembly.GetTypes())
        {
    
    
            if (typeof(IEventHandler).IsAssignableFrom(type))//判断当前类型是否实现了IEventHandler接口
            {
    
    
                Type handlerInterface = type.GetInterface("IEventHandler`1");//获取该类实现的泛型接口
                if (handlerInterface != null)
                {
    
    
                    Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 获取泛型接口指定的参数类型

                    if (_eventAndHandlerMapping.ContainsKey(eventDataType))
                    {
    
    
                        List<Type> handlerTypes = _eventAndHandlerMapping[eventDataType];
                        handlerTypes.Add(type);
                        _eventAndHandlerMapping[eventDataType] = handlerTypes;
                    }
                    else
                    {
    
    
                        var handlerTypes = new List<Type> {
    
     type };
                        _eventAndHandlerMapping[eventDataType] = handlerTypes;
                    }
                }
            }
        }
    }

    /// <summary>
    /// 手动绑定事件源与事件处理
    /// </summary>
    /// <typeparam name="TEventData"></typeparam>
    /// <param name="eventHandler"></param>
    public void Register<TEventData>(Type eventHandler)
    {
    
    
        List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (!handlerTypes.Contains(eventHandler))
        {
    
    
            handlerTypes.Add(eventHandler);
            _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;
        }
    }

    /// <summary>
    /// 手动解除事件源与事件处理的绑定
    /// </summary>
    /// <typeparam name="TEventData"></typeparam>
    /// <param name="eventHandler"></param>
    public void UnRegister<TEventData>(Type eventHandler)
    {
    
    
        List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (handlerTypes.Contains(eventHandler))
        {
    
    
            handlerTypes.Remove(eventHandler);
            _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;
        }
    }

    /// <summary>
    /// 根据事件源触发绑定的事件处理
    /// </summary>
    /// <typeparam name="TEventData"></typeparam>
    /// <param name="eventData"></param>
    public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
    {
    
    
        List<Type> handlers = _eventAndHandlerMapping[eventData.GetType()]; if (handlers != null && handlers.Count > 0)
        {
    
    
            foreach (var handler in handlers)
            {
    
    
                MethodInfo methodInfo = handler.GetMethod("HandleEvent"); if (methodInfo != null)
                {
    
    
                    object obj = Activator.CreateInstance(handler);
                    methodInfo.Invoke(obj, new object[] {
    
     eventData });
                }
            }
        }
    }
}

The event bus mainly defines three methods, registration, unregistration, and event triggering. Another point is that we use reflection to bind the event source and event processing in the constructor.
The code comments are already very clear, so I won't explain too much here.

Next, let's modify the Demo, and the modified FishingRodevent triggers:

/// <summary>
/// 下钩
/// </summary>
public void ThrowHook(FishingMan man)
{
    
    
    Console.WriteLine("开始下钩!");    //用随机数模拟鱼咬钩,若随机数为偶数,则为鱼咬钩
    if (new Random().Next() % 2 == 0)
    {
    
    
        var a = new Random(10).Next(); var type = (FishType)new Random().Next(0, 5);
        Console.WriteLine("铃铛:叮叮叮,鱼儿咬钩了"); if (FishingEvent != null)
        {
    
    
            var eventData = new FishingEventData() {
    
     FishType = type, FishingMan = man };
            //FishingEvent(eventData);//不再需要通过事件委托触发
            EventBus.Default.Trigger<FishingEventData>(eventData);//直接通过事件总线触发即可
        }
    }
}

So far, the prototype of the event bus has been formed!

5. Summary of the event bus

Through the above step-by-step analysis and practice, it is found that the event bus is not a profound concept. As long as we are good at thinking and diligent in doing things, we can also realize our own event bus.
According to our implementation, we can roughly summarize the following:

1. 事件总线维护一个事件源与事件处理的映射字典;

2. 通过单例模式,确保事件总线的唯一入口;

3. 利用反射完成事件源与事件处理的初始化绑定;

4. 提供统一的事件注册、取消注册和触发接口。

Finally, the implementation of the above event bus is only a prototype, and there are still many potential problems. If you are interested, you may wish to think about it and improve it. I will continue to update and improve it, so look forward to it!


Copyright statement: This article is collected or provided by netizens. If there is any infringement, please tell the moderator or leave a message, and this official account will be deleted immediately.

Source: http://www.cnblogs.com/sheng-jie/

This article is published by mdnice multi-platform
Please add a picture description

おすすめ

転載: blog.csdn.net/qq_36799389/article/details/131879202