Introduction

This article is part of the software design patterns series where we covered some of the useful approaches and best ways of solving the problems in modern software development. If you are interested, take a look at the previously covered topics listed below.

Software design patterns provide us, we can say, a way to organize our codebase. They represent, not an actual solution, but rather a solution template for the problems in modern software development. Great thing is that these templates are not strictly limited to the specific problem, and they can be reused whenever we see fit.

Mediator Pattern in C#

The mediator pattern is one of the behavioral patterns and theoretically, it is described in the following way:

It represents an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

Is the above sentence clear? If not, don't worry, we can make it less technical and easier to understand.

The definition says to us that a mediator is a centralized class where the interaction between two different classes occurs. It will receive input from one class and pass it to the class it refers to, or a group of classes, depending on the implementation.

A real-life example would be exchanging messages between a brother and a sister by using their mom or dad to deliver and retrieve them. As you can see from this example, the siblings don't interact with each other directly and yet they are still able to send/receive the messages.

In order to achieve this, the mediator holds an instance to each concrete class it needs to notify, and each class holds an instance to the mediator.

Example Implementation

If we think of the sibling messaging exchange system mentioned above, we can rephrase it and make the communication flow between the development team and the management, for example, and that is what we are going to implement. Ok, let's dive into it.

I like to start from the abstraction part, therefore, the first things we are going to implement are the abstract mediator and component. The mediator will contain two methods, AddComponent and Notify, and the list of components considering that it needs to be aware of all of the participants.

Our component will have two methods, Send and Recieve. The Recieve method implementation will be in the concrete components, which we will see later on in the article.

This might sound complicated, but don't worry, an example will help you understand.

The abstract mediator class:

public abstract class AbstractMediator<TData>
{
  public abstract void AddComponent(Component<TData> component);
  public abstract void Notify(Component<TData> sender, TData data);
  protected List<Component<TData>> Components { get; set; } = new List<Component<TData>>();
}

Our class has a generic type parameter, and this will not be covered in this article. If you are interested in them, take a look at the official documentation page. Do note that the generics are not a part of the mediator pattern.

The abstract component class:

public abstract class Component<TData>
{
  public abstract void Recieve(TData data);
  public void Send(TData data)
  {
    Mediator.Notify(this, data);
  }
  public AbstractMediator<TData> Mediator { get; protected set; }
}

Each component will have Send and Receive methods, and a reference to the mediator. It needs that to be able to broadcast the notification to the other participants. Send method is shared and all it does is telling the mediator to notify other participants.

Ok, now we can start implementing our concrete mediator and component classes. Let's start with the mediator implementation.

public class Mediator : AbstractMediator<string>
{
  public override void AddComponent(Component<string> component)
  {
    Components.Add(component);
  }

  public override void Notify(Component<string> sender, string data)
  {
    Components.Where(component => component != sender)
      .ToList()
      .ForEach(component => component.Recieve(data));
  }
}

Our implementation is quite simple, but it gives us a lot. It gives us the ability to notify all of the participants, except for the one sending the data, that the data has been sent by someone from the group. On the other hand, the method AddComponent is used to register the component as a participant of the group.

Another useful thing here is the generic type parameter. It enables us to decide which type of data is going to be broadcasted across the group. I've chosen a string type for the sake of simplicity, but it can be more complex type if needed.

Let's move on and implement the concrete component. We will have two components, as mentioned at the beginning of the example, a development team, and management.

The dev team component:

public class DevTeam : Component<string>
{
  public DevTeam(AbstractMediator<string> mediator)
  {
    Mediator = mediator;
  }

  public override void Recieve(string data)
  {
    // do something with the data
  }
}

The management component:

public class Management : Component<string>
{
  public Management(AbstractMediator<string> mediator)
  {
    Mediator = mediator;
  }

  public override void Recieve(string data)
  {
    // do something with the data
  }
}

Our implementation is the same for both components, and if we had more of them, those would look just like this. In the constructor, the mediator reference is set and the Recieve method will be invoked every time when someone from the group sends the data.

Now it's time to put all these things together and send some messages between the participants.

var mediator = new Mediator();
var devTeam = new DevTeam(mediator);
var management = new Management(mediator);

mediator.AddComponent(devTeam);
mediator.AddComponent(management);

devTeam.Send("We have started working on the issue number: 454786!");
management.Send("Ok, make sure that you complete it by the end of this week!");

The code above links everything together. By adding a component we are making our mediator aware of it thus giving it the ability to notify every participant when any of them sends a message.

In the real-world application, we would have dependency injection responsible for our object instantiation, but for our example, it is good enough to do it manually.

Conclusion

That's all there is to this design pattern. I find it useful in applications with a focus on message broadcasting or in cases where custom middleware is needed. Feel free to write down your use cases in the comments section below.

If you liked this article, please consider supporting me by buying me a coffee. To stay tuned, follow me on twitter or subscribe here on devinduct.

Thank you for reading and see you in the next article!