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!
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.
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!
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!
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()
{
_colleague1 = new ConcreteColleague1(this);
_colleague2 = new ConcreteColleague2(this);
}
public void SendMessage(object sender, string message)
{
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#:
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.
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")
{
_colleagueB.ReceiveEvent1(sender, eventArgs);
}
else if (eventName == "Event2")
{
_colleagueA.ReceiveEvent2(sender, eventArgs);
}
}
}
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)
{
}
}
public class ColleagueB : Colleague
{
public override void SendEvent(string eventName, object eventArgs)
{
Mediator.Notify(this, eventName, eventArgs);
}
public void ReceiveEvent1(object sender, object eventArgs)
{
}
}
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.
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!
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.
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.
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#:
class ObjectA
{
public void DoSomething() { }
}
class ObjectB
{
public void DoSomethingElse() { }
}
class ObjectC
{
public void DoAnotherThing() { }
}
class Mediator
{
List<object> _objects = new List<object>();
public void Register(object obj)
{
_objects.Add(obj);
}
public void DoSomethingWithObjects()
{
foreach (var obj in _objects)
{
}
}
}
var objA = new ObjectA();
var objB = new ObjectB();
var objC = new ObjectC();
var mediator = new Mediator();
mediator.Register(objA);
mediator.Register(objB);
mediator.Register(objC);
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.
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