Do you have a codebase that is hard to understand?
It’s probably also hard to modify because the internal structure is very complex. However, if most classes talk to many other classes, it may be time to solve that problem using the Mediator design pattern.
The Mediator is a behavioral design pattern that allows loose coupling between objects by encapsulating how these objects interact. This eliminates the need for these objects to communicate directly with each other, reducing the system’s overall complexity.
In this article, you will learn how to identify when you need a Mediator and how to implement it in C#.
What is the Mediator design pattern?
The Mediator pattern is one of the original Gang of Four design patterns and is used to define how multiple objects communicate with each other.
It is a behavioral design pattern that allows loose coupling between objects by encapsulating how these objects interact. The Mediator pattern eliminates the need for these objects to communicate directly, reducing the system’s overall communication complexity.
The Mediator design pattern provides a central point of communication between objects in a system. This can reduce the system’s complexity and make it easier to maintain. The Mediator pattern is often used in event-driven systems, where objects must communicate with each other when events occur.
The Mediator design pattern helps in reducing dependencies. The reason is that communication is not object-to-object but object-mediator-object.
When to use the Mediator design pattern?
You can leverage a mediator when object communication is complex, preventing object reusability. This complexity is typical in a few instances, although it can appear anywhere.
Here is an example.
The diagram above has four items (Object A, B, C, and D). They all communicate directly with each other. This type of communication is tightly coupled. All objects know each other. Writing and maintaining code becomes difficult when thousands of objects try to communicate. That is where a mediator comes to the rescue.
How does Mediator Pattern solve the dependency problem?
In the illustration above, objects communicate indirectly with each other. The message goes through the Mediator, transmuting it to other objects. This is how the Mediator design pattern enhances loose coupling. Not only that, it promotes code maintainability and readability.
Every participant has to communicate with the mediator object. The function of the mediator object is to obtain the message from every object and share the information with the destination object. The Mediator handles the communication.
Other reasons to use the Mediator design pattern
As a general rule, use the Mediator pattern when:
- Multiple objects communicate in well-defined but complicated ways.
- Reusing an object is difficult since it uses and communicates with many other objects.
- A behavior distributed between many classes should be customizable without subclassing.
Who are the participants of the Mediator pattern?
The participants of the C# Mediator design pattern are:
- Mediator – interface for communicating with Colleague objects.
- ConcreteMediator – implements Mediator interface. Knows about and coordinates the Colleague classes.
- Colleague classes – each Colleague class knows only about the Mediator object. It only communicates with the Mediator object.
The following image represents a UML diagram of the Mediator design pattern.
Let’s see the structural code.
public interface IMediator
{
void SendMessage(string message, ICollegue collegue);
}
public interface ICollegue
{
void Send(string message);
void GetMessage(string message);
}
public class ConcreteColleague1 : ICollegue
{
private IMediator _mediator;
public ConcreteColleague1(IMediator mediator)
{
_mediator = mediator;
}
public void GetMessage(string message)
{
Console.WriteLine($"Colleague1 got message: {message}");
}
public void Send(string message)
{
_mediator.SendMessage(message, this);
}
}
public class ConcreteColleague2 : ICollegue
{
private IMediator _mediator;
public ConcreteColleague2(IMediator mediator)
{
_mediator = mediator;
}
public void GetMessage(string message)
{
Console.WriteLine($"Colleague2 got message: {message}");
}
public void Send(string message)
{
_mediator.SendMessage(message, this);
}
}
public class ConcreteMediator : IMediator
{
public ICollegue Collegue1 { get; set; }
public ICollegue Collegue2 { get; set; }
public void SendMessage(string message, ICollegue collegue)
{
if (collegue == Collegue1)
{
Collegue2.GetMessage(message);
}
else
{
Collegue1.GetMessage(message);
}
}
}
And the usage.
var mediator = new ConcreteMediator();
ConcreteColleague1 colleague1 = new ConcreteColleague1(mediator);
ConcreteColleague2 colleague2 = new ConcreteColleague2(mediator);
mediator.Collegue1 = colleague1;
mediator.Collegue2 = colleague2;
colleague1.Send("Hello from Colleague 1");
colleague2.Send("Well, hello there!");
The code produces the following output.
Colleague2 got message: Hello from Colleague 1
Colleague1 got message: Well, hello there!
Mediator Pattern – Real-world example in C#
Using a scenario similar to the tertiary educational system, where students are required to submit their subject enrollments to their respective department examiner.
The system effectively handles all communications sent by students by sending the message to the department examiner, and the examiner can, in turn, acknowledge the students’ message.
Students can submit subjects; only the department’s examiner can see them through communication.
public interface IDepartmentMediator
{
void RegisterUser(User user);
void SendAcknowledgement(string mssg, User user);
void SendMultipleAcknowledgement(string mssg, User user);
}
public class DepartmentroupMediator : IDepartmentMediator
{
private List<User> _usersList = new List<User>();
public void RegisterUser(User user)
{
_usersList.Add(user);
}
public void SendAcknowledgement(string mssg, User user)
{
if (!user.IsExaminer)
{
var examiner = _usersList.Find(j => j.IsExaminer == true);
examiner.Receive(mssg, user);
}
else
{
user.Receive(mssg, user);
}
}
public void SendMultipleAcknowledgement(string message, User user)
{
foreach (var person in _usersList)
{
// message should not be received by any examiner who is sending the
// meesage and also the receiver should be in the same department as the examiner sending it
if (person.Department == user.Department && !person.IsExaminer)
{
person.Receive(message, person);
}
}
}
}
public abstract class User
{
protected IDepartmentMediator _mediator;
public bool IsExaminer;
public string Name { get; set; }
public string Department { get; set; }
public User(IDepartmentMediator mediator,
string name,
string department,
bool isExaminer)
{
_mediator = mediator;
Name = name;
IsExaminer = isExaminer;
Department = department;
}
public abstract void SendAcknowledgement(string message, User user);
public abstract void SendNoticeToStudents(string message, User user);
public abstract void Receive(string message, User user);
public abstract void RegisterSubjects(string[] subject, User user);
}
public class SchoolMember : User
{
public SchoolMember(IDepartmentMediator mediator,
string name,
string department,
bool isExaminer) : base(mediator, name, department, isExaminer)
{
}
public override void Receive(string message, User user)
{
Console.WriteLine(user?.Name + ": Received Message: " + message);
}
public override void RegisterSubjects(string[] subject, User user)
{
Console.WriteLine(this.Name + ": Submitting Subjects: " + subject.Length +
" subjects submitted to the department of " + this.Department + "\n");
var message = subject.Length + " subjects submitted by " + this.Name +
" and received by " + user.Name;
_mediator.SendAcknowledgement(message, user);
}
public override void SendAcknowledgement(string message, User user)
{
Console.WriteLine(this.Name + ": Sending Message: " + message + "\n");
_mediator.SendAcknowledgement(message, user);
}
public override void SendNoticeToStudents(string message, User user)
{
Console.WriteLine(this.Name + ": Sending Message: " + message + "\n");
_mediator.SendMultipleAcknowledgement(message, user);
}
}
The usage:
DepartmentroupMediator departmentMediator = new DepartmentroupMediator();
//Instantiate students
User Student1 = new SchoolMember(departmentMediator, "Bob", "Computer", false);
User Student2 = new SchoolMember(departmentMediator, "Mary", "Mechanics", false);
User Student3 = new SchoolMember(departmentMediator, "Frank", "Mechanics", false);
//Instantiate Examiners
User Examinar1 = new SchoolMember(departmentMediator, "Philip", "Computer", true);
User Examinar2 = new SchoolMember(departmentMediator, "Sarah", "Mechanics", true);
departmentMediator.RegisterUser(Student1);
departmentMediator.RegisterUser(Student2);
departmentMediator.RegisterUser(Student3);
departmentMediator.RegisterUser(Examinar1);
departmentMediator.RegisterUser(Examinar2);
Student1.SendAcknowledgement("Hello sir, I would like to register my class subjects today", Examinar1);
Console.WriteLine();
Examinar2.SendNoticeToStudents("June 30th is the deadline to submit all class subjects. ", Examinar2);
Console.WriteLine();
Examinar1.SendAcknowledgement("Go ahead, its about time you did.", Student1);
Student3.SendAcknowledgement("Alright Ma. I'll be submitting mine right away", Examinar2);
//list of subjects to register
string[] computerSubjects = { "Java", "C#", "Phython", "MS Sql" };
Student1.RegisterSubjects(computerSubjects, Examinar1);
string[] mechanicSubjects = { "English", "CAD", "Mathematics" };
Student3.RegisterSubjects(mechanicSubjects, Examinar2);
Here is the output.
Bob: Sending Message: Hello sir, I would like to register my class subjects today
Philip: Received Message: Hello sir, I would like to register my class subjects today
Sarah: Sending Message: June 30th is the deadline to submit all class subjects.
Mary: Received Message: June 30th is the deadline to submit all class subjects.
Frank: Received Message: June 30th is the deadline to submit all class subjects.
Philip: Sending Message: Go ahead, its about time you did.
Bob: Received Message: Go ahead, its about time you did.
Frank: Sending Message: Alright Ma. I'll be submitting mine right away
Sarah: Received Message: Alright Ma. I'll be submitting mine right away
Bob: Submitting Subjects: 4 subjects submitted to the department of Computer
Philip: Received Message: 4 subjects submitted by Bob and received by Philip
Frank: Submitting Subjects: 3 subjects submitted to the department of Mechanics
Sarah: Received Message: 3 subjects submitted by Frank and received by Sarah
The advantages of the Mediator design pattern
The advantages of the Mediator design pattern are:
- It minimizes the communication channels required between components or colleagues in a system.
- The loose decoupling structure makes it easier to incorporate new subscribers and publishers.
The Mediator design pattern is a useful design pattern that allows different classes to communicate with each other. It is useful because it decouples classes from each other and makes adding new functionality easier. The Mediator design pattern is also useful because it allows different classes to communicate without having to pass references to other objects.
The disadvantages of the Mediator design pattern
The disadvantages of the Mediator pattern are:
- It can cause a single point of failure. There could be a performance hit as all modules communicate indirectly.
- The loose coupling structure makes it challenging to track how systems react by merely looking at the broadcasts.
Nevertheless, decoupled systems come with several benefits. For example, if colleagues communicate with one another directly, another colleague could cause a domino effect on the entire application. This kind of issue is not a challenge with a decoupled system.
What are related patterns to the Mediator design pattern?
Related patterns to the Mediator design pattern are Facade and Observer patterns.
Mediator vs. Observer
There are some key differences between the Mediator and Observer design patterns. You typically use the Observer pattern when there is a one-to-many relationship between objects. The Mediator pattern is used when there is a many-to-many relationship.
With the Observer pattern, objects register themselves with an Event manager and are notified when events occur. With the Mediator pattern, objects register themselves with a Mediator, and the Mediator notifies them when events occur.
The Observer design pattern is used when a change to an object is observed, and other objects need to be notified of this change. The Observable class notifies its Observers as needed. The Mediator design pattern is used when multiple objects are to be coordinated or communicate with each other, but there are no one-to-many relationships between objects.
The Mediator acts as an object that controls the communication between multiple objects, hence the name Mediator.
Mediator vs. Facade
The Mediator pattern and Facade pattern are often confused with each other. They are design patterns that allow you to decouple objects, but they do so differently.
The Mediator pattern allows objects to communicate by using a mediator object. The Facade pattern allows objects to communicate with a complex subsystem using a facade object. The facade object hides the complexity of the subsystem.
Mediator design patterns help to promote loose coupling between objects by creating a central object that can mediate communication between other objects. Facade objects have intimate knowledge of the subsystem they are providing an interface to.
MediatR
MediatR is a lightweight and flexible library that makes it easier to create a mediator and communicate between your services and your business logic.
It is a library that provides a simple way to mediate requests and notifications between application components. It can act as a message broker that you can use to send requests and notifications between application components, and it provides features like routing, logging, and exception handling.
Although Jimmy Bogard, the creator of the MediatR library, talks about how MediatR is not implemented fully as a Mediator design pattern, you can think of a MediatR as a combination of the Mediator and Command design patterns.
Let’s go over a simple example of MediatR usage. First, you define a message that you will send through the MediatR:
public class Message : IRequest<string> { }
Then you implement the request handler for that message.
public class MessageHandler : IRequestHandler<Message, string>
{
public Task<string> Handle(Message request, CancellationToken cancellationToken)
{
return Task.FromResult("Message handled");
}
}
Finally, you can send the message using the mediator instance.
var response = await mediator.Send(new Message());
Conclusion
The Mediator design pattern greatly reduces coupling and improves object interaction in your software application.
It is a useful tool for solving many common software design problems. The Mediator pattern can help improve your software’s maintainability and extensibility. It decouples objects and provides a central point for interaction logic.
With its many benefits, the Mediator pattern is a powerful tool that every software developer should be familiar with.
If you want to learn more about design patterns, check the separate in-depth article about design patterns in C#.