Mediator Pattern in C#
Kostas Kalafatis
Posted on September 11, 2022
Originally posted here
The Mediator is a behavioural design pattern that lets us reduce chaotic dependencies between objects. The pattern restricts direct communications between the entities and forces them to collaborate only via a mediator object.
You can find the example code of this post, on GitHub
Conceptualizing the Problem
Let's say we have a user interface dialogue for creating and editing a customer profile. It consists of various form controls such as text fields, checkboxes, buttons, tabs, etc.
Some of the form elements may interact with others. For instance, selecting the "I have a roommate" checkbox may reveal a hidden text field for entering the roommate's name. Another example is the submit button that triggers the validation of values of all fields before submitting the data.
By having this logic implemented directly by the form elements, we make these elements much harder to reuse in other parts of the application. For example, we won't be able to use that checkbox on another form, since it's coupled to the roommate's text field. We can either use all the classes involved in rendering on the profile form, or none at all.
The Mediator pattern suggests that we should cease all direct communication between the components that should be independent of each other. Instead, these components must communicate indirectly, by calling a special mediator object. This mediator object will redirect the calls to the appropriate components. As a result, the components only depend on a single mediator class instead of being coupled to each other.
In our example, the dialogue class may act as the mediator. Most likely, the dialogue class is already aware of all of its sub-elements, so we won't even need to introduce new dependencies into this class.
The most significant change happens to the actual form elements. Consider the submit button. With the previous implementation, each time a user clicked the button, it had to validate the values of every form element. Now its single job is to notify the dialogue about the click. Upon receiving this notification, the dialogue itself performs the validation or passes the task to the individual elements. Thus instead of being coupled with dozens of elements, the button is only dependent on the dialogue class.
We can make the dependency even looser by extracting the common interface for all types of dialogues. The interface would declare the notification method that all form elements can use to notify the dialogue about events triggered by those elements. Thus, our submit button should now be able to work with any dialogue that implements that interface.
This way, the Mediator pattern lets us encapsulate a complex web of relations between various objects inside a single mediator object. The fewer dependencies a class has, the easier it becomes to modify, extend or reuse that class.
Structuring the Mediator Pattern
In its base implementation, the Mediator pattern has three participants:
- Component: The Components are various classes that contain some business logic. Each component has a reference to a mediator, declared through the mediator interface. The component isn't aware of the actual class of the mediator, so we can reuse the component by linking it to a different mediator. Components must not be aware of other components. If something important happens within or to a component, it must notify the mediator. When the mediator receives the notification, it can identify the sender, which is enough to decide what component should be triggered.
- Mediator: The Mediator interface declares methods of communication with components, which most of the time include just a single notification method. Components may pass any context as arguments of this method, including their objects, but only in such a way that no coupling can occur.
- Concrete Mediator: The Concrete Mediators encapsulate relations between various components. Concrete mediators often keep references to all components they manage and sometimes even manage their lifecycles.
To demonstrate how the Mediator pattern works, we are going to model the snack bars in big amusement parks.
Amusement parks usually have a somewhat centralized food court with some snack bars and several smaller establishments peppered around, for gluttonous patrons to order salty snacks and sugary drinks to their increasingly-stressed heart's content.
But selling snacks to hungry hungry patrons requires supplies, and sometimes the different snack bars might run out of them. Let's imagine a system in which the different concession stands can talk to each other, communicating what supplies they need and who might have them. We can model this system using the Mediator pattern.
First, we'll need a Mediator interface, which defines a method by which the snack bars can talk to each other:
using Mediator.Components;
namespace Mediator
{
/// <summary>
/// The Mediator interface, which defines a send message
/// method which the concrete mediators must implement.
/// </summary>
public interface IMediator
{
public void SendMessage(string message, SnackBar snackBar);
}
}
We also need an abstract class to represent the Components that will be talking to one another:
namespace Mediator.Components
{
/// <summary>
/// The SnackBar abstract class represents an
/// entity involved in the conversation which
/// should receive messages.
/// </summary>
public class SnackBar
{
protected IMediator _mediator;
public SnackBar(IMediator mediator)
{
_mediator = mediator;
}
}
}
Now let's implement the different Components. In this case, we have two snack bars: one selling hotdogs and one selling french fries.
namespace Mediator.Components
{
/// <summary>
/// A Concrete Component class
/// </summary>
public class HotDogStand : SnackBar
{
public HotDogStand(IMediator mediator) : base(mediator)
{
}
public void Send(string message)
{
Console.WriteLine($"HotDog Stand says: {message}");
_mediator.SendMessage(message, this);
}
public void Notify(string message)
{
Console.WriteLine($"HotDog Stand gets message: {message}");
}
}
}
namespace Mediator.Components
{
/// <summary>
/// A Concrete Component class
/// </summary>
public class FrenchFriesStand : SnackBar
{
public FrenchFriesStand(IMediator mediator) : base(mediator)
{
}
public void Send(string message)
{
Console.WriteLine($"French Fries Stand says: {message}");
_mediator.SendMessage(message, this);
}
public void Notify(string message)
{
Console.WriteLine($"French Fries Stand gets message: {message}");
}
}
}
Note that each Component must be aware of the Mediator that is mediating the snack bar's messages.
Finally, we can implement the ConcreteMediator class, which will keep a reference to each Component and manage communication between them.
using Mediator.Components;
namespace Mediator
{
/// <summary>
/// The Concrete Mediator class, which implements the send message
/// method and keep track of all participants in the conversation.
/// </summary>
public class SnackBarMediator : IMediator
{
private HotDogStand hotDogStand;
private FrenchFriesStand friesStand;
public HotDogStand HotDogStand { set { hotDogStand = value; } }
public FrenchFriesStand FriesStand { set { friesStand = value; } }
public void SendMessage(string message, SnackBar snackBar)
{
if (snackBar == hotDogStand)
friesStand.Notify(message);
if (snackBar == friesStand)
hotDogStand.Notify(message);
}
}
}
In our Main()
method, we can use our Mediator to simulate a conversation between two snack bars. Suppose that one of the snack bars has run out of cooking oil and needs to know if the other has extra that they're not using:
using Mediator;
using Mediator.Components;
SnackBarMediator mediator = new SnackBarMediator();
HotDogStand leftKitchen = new HotDogStand(mediator);
FrenchFriesStand rightKitchen = new FrenchFriesStand(mediator);
mediator.HotDogStand = leftKitchen;
mediator.FriesStand = rightKitchen;
leftKitchen.Send("Can you send more cooking oil?");
rightKitchen.Send("Sure thing, Homer's on his way");
rightKitchen.Send("Do you have any extra soda? We've had a rush on them over here.");
leftKitchen.Send("Just a couple, we'll send Homer back with them");
Console.ReadKey();
If we run this application, we'll see a conversation between the two snack stands:
The MediatR Library
The MediatR library describes itself as a Simple mediator implementation in .NET. MediatR is essentially a library that facilitates in-process messaging.
Installing MediatR
First, we need to install the MediatR NuGet package. So from our package manager console, we can run:
Install-Package MediatR
We'll also need a package of extensions that allows us to use the .NET Core in-built IoC container.
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
Next, we need to setup our MediatR dependencies:
private static IMediator BuildMediator(WrappingWriter writer)
{
var services = new ServiceCollection();
services.AddSingleton<TextWriter>(writer);
services.AddMediatR(typeof(Ping));
var provider = services.BuildServiceProvider();
return provider.GetRequiredService<IMediator>();
}
Creating our Handlers
MediatR has two messaging types. We can use either send and receive messaging or broadcast messaging. In this example, we are going to simulate a very simple health check system, where the client pings a server, and if the server is up and running will pong the client.
First, we are going to implement a simple Ping
class that will implement the IRequest
interface.
namespace MediatRExample
{
public class Ping : IRequest<string>
{
public string Message { get; set; }
}
}
Next, we need a handler for the message. To do that we just need to implement the IRequestHandler
interface.
namespace MediatRExample
{
public class PingHandler : IRequestHandler<Ping, string>
{
public Task<string> Handle(Ping request, CancellationToken cancellationToken)
{
return Task.FromResult("Pong");
}
}
}
Using our Mediator Service
Finally, we are going to create a runner class that will call our Mediator service. The method starts by using the writer instance to write the text "Sending Ping..." to the output stream. Then, it calls the Send method of the mediator instance and passes a new instance of the Ping class with the message "Ping" The Send method sends the Ping message to the Handler that should handle it, and then returns the answer:
using MediatR;
namespace MediatRExample
{
public static class Runner
{
public static async Task Run(IMediator mediator, WrappingWriter writer)
{
await writer.WriteLineAsync("Sending Ping...");
var pong = await mediator.Send(new Ping { Message = "Ping" });
await writer.WriteLineAsync("Received: " + pong);
await writer.WriteLineAsync();
}
}
}
Finally, we need our main method to run everything.
The BuildMediator
method makes a new ServiceCollection
object and adds the TextWriter
object and the MediatR
library to it. Then, it makes a new ServiceProvider
instance and retrieves the IMediator
instance from the provider.
At the start of the Main
method, a new instance of the WrappingWriter
class is created. This class wraps the Console.Out
stream. Then, it calls the BuildMediator
method and gives the writer instance as an argument to make a new IMediator
instance.
Finally, the Main
method calls the Runner.Run
method, passing in the IMediator
instance and the writer instance, to run the Ping
message through the mediator and write the result to the output stream.
using MediatR;
namespace MediatRExample
{
public static class Program
{
public static Task Main(string[] args)
{
var writer = new WrappingWriter(Console.Out);
var mediator = BuildMediator(writer);
return Runner.Run(mediator, writer);
}
private static IMediator BuildMediator(WrappingWriter writer)
{
var services = new ServiceCollection();
services.AddSingleton<TextWriter>(writer);
services.AddMediatR(typeof(Ping));
var provider = services.BuildServiceProvider();
return provider.GetRequiredService<IMediator>();
}
}
}
Running all of this and opening our debug console, we will get the following:
Difference Between Mediator and Observer
The difference between the Mediator and the Observer pattern is often elusive. In most cases, we can implement either of these patterns; but there are cases where we can implement both.
The primary goal of the Mediator is to eliminate dependencies among a set of components. These components instead become dependent on a single mediator object. The primary goal of the Observer, on the other hand, is to establish dynamic one-way connections between objects, where some objects act as subordinates of others.
One popular implementation of the Mediator pattern relies on the Observer pattern. The mediator plays the role of the publisher and the components act as subscribers to the mediator events. When the Mediator is implemented this way, it looks very similar to the Observer pattern.
When you are confused about whether you have a Mediator or an Observer remember that the Mediator can be implemented differently. For example, you can permanently link all components to the same mediator object. This implementation won't resemble the Observer pattern but will still be an instance of the Mediator pattern.
Now if you have a program where all components are publishers, allowing dynamic connections between each other, you have a distributed set of Observers, since there is no centralized Mediator object.
Pros and Cons of Mediator Pattern
✔ We can extract the communications between various components into a single place, making it easier to comprehend and maintain, thus satisfying the Single Responsibility Principle | ❌ Over time the mediator can evolve into a God Object |
✔ We can introduce new mediators without having to change the actual components. | ❌ It can introduce a single point of failure. There could be a performance hit as all modules communicate indirectly. |
✔ We can reduce coupling between components | |
✔ We can reuse individual components more easily. |
Relations with Other Patterns
-
Chain of Responsibility, Command, Mediator and Observer are patterns that address various ways of connecting senders and receivers of requests.
- The Chain of Responsibility pattern passes a request sequentially along a dynamic chain of receivers until one of them handles it
- The Command pattern establishes unidirectional communication channels between senders and receivers
- The Mediator pattern eliminates direct connections between senders and receivers, forcing them to communicate indirectly via the mediator object.
- The Observer pattern lets receivers dynamically subscribe to and unsubscribe from receiving requests.
Final Thoughts
In this article, we have discussed what is the Mediator pattern, when to use it and what are the pros and cons of using this design pattern. We also examined the popular MediatR library and how the Mediator pattern relates to other classic design patterns.
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.
The Mediator design pattern is helpful in many ways and is quite flexible if appropriately used. However, it's worth noting that the Mediator pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Posted on September 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.