Exploring Examples Of The Mediator Pattern In C#

Exploring Examples Of The Mediator Pattern In C#

·

10 min read

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.


Sign up for Dev Leader Weekly!


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#:


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

If you’ve enjoyed this article so far, head over to the original article to see practical examples along with an interactive solution! Thanks so much for reading, and I hope that you learned something valuable from this article!


Want More Dev Leader Content?

  • Follow along on this platform if you haven’t already!

  • Subscribe to my free weekly software engineering and dotnet-focused newsletter. I include exclusive articles and early access to videos:
    SUBSCRIBE FOR FREE

  • Looking for courses? Check out my offerings:
    VIEW COURSES

  • Watch hundreds of full-length videos on my YouTube channel:
    VISIT CHANNEL

  • Visit my website for hundreds of articles on various software engineering topics (including code snippets):
    VISIT WEBSITE

  • Check out the repository with many code examples from my articles and videos on GitHub:
    VIEW REPOSITORY

Did you find this article valuable?

Support Dev Leader by becoming a sponsor. Any amount is appreciated!