DEV Community

Cover image for Mediator Pattern in C#
Kostas Kalafatis
Kostas Kalafatis

Posted on • Edited on

Mediator Pattern in C#

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.

Pre-Mediator Spagghetti

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.

Mediator Dependencies

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:

Mediator Class Diagram

  • 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);
    }
}


Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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}");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


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}");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

If we run this application, we'll see a conversation between the two snack stands:

Mediator Example Output

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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>();
}


Enter fullscreen mode Exit fullscreen mode

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; }
    }
}


Enter fullscreen mode Exit fullscreen mode

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");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

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>();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Running all of this and opening our debug console, we will get the following:

MediatR Example Output

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.

Top comments (2)

Collapse
 
nies14 profile image
Tanvir Hassan

Thanks for the great article. Could you please check your code for the 'Using our Mediator Service' section again?

Collapse
 
kalkwst profile image
Kostas Kalafatis

Hey! Thank you for catching that mistake. I went ahead and updated the section with the correct code this time.