Mediator Pattern

Mediator is a behavioral design pattern that reduces coupling between components of a program by making them communicate indirectly, through a special mediator object. This eliminates the need for these objects to communicate directly with each other, reducing the system’s overall complexity.

When to use the Mediator?

You should use Mediator pattern when object communication is complex, preventing object reusability. Here are a few examples:

  • Multiple objects communicate directly with each other. This type of communication is tightly coupled.

  • 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.

Implementation

public interface IMediator
{
    void Notify(object sender, string ev);
}

class ConcreteMediator : IMediator
{
    private readonly Component1 _component1;

    private readonly Component2 _component2;

    public ConcreteMediator(Component1 component1, Component2 component2)
    {
        _component1 = component1;
        _component1.SetMediator(this);
        _component2 = component2;
        _component2.SetMediator(this);
    } 

    public void Notify(object sender, string ev)
    {
        switch (ev)
        {
            case "A":
                Console.WriteLine("Mediator reacts on A and triggers folowing operations:");
                _component2.DoC();
                break;
            case "D":
                Console.WriteLine("Mediator reacts on D and triggers following operations:");
                _component1.DoB();
                _component2.DoC();
                break;
        }
    }
}

class BaseComponent
{
    protected IMediator? _mediator;

    public BaseComponent(IMediator? mediator = null)
    {
        _mediator = mediator;
    }

    public void SetMediator(IMediator? mediator)
    {
        _mediator = mediator;
    }
}

class Component1 : BaseComponent
{
    public void DoA()
    {
        Console.WriteLine("Component 1 does A.");

        _mediator?.Notify(this, "A");
    }

    public void DoB()
    {
        Console.WriteLine("Component 1 does B.");

        _mediator?.Notify(this, "B");
    }
}

class Component2 : BaseComponent
{
    public void DoC()
    {
        Console.WriteLine("Component 2 does C.");

        _mediator?.Notify(this, "C");
    }

    public void DoD()
    {
        Console.WriteLine("Component 2 does D.");

        _mediator?.Notify(this, "D");
    }
}

The Mediator interface declares the Notify method, that will be used by components to notify the mediator about various events. The Concrete Mediator implementation may react to these events and pass the execution to other components. In this way, the Concrete Mediator coordinates several components.

The Base Component provides the basic functionality of storing a mediator's instance inside component objects. Concrete Components don't depend on other components or any concrete mediator classes.

Usage:

Component1 component1 = new Component1();
Component2 component2 = new Component2();
new ConcreteMediator(component1, component2);

Console.WriteLine("Client triggers operation A.");
component1.DoA();

Console.WriteLine("Client triggers operation D.");
component2.DoD();

Mediator Pattern and MediatR

MediatR, created by Jimmy Bogard (who is also the creator of AutoMapper), is a library designed to address similar challenges that the Mediator Pattern is trying to solve. Despite its name, it is not the exact implementation of the Mediator Pattern.

In the Mediator Pattern example, it is visible that Concrete Mediator acts as an object that encapsulates how a set of objects interact. The MediatR library is trying to solve the problem of decoupling the in-process sending of messages from the handling of the messages.

There is some dispute about whether MediatR slows down the application or if it introduces bad design practices. But MediatR is very useful when used in CQRS pattern and brings benefits like:

  • Helps with code decoupling

  • Isolate the concerns of the requested work

  • Request pipelines

One thing that you need to be aware of when using MediatR is that everything is done in-process. This means that whatever process is calling mediator.Send() is also the same process that is executing the relevant Handler for that request. This can be problematic when dealing with exceptions. For example, if you have multiple handlers for the request, if one of them fails, the exception can make the entire initial request chain fail.

Takeaways

Mediator is the GoF pattern that is still relevant in modern applications, especially when handling and executing various tasks or operations in enterprise applications. With Mediator, it is easy to incorporate an open number of cross-cutting concerns, such as logging, validations, audit, and security.

MediatR makes it easy to implement some aspects of the Mediator pattern. It comes with some cool features like request pipelines. But be aware that everything is done in-process and implement error handling accordingly.

Resources: