Understanding Event Bus: Concepts and Implementation

1. Introduction

The concept of an Event Bus may be unfamiliar to you, but you might be familiar with the Observer (Publish-Subscribe) pattern. The Event Bus is an implementation of the publish-subscribe pattern. It is a centralized event handling mechanism that allows different components to communicate with each other without needing to depend on one another, achieving a decoupling effect.

Let’s take a look at the processing flow of the Event Bus:

Understanding Event Bus: Concepts and Implementation

Having understood the basic concept and processing flow of the Event Bus, let’s analyze how to implement it.

2. Returning to the Essence

Before we implement the Event Bus, we need to trace back and explore the essence of events and the implementation mechanism of the publish-subscribe pattern.

2.1. The Essence of Events

Let’s first discuss the concept of events. Everyone who has studied should remember the six elements of narrative writing: time, place, character, event (cause, process, result).

Let’s use a registration case to explain. After the user inputs a username, email, and password, clicks register, and passes validation, the registration is successful, and an email is sent to the user requesting them to verify their email.

There are two main events involved:

  1. Registration Event: The cause is the user clicking the register button, the process is input validation, and the result is whether the registration was successful.

  2. Email Sending Event: The cause is the user needing to verify their email after successful registration, the process is sending the email, and the result is whether the email was sent successfully.

In fact, these six elements also apply to the event handling process in our programs. Those who have developed WinForm applications know that when designing the UI, dragging a register button (btnRegister) from the toolbox and double-clicking it, Visual Studio automatically generates the following code:

void btnRegister_Click(object sender, EventArgs e){ // Event handling logic}

Here, object sender refers to the object that raised the event, which is the button object; EventArgs e represents the event parameters, which can be understood as a description of the event, and they can collectively be referred to as event sources. The logic in the code is the handling of the event, which we can collectively refer to as event handling.

All this is to clarify that: an event consists of an event source and event handling.

2.2. Publish-Subscribe Pattern

Defines a one-to-many dependency between objects, so that when one object changes state, all its dependents are notified and updated automatically. — Publish-Subscribe Pattern

The publish-subscribe pattern mainly has two roles:

  • Publisher: Also known as the subject, is responsible for notifying all subscribers when its state changes.

  • Subscriber: Also known as the observer, subscribes to events and processes the received events.

There are two implementation ways for the publish-subscribe pattern:

  • Simple implementation: The Publisher maintains a list of subscribers and notifies them by looping through the list when its state changes.

  • Delegate implementation: The Publisher defines event delegates, and the Subscriber implements the delegates.

In summary, there are two key terms in the publish-subscribe pattern: notification and update. The subject notifies the observer to make corresponding updates when its state changes. It solves the problem of notifying other objects to make corresponding changes when an object changes.

If we were to draw a diagram to represent this process, it would look like this:

Understanding Event Bus: Concepts and Implementation

3. Implementing the Publish-Subscribe Pattern

After the explanation above, you should have a general impression of events and the publish-subscribe pattern. They say theory should be combined with practice, so let’s write some code. I will take the example of the ‘Observer Pattern’ in fishing and improve it to create a more general publish-subscribe pattern. Here’s the code:

/// <summary>/// Enumeration of fish types/// </summary>public enum FishType
{
    Carp,
    Common carp,
    Black fish,
    Green fish,
    Grass carp,
    Bass
}

Implementation of the fishing rod:

 /// <summary>
 ///     Fishing rod (subject)
 /// </summary>
 public class FishingRod
 {     public delegate void FishingHandler(FishType type); // Declare delegate
     public event FishingHandler FishingEvent; // Declare event

     public void ThrowHook(FishingMan man)     {
         Console.WriteLine("Starting to fish!");     
     // Simulate fish biting with a random number, if the random number is even, a fish bites
         if (new Random().Next() % 2 == 0)
         {             var type = (FishType) new Random().Next(0, 5);
             Console.WriteLine("Bell: Ding ding ding, a fish bit the hook");             if (FishingEvent != null)
                 FishingEvent(type);
         }
     }
 }

Fisher:

/// <summary>///     Fisher (observer)/// </summary>public class FishingMan{    public FishingMan(string name)    {
        Name = name;
    }    public string Name { get; set; }    public int FishCount { get; set; }    /// <summary>
    /// Fisher must have a fishing rod
    /// </summary>
    public FishingRod FishingRod { get; set; }    public void Fishing()    {        this.FishingRod.ThrowHook(this);
    }    public void Update(FishType type)    {
        FishCount++;
        Console.WriteLine("{0}: Caught a [{2}], caught {1} fish already!", Name, FishCount, type);
    }
}

The scene class is also very simple:

//1. Initialize the fishing rod
var fishingRod = new FishingRod();
//2. Declare the fisher
var jeff = new FishingMan("Jeff");
//3. Assign the fishing rod
jeff.FishingRod = fishingRod;
//4. Register the observer
fishingRod.FishingEvent += jeff.Update;
//5. Loop to fish
while (jeff.FishCount < 5)
{
    jeff.Fishing();
    Console.WriteLine("-------------------");    // Sleep for 5s
    Thread.Sleep(5000);
}

The code is straightforward, and I’m sure you will understand it at a glance. However, this implementation is clearly only suitable for the current fishing scenario. If we want to use this pattern in other scenarios, we would need to redefine the delegate and the event handling, which would be tedious. Based on the principle of “Don’t Repeat Yourself”, we need to refactor it.

Combining our discussion on the essence of events, events consist of an event source and event handling. In our case, public delegate void FishingHandler(FishType type); already indicates the event source and event handling. The event source is FishType type, and the event handling is the delegate instance registered on FishingHandler. The problem is clear: our event source and event handling are not abstract enough to be generic, so let’s modify it.

3.1. Extracting the Event Source

The event source should at least contain the time of the event occurrence and the object that triggered the event. We extract the IEventData interface to encapsulate the event source:

/// <summary>/// Defines the event source interface; all event sources must implement this interface/// </summary>public interface IEventData{    /// <summary>
    /// Time of the event occurrence
    /// </summary>
    DateTime EventTime { get; set; }    /// <summary>
    /// The object that triggered the event
    /// </summary>
    object EventSource { get; set; }
}

We should also provide a default implementation of EventData:

/// <summary>/// Event source: Describes event information for parameter passing/// </summary>public class EventData : IEventData{    /// <summary>
    /// Time of the event occurrence
    /// </summary>
    public DateTime EventTime { get; set; }    /// <summary>
    /// The object that triggered the event
    /// </summary>
    public Object EventSource { get; set; }    public EventData()    {
        EventTime = DateTime.Now;
    }
}

For the demo, we extend the event source as follows:

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

Once completed, we can change the delegate parameter type declared in FishingRod to FishingEventData, i.e., public delegate void FishingHandler(FishingEventData eventData); // Declare delegate; then modify the Update method of FishingMan according to the parameter type defined by the delegate, the code I will not provide here, you can imagine.

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

3.2. Extracting the Event Handler

With the event source unified, the event handling must also be restricted. For example, if we randomly name the event handling methods, we would have to match them according to the parameter types defined by the delegate during event registration, which would be cumbersome.

We extract an IEventHandler interface:

 /// <summary>
 /// Defines a public interface for event handlers; all event handling must implement this interface
 /// </summary>
 public interface IEventHandler
 {
 }

Event handling needs to be bound to the event source, so we define a generic interface:

 /// <summary>
 /// Generic event handler interface
 /// </summary>
 /// <typeparam name="TEventData"></typeparam>
 public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
 {     /// <summary>
     /// The event handler implements this method to handle events
     /// </summary>
     /// <param name="eventData"></param>
     void HandleEvent(TEventData eventData);
 }

You might wonder why we first defined an empty interface? This is left for you to think about.

At this point, we have completed the abstraction of event handling. Next, we will continue to modify our demo. We let FishingMan implement the IEventHandler interface, and then modify the scene class to change fishingRod.FishingEvent += jeff.Update; to fishingRod.FishingEvent += jeff.HandleEvent;. The code changes are simple, and I will skip this here.

At this point, you may feel that we have completed the modification of the demo. However, in fact, we need to clarify one question — if the FishingMan subscribes to other events, how should we handle them? Smart as you are, you immediately think of distinguishing handling through the event source.

public class FishingMan : IEventHandler<IEventData>{    // Omitted other code
    public void HandleEvent(IEventData eventData)
    {        if (eventData is FishingEventData)
        {            //do something
        }        if(eventData is XxxEventData)
        {            //do something else
        }
    }
}

At this point, the implementation of this pattern has become generally applicable.

4. Implementing the Event Bus

The general publish-subscribe pattern is not our goal; our goal is a centralized event handling mechanism where various modules do not produce dependencies on each other. So how do we achieve this? We will analyze and modify step by step.

4.1. Analyzing the Problem

Consider that every time to implement this pattern, we have to complete the following three steps:

  1. The event publisher defines the event delegate.

  2. The event subscriber defines the event handling logic.

  3. Explicitly subscribe to the event.

Although there are only three steps, these three steps are already quite cumbersome. Moreover, the event publisher and subscriber still have dependencies (reflected in the fact that the subscriber must explicitly register and unregister events). When there are too many events, implementing the IEventHandler interface in the subscriber to handle multiple event logics is obviously not suitable, violating the single responsibility principle. This exposes three issues:

  1. How to simplify the steps?

  2. How to eliminate the dependency between the publisher and subscriber?

  3. How to avoid handling multiple event logics simultaneously in the subscriber?

By thinking with these questions, we can get closer to the truth.

To simplify the steps, we need to look for commonalities. The commonality is the essence of events, which is the two interfaces we extracted for event sources and event handling.

To eliminate the dependency, we need to add a mediator between the publisher and subscriber.

To avoid the subscriber handling too many event logics simultaneously, we will extract the event logic handling outside the subscriber.

With the thought process in place, let’s implement it.

4.2. Solving the Problems

Following the principle of starting easy and then moving to difficult, we will solve the above problems.

4.2.1. Implementing IEventHandler

Let’s first solve the third problem: how to avoid handling multiple event logics simultaneously in the subscriber?

Naturally, we implement different IEventHandler for different event sources IEventData. The modified fishing event handling logic is as follows:

/// <summary>/// Fishing event handling/// </summary>public class FishingEventHandler : IEventHandler<FishingEventData>
{    public void HandleEvent(FishingEventData eventData)
    {
        eventData.FishingMan.FishCount++;

        Console.WriteLine("{0}: Caught a [{2}], already caught {1} fish!",

Leave a Comment