Exploring Examples Of The Mediator Pattern In C#

The Mediator Pattern is a behavioral design pattern that promotes loose coupling between objects by encapsulating their interactions with each other. This pattern simplifies object communication and helps increase the overall modularity and scalability of a software system. The Mediator Pattern can be a critical part of communicating between different parts of a complex system — and what better way to learn it than by looking at examples of the mediator pattern in C#!

In this article, I’ll provide you with a practical guide on how to use the Mediator Pattern in C#. You’ll learn about the benefits and drawbacks of using the pattern and how to implement it in real-world scenarios. Examples of code and practical applications of the Mediator Pattern, I feel are the best way to see this in action — and therefore make it easier for you to start using!

By the end of this article, you should feel comfortable with the Mediator Pattern and leverage it to have better software engineering results in your C# projects!


Understanding the Mediator Pattern

The Mediator Pattern is a design pattern that promotes loose coupling between objects by using a mediator to handle communication among them. Simply put, the mediator acts as a middleman that coordinates messages between objects, instead of the objects communicating directly with one another as you might see with the Observer Pattern. This helps to reduce the complexity and dependencies of a system, making it easier to maintain and modify over time.

Benefits of the Mediator Pattern – At a Glance

One advantage of using the Mediator Pattern is that it simplifies the communication between objects. By centralizing the communication through a mediator, objects don’t need to know about each other, making it easier to add, remove, or modify objects as needed. Another benefit is that it promotes modularity and scalability, allowing you to break up large systems into smaller, reusable components. More on these later!

Drawbacks to the Mediator Pattern – At a Glance

Like any design pattern, there are also drawbacks to consider. One drawback of the Mediator Pattern is that it can add complexity to the implementation of a system. Since all communication goes through the mediator, it needs to be well-designed and maintained to avoid becoming a bottleneck or point of failure. Additionally, using the Mediator Pattern can have potential performance issues, as each message needs to pass through the mediator, adding overhead to the system.


Let’s See Examples of the Mediator Pattern in C#!

In C#, implementing the Mediator Pattern involves creating a mediator class that handles the communication between objects. Objects then send messages to the mediator, which relays them to the appropriate objects. This can be implemented using interfaces, events, or other methods depending on the needs of the system.

Here’s an example of how to implement the Mediator Pattern in C# using interfaces:

public interface IMediator
{
   void SendMessage(object sender, string message);
}

public interfaces IColleague
{
   void Receive(string message);
}

public class ConcreteColleague1 : IColleague
{
   private readonly IMediator _mediator;

   public ConcreteColleague1(IMediator mediator)
   {
      _mediator = mediator;
   }

   public void Send(string message)
   {
       _mediator.SendMessage(this, message);
   }

   public void Receive(string message)
   {
       Console.WriteLine("Colleague1 received message: " + message);
   }
}

public class ConcreteColleague2 : IColleague
{
   private readonly IMediator _mediator;

   public ConcreteColleague1(IMediator mediator)
   {
      _mediator = mediator;
   }

   public void Send(string message)
   {
       _mediator.SendMessage(this, message);
   }

   public void Receive(string message)
   {
       Console.WriteLine("Colleague2 received message: " + message);
   }
}

public class ConcreteMediator : IMediator
{
   private IColleague _colleague1;
   private IColleague _colleague2;

   public ConcreteMediator()
   {
       // NOTE: we'd use dependency injection or have
       // other methods to (un)register these. this is
       // just to demonstrate the overall pattern
       _colleague1 = new ConcreteColleague1(this);
       _colleague2 = new ConcreteColleague2(this);
   }

   public void SendMessage(object sender, string message)
   {
       // this pattern is nice for our callers but...
       // how can we make THIS part better? surely
       // we don't want to keep this if-else chain going...
       if (sender == colleague1)
       {
	   _colleague2.Receive(message);
       }
       else
       {
          _colleague1.Receive(message);
       }
   }
}

In this example, the ConcreteMediator class acts as the mediator, while the ConcreteColleague1 and ConcreteColleague2 classes act as the objects that communicate with each other through the mediator. By using the Mediator Pattern in this way, we can successfully reduce the coupling between objects and promote modularity in our code.

Except… Check out that mediator implementation! There’s no way that’s extensible. How can we improve it? Let’s keep going and we’ll address this later on. You can follow along with this video for examples of the mediator pattern in C#:

YouTube player

Benefits of Using the Mediator Pattern

Implementing the Mediator Pattern in your C# codebase can have numerous benefits that can simplify communication between objects, leading to looser coupling and increased modularity and scalability. By having a mediator object handle the communications between objects, it reduces the dependencies between objects that would otherwise be tightly coupled together. By reducing these dependencies, it also enables better testing and easier maintenance as there are less issues with cascading changes.

Using a mediator pattern also leads to a simpler communication flow. Instead of objects having to directly communicate with each other, they communicate with the mediator object instead. The mediator object then processes the communication and passes it along to the intended recipient object. This simplifies the communication flow and minimizes the amount of code that has to be written, making it easier to follow the code in question.

Another main advantage of the Mediator Pattern is that it promotes modularity and scalability of the objects that need to communicate. By utilizing a mediator object to handle communications between objects, it allows developers to easily add or remove objects from the system without affecting existing objects, as long as the updates are in accordance with the communication protocol. This allows the codebase to grow and evolve as needed, without drastic changes to the overall system structure.

Example Code: Demonstrating the Benefits of the Mediator Pattern in C#

// This is an example of how the Mediator Pattern can simplify communication 
// between objects in C# using a mediator object.

public interface IMediator
{
    void Notify(object sender, string eventName, object eventArgs);
}

public class ConcreteMediator : IMediator
{
    private readonly ColleagueA _colleagueA;
    private readonly ColleagueB _colleagueB;

    public ConcreteMediator(ColleagueA colleagueA, ColleagueB colleagueB)
    {
        _colleagueA = colleagueA;
        _colleagueB = colleagueB;

        _colleagueA.SetMediator(this);
        _colleagueB.SetMediator(this);
    }

    public void Notify(object sender, string eventName, object eventArgs)
    {
        if (eventName == "Event1")
        {
            // Do something when Event1 is triggered.
            _colleagueB.ReceiveEvent1(sender, eventArgs);
        }
        else if (eventName == "Event2")
        {
            // Do something when Event2 is triggered.
            _colleagueA.ReceiveEvent2(sender, eventArgs);
        }
        // more conditions here for other possible events...
    }
}

public abstract class Colleague
{
    protected IMediator Mediator;

    public void SetMediator(IMediator mediator)
    {
        Mediator = mediator;
    }

    public abstract void SendEvent(string eventName, object eventArgs);
}

public class ColleagueA : Colleague
{
    public override void SendEvent(string eventName, object eventArgs)
    {
        Mediator.Notify(this, eventName, eventArgs);
    }

    public void ReceiveEvent2(object sender, object eventArgs)
    {
        // Do something when Event2 is received.
    }
}

public class ColleagueB : Colleague
{
    public override void SendEvent(string eventName, object eventArgs)
    {
        Mediator.Notify(this, eventName, eventArgs);
    }

    public void ReceiveEvent1(object sender, object eventArgs)
    {
        // Do something when Event1 is received.
    }
}

Even in this example code, we’re greatly simplifying communication inside of these classes that want to communicate with each other. Notice how they only need to know how to interact with the mediator interface. However, we’re incurring an enormous amount of complexity in the mediator itself because as we scale this, the mediator is now responsible for routing messages as needed!

When we get to the first practical example of the mediator pattern in this article, we’ll start to see a pattern unfold. From there, we can look at making this more generic and even looking at a popular framework to help. The point of these examples so far is to show the classes that want to communicate are very decoupled from each other.


Drawbacks of Using the Mediator Pattern

When implementing software design patterns like the Mediator Pattern, it’s important to take into consideration the potential drawbacks and challenges that might arise. Here are some of the drawbacks of using the Mediator Pattern in C#.

Complexity of Implementation

One of the main drawbacks of the Mediator Pattern is its complexity. With this pattern, there are often many objects involved and keeping track of the interactions between all of them can be difficult. Additionally, there might be complex logic required to determine when and how to trigger events between objects. This complexity can make the implementation of the Mediator Pattern challenging and could lead to errors or bugs.

So far in the examples we’ve seen we’ve been creating very explicit complexities in the mediator class itself. This is because it needs to know about every potential type that is communicating in the system and how to route those messages. Adding new classes to communicate is easy to scale at the expense of the mediator itself becoming unwieldy. But there are tools we can use to address this part, which we’ll see!

Potential Performance Issues

Another drawback of using the Mediator Pattern is that it can potentially lead to performance issues. As the number of objects being mediated grows, so does the complexity of the interactions between those objects. This added complexity could lead to slower performance or even bottlenecks in the system.

Limits the Types of Objects That Can Be Mediated

The final drawback of the Mediator Pattern is that it can limit the types of objects that can be mediated. This is because the Mediator Pattern generally requires objects to be designed specifically for use with the pattern, which means that objects not designed with the Mediator Pattern in mind may not be able to interact properly. This can make it difficult to integrate existing objects with the Mediator Pattern.

However, if we refer back to the complexity issue where the mediator class is hard to scale… this is part of the concern. If we can limit the types being mediated, we can reach a happy middle ground! This way, the mediator class doesn’t need custom logic and the new classes being added to communicate need to adhere to a more suitable API.

Example Code: Demonstrating the Drawbacks of the Mediator Pattern in C#

Here’s an example of code that demonstrates some of the potential complexity and performance issues that can arise when implementing the Mediator Pattern in C#:

// Define multiple objects to be mediated
class ObjectA
{
    public void DoSomething() { }
}
class ObjectB
{
    public void DoSomethingElse() { }
}
class ObjectC
{
    public void DoAnotherThing() { }
}

// Define the mediator to handle interactions between objects
class Mediator
{
    List<object> _objects = new List<object>();
    public void Register(object obj)
    {
        _objects.Add(obj);
    }
    public void DoSomethingWithObjects()
    {
        foreach (var obj in _objects)
        {
            // Complex logic to determine how to trigger events between objects
            // ...
        }
    }
}

// Instantiate the objects and mediator
var objA = new ObjectA();
var objB = new ObjectB();
var objC = new ObjectC();
var mediator = new Mediator();

// Register the objects with the mediator
mediator.Register(objA);
mediator.Register(objB);
mediator.Register(objC);

// Use the mediator to handle interactions between objects
mediator.DoSomethingWithObjects();

While the above code is relatively simple, as the number of objects being mediated grows, the complexity of the logic in the DoSomethingWithObjects() method could quickly become overwhelming. Additionally, as more and more objects are registered with the mediator, performance issues could arise due to the increased complexity of interactions between objects.


Practical Applications of the Mediator Pattern

Practical Examples of the Mediator Pattern

The Mediator Pattern is seen in many real-world scenarios in software development. One use case of the Mediator Pattern is in complex user interface interactions such as a chap app or social media platforms. Instead of tightly coupling the chat messages and notifications, using the Mediator Pattern allows for loose coupling and simplifies communication between objects.

Another example of a real-world scenario where the Mediator Pattern can be applied is in e-commerce systems. When a user makes a purchase, there are multiple objects that need to be notified, such as inventory, shipping, and billing systems. The Mediator Pattern can be used to simplify communication between these objects and promote loose coupling.

Implementing the Mediator Pattern in a software project can be done in several ways. One approach is to identify which objects need to communicate with each other and define a mediator object to handle the interactions. The mediator object can then handle the communication between objects, reducing the complexity of the system.

Example Code: Implementing the Mediator Pattern in a Chat App

A chat app is a great example of where the Mediator Pattern can be leveraged. This will be the first example we see where the mediator class is more simplified. There isn’t custom logic for multiple types to communicate:

public interface IUser
{
    void SendMessage(string message);
    void ReceiveMessage(string message);
}

public class ChatMediator
{
    private List<IUser> users = new List<IUser>();

    public void RegisterUser(IUser user)
    {
        users.Add(user);
    }

    public void SendMessage(string message, IUser sender)
    {
        foreach (var user in users)
        {
            if (user != sender)
            {
                user.ReceiveMessage(message);
            }
        }
    }
}

public class ChatUser : IUser
{
    private ChatMediator mediator;
    public string Name { get; set; }

    public ChatUser(ChatMediator mediator, string name)
    {
        this.mediator = mediator;
        Name = name;
    }

    public void SendMessage(string message)
    {
        Console.WriteLine("{0}: Sending message: {1}", Name, message);
        mediator.SendMessage(message, this);
    }

    public void ReceiveMessage(string message)
    {
        Console.WriteLine("{0}: Received message: {1}", Name, message);
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        ChatMediator mediator = new ChatMediator();

        ChatUser user1 = new ChatUser(mediator, "User 1");
        ChatUser user2 = new ChatUser(mediator, "User 2");
        ChatUser user3 = new ChatUser(mediator, "User 3");
               
        mediator.RegisterUser(user1);
        mediator.RegisterUser(user2);
        mediator.RegisterUser(user3);

        user1.SendMessage("Hello, everyone!");
    }
}

In this example, the ChatUser objects communicate through the ChatMediator object instead of directly with each other. The mediator object promotes loose coupling and simplifies communication between the objects, resulting in a more scalable and modular system.


Examples of The Mediator Pattern in C# – With MediatR!

Let’s go one step further with the chat app we just looked at. We can use a popular framework for the Mediator Pattern with a very fitting name — MediatR. Let’s see how this implementation will change, and we’ll spice it up with an interactive example:

MediaR gets configured through the built-in Microsoft dependency injection framework and we proceed to register users to a registrar. This separation of classes is important because MediatR will create a new instance of the handler every time it is handling a message — There is no state preserved in the handler. We can see that our mediator implementation remains simple AND the chat user classes only have to know how to communicate with the mediator.

If you’d like to see more, you can watch this video on using Mediatr for the Mediator Pattern in C#:

YouTube player

Wrapping Up Examples of The Mediator Pattern in C#

In conclusion, the Mediator Pattern is a powerful software design pattern that simplifies communication between objects, promotes loose coupling, and increases modularity and scalability. While it has its advantages, it also has its downsides, such as the complexity of implementation and potential performance issues.

Using the Mediator Pattern in your C# projects allows you to achieve better software engineering practices and organization of code. So, whether you are building a new C# software project or looking to improve an existing one, consider implementing the Mediator Pattern! Give MediatR a try as well and see how you can take advantage of it!

Don’t forget to subscribe to the Dev Leader Weekly newsletter and check out my YouTube channel for more tips and guidance on C# programming!

Affiliations:

These are products & services that I trust, use, and love. I get a kickback if you decide to use my links. There’s no pressure, but I only promote things that I like to use!

      • RackNerd: Cheap VPS hosting options that I love for low-resource usage!
      • Contabo: Alternative VPS hosting options with very affordable prices!
      • ConvertKit: This is the platform that I use for my newsletter!
      • SparkLoop: This service helps me add different value to my newsletter!
      • Opus Clip: This is what I use for help creating my short-form videos!
      • Newegg: For all sorts of computer components!
      • Bulk Supplements: For an enormous selection of health supplements!
      • Quora: I try to answer questions on Quora when folks request them of me!

    author avatar
    Nick Cosentino Principal Software Engineering Manager
    Principal Software Engineering Manager at Microsoft. Views are my own.

    Leave a Reply