Design Patterns Software Engineering

Design Patterns – Mediator

Pinterest LinkedIn Tumblr

The code is available on GitHub

The Mediator Pattern is responsible for controlling and coordinating the interactions between different objects. The pattern promotes decoupling between different components that otherwise would end up interconnected. The Mediator acts as a hub, containing logic to facilitate information dispatching between components. This way, each component knows only the mediator and is unaware of the other components, reducing the number of interconnections.

When to use

Use the Mediator Pattern when dealing with numerous components (Participants) that cooperate with each other usually in complex ways.

The Mediator Pattern promotes decoupling between objects, preventing the need for direct references that could lead to an interconnected design, which is hard to maintain and extend. By localizing cooperation logic within the mediator, we can avoid each component having its own logic for notifying and communicating with other objects directly. Distributing cooperation behavior across different components would make the system more difficult to modify and extend.

In the following example, we demonstrate how various participants can cooperate with each other using a mediator. Each participant represents a home appliance, such as a coffee machine, lights and an alarm. The goal is to model a behavior where, when the alarm is set off, the lights will turn on, and the coffee machine will start making coffee. This behavior is achieved through a mediator without participants knowing, or directly communicating with each other.

// All Appliances.
public abstract class Appliance
{
   public Mediator Mediator { get; set; }
}

public class Alarm : Appliance
{
   public void StartRinging() {
      Console.WriteLine("Alarm starts ringing!");
      Mediator.StateChanged(this, "on");
   }
}

public class CoffeeMachine : Appliance
{
   public void MakeCoffee() 
      => Console.WriteLine("CoffeeMachine: Start making coffee.");
}

public class Lights : Appliance
{
   public void TurnOn() 
      => Console.WriteLine("Lights: Turn ON.");
}

Following, we show the IApplianceMediator containing a StateChanged method. We also, have the DailyApplianceMediator as an implementation of the IApplianceMediator interface.

public interface IApplianceMediator
{
   void StateChanged(Appliance appliance, string state);
}

public class DailyApplianceMediator : IApplianceMediator
{
   private readonly Alarm _alarm;
   private readonly CoffeeMachine _coffeeMachine;
   private readonly Lights _lights;

   public DailyApplianceMediator(Alarm alarm,
      CoffeeMachine coffeeMachine,
      Lights lights)
   {
      // set properties...
   }

   public void StateChanged(Appliance appliance, string state)
   {
      if(appliance.GetType() == typeof(Alarm) && state == "on") {
         // The Mediator coordinates the behavior in case the alarm
         // is set off.
         Console.WriteLine("DailyMediator: The alarm is set. Time to turn on the lights and make some coffee.");
         _coffeeMachine.MakeCoffee();
         _lights.TurnOn();
      }
   }
}

Putting the components together, we can see how behavior emerge in the following example:

Alarm alarm = new Alarm();
CoffeeMachine coffeeMachine = new CoffeeMachine();
Lights lights = new Lights();

DailyApplianceMediator dailyMediator = new(alarm, coffeeMachine, lights);

alarm.StartRinging();
// Output:
// Alarm starts ringing!
// DailyMediator: The alarm is set. Time to turn on the lights and make some coffee.
// CoffeeMachine: Start making coffee.
// Lights: Turn ON.

Use the Mediator Pattern when you need to add a layer of indirection between request senders and receivers.

The mediator can decouple the sender of a request from the receiver, allowing for the replacement of either without affecting other components. For instance, in a system where communication with a physical device (e.g., a sensor or an RC car) involves numerous commands, the Mediator Pattern can be used to separate the commands from the command handlers. This way, we can have a more maintainable and extensible design.

In the following example, we create requests that encapsulate information related to the request, and handlers that fulfil the request. The request is unaware of its handler, and we use the mediator to send the request. The mediator is responsible to get the proper handler and pass the request object to it. This allows for the registration of multiple pairs for requests and request handlers or swapping any handler with another one.

Below are the participants (requests/requestHandlers):

// Interface all requests must implement.
public interface IRequest { }

// Interface all handlers must implement.
public interface IRequestHandler<T>
   where T : IRequest
{
   object Execute(T request);
}

public class GetUsersRequest : IRequest
{
   public int Count { get; }

   public GetUsersRequest(int count)
   {
      Count = count;
   }
}

public class GetUsersRequestHandler : IRequestHandler<GetUsersRequest>
{
   public object Execute(GetUsersRequest request)
   {
      var users = new List<object>();
      for(int i = 0; i < request.Count; i++)
      {
         users.Add(new { UserName = "user" + i });
      }
      return users;
   }
}

public class CreateUserRequest : IRequest
{
   public string UserName { get; }

   public CreateUserRequest(string username)
   {
      UserName = username;
   }
}

public class CreateUserRequestHandler : IRequestHandler<CreateUserRequest>
{
   public object Execute(CreateUserRequest request)
   {
      Console.WriteLine("Create user: " + request.UserName);
      return true;
   }
}

Next, we implement a simple Mediator that allows registration of various request/requestHandler pairs and facilitates sending any request for execution:

public class Mediator
{
   public Dictionary<Type, Type> _requestHandlers;

   public Mediator(Dictionary<Type, Type> requestHandlersMap)
   {
      _requestHandlers = requestHandlersMap;
   }

   public object Send(IRequest request)
   {
      var handlerType = _requestHandlers[request.GetType()];
      var handler = Activator.CreateInstance(handlerType);
      return handlerType.GetMethod("Execute").Invoke(handler, new object[] { request });
   }
}

Finally, we can use the Mediator as shown below:

var mediator = new Mediator(new Dictionary<Type, Type>()
{
   { typeof(GetUsersRequest), typeof(GetUsersRequestHandler) },
   { typeof(CreateUserRequest), typeof(CreateUserRequestHandler) }
});

var users = mediator.Send(new GetUsersRequest(count: 10)); // We don't care about the handler.
var userIsCreated = mediator.Send(new CreateUserRequest(username: "newUser"));

Use the Mediator Pattern when you want to model behavior that is distributed between several components.

A system’s behavior can emerge by many individual components that each one do a simple thing. By using the mediator, a more reactive system can be created. Here, reactive refers to the mediator responding to notifications from various participants, triggering additional actions among other participants, which in turn they notify the mediator again, and a complex behavior can emerge. For example a component can use the mediator to signal an event. This event may trigger three other components inside the mediator to be notified. These components may call the mediator again because another operation of interest needs to happen. This way you can model a complex behavior in your system, that is localized in a single component (Mediator) event though it involves multiple components.

In the following example, we make use of UI controls such as Dropdown, Textbox, Button, and FontEditor. The FontEditor is enabled only when the TextBox is enabled, because the FontEditor is responsible to change the font of the Textbox. Similarly, if the TextBox is disabled, the FontEditor must also be disabled. Additionally, when the user selects the Manual option in the dropdown, the Button and the Textbox must be enabled. In contrast, selecting the Auto option disables the Button and TextBox. These two procedures can be modeled separately in the mediator, making the design more maintainable.

Let’s begin by defining the Participants (UI controls):

public abstract class Participant
{
   protected Mediator Mediator { get; }
   protected Participant(Mediator mediator) { Mediator = mediator; }
}

public class DropDown : Participant
{
   private readonly Dictionary<string, bool> items;
   public DropDown(Mediator mediator) : base(mediator) 
   {
      // Initializing values.
      items = new Dictionary<string, bool>() { {"Auto", false }, {"Manual", false } };
   }

   public string SelectedItem => items.FirstOrDefault(x => x.Value).Key;

   public void SelectValue(string value)
   {
      // First we deselect any previous selected value.
      var selected = items.FirstOrDefault(x => x.Value).Key;
      if(selected != null)
         items[selected] = false;

      items[value] = true;
      Console.WriteLine("DropDown value changed to: " + value);
      Mediator.StateChanged(this); // We notify the mediator.
   }
}

public class Button : Participant
{
   public Button(Mediator mediator) : base(mediator) { }
   public void Enable() => Console.WriteLine("Button is Enabled.");
   public void Disable() => Console.WriteLine("Button is Disabled.");
}

public class TextBox : Participant
{
   public TextBox(Mediator mediator) : base(mediator) { }
   public bool IsEnabled { get; set; } = false;
   public void Enable()
   {
      IsEnabled = true;
      Console.WriteLine("TextBox is Enabled.");
      Mediator.StateChanged(this); // We notify the Mediator.
   }

   public void Disable()
   {
      IsEnabled = false;
      Console.WriteLine("TextBox is Disabled.");
      Mediator.StateChanged(this); // We notify the Mediator.
   }
}

public class FontEditor : Participant
{
   public FontEditor(Mediator mediator) : base(mediator) { }
   public void Enable() => Console.WriteLine("FontEditor is Enabled.");
   public void Disable() => Console.WriteLine("FontEditor is Disabled.");
}

Next, the mediator encapsulates the behavior of these components:

public class Mediator
{
   public DropDown DropDown;
   public Button Button;
   public TextBox TextBox;
   public FontEditor FontEditor;

   public Mediator() { /* Initialize controls.. */ }

   // This event is triggered whenever the Textbox or the Dropdown
   // changes their state.
   public void StateChanged(Participant participant)
   {
      if(participant == TextBox && TextBox.IsEnabled) {
         FontEditor.Enable();
         return;
      }

      if (participant == TextBox && !TextBox.IsEnabled) {
         FontEditor.Disable();
         return;
      }

      if (participant == DropDown && DropDown.SelectedItem == "Manual") {
         Button.Enable();
         TextBox.Enable();
         return;
      }

      if (participant == DropDown && DropDown.SelectedItem == "Auto") {
         Button.Disable();
         TextBox.Disable();
         return;
      }
   }
}

As we can see inside the Mediator, whenever a state change happens in the Dropdown, the mediator also changes the state of the TextBox. This also makes the Textbox to notify the mediator, because its state is also changed, and this triggers the mediator to change the state of the FontEditor. A usage example is shown below:

Mediator mediator = new Mediator();
var dropDown = mediator.DropDown;

// The following change will trigger two separate procedures.
// First, the Mediator will enable the button and the text box.
// The second procedure is triggered by the textbox and will enable the FontEditor.
dropDown.SelectValue("Manual");

// This also will trigger two procedures.
// In the first one, the mediator will disable the button and the textbox,
// and in the second one, the mediator will disable the FontEditor.
dropDown.SelectValue("Auto");

Below se present a diagram that illustrates the sequence of events between the different components.

The level of chains that can be formulated by this approach can be arbitrary, but keep in mind to avoid infinite loop situations.

Use the Mediator Pattern when you want to implement the Command and Query Segregation Pattern (CQRS)

The mediator decouples the sender from the receiver, allowing you to have commands with their corresponding handlers. This way, you can shift from a monolithic design to a more decoupled one. For example, a controller that accepts requests can utilize the Mediator Pattern to dispatch these requests to their appropriate handlers.

This pattern is commonly used in controllers, such as those in HTTP and GRPC. You can separate requests that don’t change the state of the application, from commands that do. Below we show an image for this kind of separation:

Separate requests that change the state of the system, from those that don’t.

In the following example, the Mediator Pattern is implemented for an HTTP controller, ProductsController, that contains create and get methods.

First, we create the Participants, which are the requests, commands and their handlers:

public interface IRequest { }

public interface IRequestHandler<T>
   where T : IRequest
{
   object Execute(T request);
}

// For every operation that changes state, we separate them in command objects.
public class CreateProductCommand : IRequest
{
   public string Name { get; }
   public decimal Price { get; }

   public CreateProductCommand(string name, decimal price)
   {
      Name = name;
      Price = price;
   }
}

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand>
{
   public object Execute(CreateProductCommand command)
   {
      // In this command we change the state of the application 
      // by creating a new product.
      Console.WriteLine("Creating the product " + command.Name);
      return true;
   }
}

// For every operation that retrieves data without changing state,
// we separate them in request objects.
public class GetProductsRequest : IRequest { }

public class GetProductsRequestHandler : IRequestHandler<GetProductsRequest>
{
   public object Execute(GetProductsRequest requst)
   {
      // In this request we don't change any system state. We just return the products
      // from the database.
      Console.WriteLine("Return the products");
      return new List<object>() { new { Name = "product1" }, new { Name = "product2" } };
   }
}

Next, the Mediator is similar to the previous examples:

public interface IMediator {
   object Send(IRequest request);
}

public class Mediator : IMediator
{
   public Dictionary<Type, Type> _requestHandlers;

   public Mediator(Dictionary<Type, Type> requestHandlersMap)
   {
      _requestHandlers = requestHandlersMap;
   }

   public object Send(IRequest request)
   {
      var handlerType = _requestHandlers[request.GetType()];
      var handler = Activator.CreateInstance(handlerType);
      return handlerType.GetMethod("Execute").Invoke(handler, new object[] { request });
   }
}

Finally, the controller has a reference to the IMediator interface and issues requests and commands to that interface:

public class ProductsController
{
   private readonly IMediator _mediator;

   public ProductsController(IMediator mediator)
   {
      _mediator = mediator;
   }

   // POST /product
   public void CreateProduct(string productName, decimal price)
   {
      this._mediator.Send(new CreateProductCommand(productName, price));
   }

   // GET /products
   public object GetProducts()
   {
      return this._mediator.Send(new GetProductsRequest());
   }
}

A usage example is shown below:

ProductsController controller = new ProductsController(new Mediator(new Dictionary<Type, Type>()
{
   { typeof(CreateProductCommand), typeof(CreateProductCommandHandler) },
   { typeof(GetProductsRequest), typeof(GetProductsRequestHandler) }
}));

controller.CreateProduct("product 1", 100);
var products = controller.GetProducts();

Mapping between requests and handlers doesn’t need to be done manually. Dependency injection can be used to eliminate the need for manual mapping, as discussed in the next sections.

How to implement

Define an IMediator interface.

Start by defining an IMediator interface with the necessary methods. Create a Notify method (or any other as per requirements) that enables participants to notify the Mediator.

public interface IMediator {
   void Notify(Participant participant, string info);
}

Creating an interface for Mediator is not mandatory, but it allows for different implementations of mediators to be used depending on requirements. Also, the Notify method can accept extra parameters from participants.

Create the Participant abstract class.

The Participant class is the common class for all the concrete participants. It holds a reference to the IMediator interface, which is used by participant subclasses. Each participant is a component that contains business logic, and notifies the mediator whenever its state changes or an event occurs.

public abstract class Participant
{
   protected readonly IMediator mediator;

   public Participant(IMediator mediator)
   {
      this.mediator = mediator;
   }
}

Create various Participant subclasses.

The various concrete participants notify the mediator instance, providing additional information if necessary. This triggers the mediator to coordinate participant actions.

public class Participant1 : Participant
{
   public Participant1(IMediator mediator) : base(mediator) { }

   public void ExecuteOperation()
   {
      // Execute business logic..
      this.mediator.Notify(this, "info:1:a");
      // Execute business logic..
   }

   public void ExecuteAnotherOperation()
   {
      // Execute business logic..
      this.mediator.Notify(this, "info:1:b");
      // Execute business logic..
   }
}

public class Participant2 : Participant
{
   public Participant2(IMediator mediator) : base(mediator) { }

   public void ExecuteOperation2()
   {
      this.mediator.Notify(this, "info:2:a");
   }

   public void ExecuteAnotherOperation2()
   {
      // This operation does't notify the Mediator.
   }
}

Create a ConcreteMediator that encapsulates the behavior among various participants.

Create an implementation of the IMediator interface, which encapsulates the behavior among various participants. The ConcreteMediator coordinates participants based on the notifications it receives.

public class ConcreteMediator : IMediator
{
   public Participant1 Participant1 { get; }
   public Participant2 Participant2 { get; }

   public ConcreteMediator()
   {
      Participant1 = new Participant1(this);
      Participant2 = new Participant2(this);
   }

   public void Notify(Participant participant, string info)
   {
      if(participant == Participant1)
      {
         if(info == "info:1:a")
         {
            // Orchestrate other participants or execute bussiness logic.
            Participant2.ExecuteAnotherOperation2();
         }else if (info == "info:1:b")
         {
            // Orchestrate other participants or execute bussiness logic.
         }
      }else if(participant == Participant2)
      {
         if (info == "info:2:a")
         {
            // Orchestrate other participants or execute bussiness logic.
            Participant2.ExecuteAnotherOperation2();
         }
      }    
   }
}

Mediator Pattern – Class diagram

In the class diagram provided below, we can observe the components of the Mediator Pattern.

Pros and Cons

Pros
  • Limits Subclassing – Because the mediator has all the routing and dispatching logic, no other component should be extended to support a new logic of routing and behavior, all the changes are localized to the mediator.
  • Decoupling of Participants – Participants only have a reference to the mediator, eliminating the need to reference other components. This makes the components easier to be extended and reused.
  • Simpler component interaction – The mediator converts many-to-many relationship between components to many one-to-many relationships between components and mediator, which is easier to maintain and extend.
  • Easier to understand participant behavior – Because the behavior of how objects cooperate with each other is located in the mediator, you can more easily understand and modify this behavior.
Cons
  • Complex Mediator – As all behavior is localized in the mediator, it can become complex. Applying additional principles and patterns to the mediator may be necessary in order to mitigate complexity to the degree needed.
  • The indirection introduced by the mediator may lead to a more difficult understanding of the execution flow and debugging. Organizing components in the project can help mitigate this side effect.

Real World Example

In .NET, the MediatR library is used for implementing the Mediator Pattern. Its useful, particularly in scenarios involving CQRS, or when the separation of requests and handlers is desired.

To integrate MediatR into your project, first install it by using the NuGet Package Manager. The library integrates with dependency injection, making it suitable for both web and console projects.

Below, we show how real world Web projects utilize the Mediator Pattern in order to separate queries from commands. Typically, for such a project, the folder structure can look like the following:

  • Controllers
    • UsersController.cs
  • Commands
    • CreateUserCommand.cs
    • Handlers
      • CreateUserCommandHandler.cs
  • Queries
    • GetUsersQuery.cs
    • Handler
      • GetUsersQueryHandler.cs

The controller handles HTTP requests related to users and dispatches them to the Mediator. The Queries folder contains requests and handlers that retrieve data from the database without altering the system’s state, and they can be scaled independently. On the other hand, the Commands directory holds objects responsible for changing the system’s state, in our case for user-related operations like create, update, and delete.

[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
   // We use the Mediator Pattern by using the MediatR library.
   private readonly IMediator _mediator;

   public UsersController(IMediator mediator)
   {
      _mediator = mediator;
   }

   [HttpGet]
   public async Task<IEnumerable<User>> Get(string nameStartsWith = null)
   {
      // We forward the GET request to a query.
      return await _mediator.Send(new GetUsersQuery(nameStartsWith));
   }

   [HttpPost]
   public async Task Post(string name)
   {
      // We forward the POST request to a command.
      await _mediator.Send(new CreateUserCommand(name));
   }
}

In the previous code, the controller utilizes the Mediator for handling GET and POST requests by forwarding them to corresponding queries and commands.

Next, we show the implementation of queries and their handlers:

public class GetUsersQuery : IRequest<List<User>>
{
   public string NameStartsWith { get; }

   public GetUsersQuery(string nameStartsWith)
   {
      NameStartsWith = nameStartsWith;
   }
}

public class GetUsersQueryHandler : IRequestHandler<GetUsersQuery, List<User>>
{
   private readonly DbContext _context;

   public GetUsersQueryHandler(DbContext context)
   {
      _context = context;
   }

   public Task<List<User>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
   {
      if (string.IsNullOrEmpty(request.NameStartsWith))
         return Task.FromResult(_context.GetUsers());

      return Task.FromResult(_context.GetUsers()
         .Where(x => x.Name.StartsWith(request.NameStartsWith))
         .ToList());
   }
}

These query and handler classes illustrate how requests for user data are processed. When a GetUsersQuery instance is sent to the Mediator, it automatically instantiates a new handler (GetUsersQueryHandler) and passes the query instance as a parameter.

Similarly, the implementation of commands is shown below:

public class CreateUserCommand : IRequest
{
   public string Name { get; }

   public CreateUserCommand(string name)
   {
      Name = name;
   }
}

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand>
{
   private readonly DbContext _context;

   public CreateUserCommandHandler(DbContext context)
   {
      _context = context;
   }

   public Task Handle(CreateUserCommand request, CancellationToken cancellationToken)
   {
      _context.InsertUser(new User(request.Name));
      return Task.CompletedTask;
   }
}

To complete the integration of MediatR, the dependency injection setup involves calling the AddMediatR extension method and passing the assembly where MediatR will scan for IRequest and IRequestHandler objects to register automatically:

builder.Services.AddMediatR(cfg => 
   cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

This way, many real world web applications can utilize the Mediator Pattern by separating different participants and concepts inside the application.

Example – Chat / Communication Hub

In this example, we demonstrate the implementation of the Mediator Pattern in a chat-communication system involving users and sensors. Various mediators act as hubs to facilitate communication between different participants. We’ll create different types of mediators, each serving a specific purpose within the application.

The code is available on GitHub

Requirements

  1. Users can communicate with each other and issue commands (send messages) to any sensor in the system. For this, a DirectMediator hub is created.
  2. The application supports the concept of groups, allowing participants (users or sensors) in a group to receive messages sent by any group member. The GroupMediator is designed for this purpose. This way, we can make particular sensors to notify their values to multiple users.
  3. Certain sensors need to broadcast their values to all participants. A BroadcastMediator is created for broadcasting messages to all participants.
  4. Some sensors have a constrain that whenever their value is updated, other sensors must also update their values. A subclass of GroupMediator will be created to handle this scenario.

Participants

First, we model the Participant abstract class along with the User and Sensor subclasses.

// The abstract class of each Participant, either User or Sensor.
public abstract class Participant
{
   public IMediator Mediator { get; set; }
   public abstract void Receive(Participant sender, object args);
}

// A User can send and receive messages to or from other users.
public class User : Participant
{
   // A User is uniquely identified by its name.
   public string Name { get; }
    
   public User(string name)
   {
      Name = name;
   }

   public override void Receive(Participant sender, object args)
   {
      Console.WriteLine($"User:{this}, Received From:{sender}, Message:{args}.");
   }

   public void Send(Participant receiver, object args)
   {
      Mediator.Notify(this, new List<object>() { receiver, args });
   }

   public override string ToString() => Name;
}

// A Sensor can receive commands and notify its state to the mediator.
public class Sensor : Component
{
   // A Sensor is uniquely identified by its id.
   public string Id { get; }
   public object LastValue { get; protected set; }

   public Sensor(string id)
   {
      Id = id;
   }

   public override void Receive(Participant sender, object args)
   {
      // A "measure" message makes the sensor to update its value.
      if(args == "measure")
      {
         LastValue = new Random().NextDouble(); // Make measurement...
         Mediator.Notify(this, LastValue);
      }
   }

   // This simulates the measurement the sensor does.
   public virtual void ValueChanged(object value)
   {
      LastValue = value;
      Mediator.Notify(this, LastValue);
   }

   public override string ToString() => $"Sensor({Id})";
}

Users can send and receive messages, while sensors can receive commands and notify their state changes to the mediator. Additionally, we can make a measurement by calling the ValueChanged method of the Sensor class, simulating this way a measurement that the sensor will normally do by its own. The last operation will trigger the sensor to notify its mediator/hub with the updated value.

Mediators

We will have a common IMediator interface with a Notify method for the participants to call. Below we present the interface:

public interface IMediator
{
   void Notify(Participant participant, object senderArgs);
}

The Notify method has two arguments. The first one, refers to the Participant that sends the message and the second arguments includes the message and in specific cases also the receiver of the message (as a two value array).

The DirectMediator, BroadcastMediator and GroupMediator are shown below:

public class DirectMediator : IMediator
{
   public void Notify(Participant sender, object args)
   {
      // The arguments consists of two parts. The first is the receiver
      // and the second the message.
      if (args is not List<object> argsList) return;
      if (argsList[0] is not Component receiver) return;

      receiver.Receive(sender, argsList[1]);
   }
}
// A class represents a group of participants.
public class Group
{
   public List<Participant> Participants = new();
   public bool ParticipantExists(Participant participant) 
      => Participants.Contains(participant);
}

// A Mediator that forwards messages to the recipients of a group.
public class GroupMediator : IMediator
{
   public List<Group> Groups { get; set; } = new();

   public virtual void Notify(Participant sender, object senderArgs)
   {
      var groupsToParticipantBelongsTo = Groups.Where(x=>x.ParticipantExists(sender))
         .ToList();
      var receivers = groupsToParticipantBelongsTo
         .SelectMany(x => x.Participants)
         .Where(x=> x != sender)
         .Distinct()
         .ToList();
      receivers.ForEach(x => x.Receive(sender, senderArgs));
   }
}

// Mediator that broadcasts the message to all participants.
public class BroadcastMediator : IMediator
{
   public List<Participant> Participants = new();

   public void Notify(Participant sender, object senderArgs)
   {
      Participants.ForEach(x=>x.Receive(sender, senderArgs));
   }
}

Each Mediator serves a different purpose, and each Participant of the system can have a different Mediator depending its needs.

Class Diagram

The Class diagram of our system so far, includes the different Mediators and Components as shown below:

Usage Example

Let’s put everything together by creating participants, mediators, and connecting them:

// Create the Users.
var alice = new User("Alice");
var bob = new User("Bob");
var jim = new User("Jim");
var tom = new User("Tom");

// Create the sensors.
var temperature = new Sensor("temperature");
var wind = new Sensor("wind");
var humidity = new Sensor("humidity");

// Create the mediators.
var directHub = new DirectMediator();
var groupHub = new GroupMediator();
var broadcastHub = new BroadcastMediator();

// Hook them together.
// For users we want to be able to send messages directly to each other,
// so we hook them with the DirectMediator
alice.Mediator = directHub;
bob.Mediator = directHub;
jim.Mediator = directHub;
tom.Mediator = directHub;

// The BroadcastMediator will have all users as recipients.
broadcastHub.Participants.AddRange(new[] {alice, bob, jim});

// We create two new groups inside the GroupMediator.
// Every time a participant uses this mediator and sends a messages,
// all other participants in that group (the sender belongs to) will be notified.
groupHub.Groups.Add(new Group() { Participants = new List<Participant>() { alice, bob, temperature } });
groupHub.Groups.Add(new Group() { Participants = new List<Participant>() { humidity, jim, tom } });

// We want temperature and humidity to notify multiple users.
temperature.Mediator = groupHub;
humidity.Mediator = groupHub;

// We want the wind sensor to notify all users.
wind.Mediator = broadcastHub;


bob.Send(alice, "a message"); // Output: User:Alice, Received From:Bob, Message:a message.
alice.Send(bob, "another message"); // Output: User:Bob, Received From:Alice, Message:another message.

temperature.ValueChanged(3.5m);
// Output: 
// User:Alice, Received From:Sensor(temperature), Message:3.5.
// User:Bob, Received From:Sensor(temperature), Message:3.5.

wind.ValueChanged(2);
// Output: 
// User:Alice, Received From:Sensor(wind), Message:2.
// User:Bob, Received From:Sensor(wind), Message:2.
// User:Jim, Received From:Sensor(wind), Message:2.

// The following command will make the humidity sensor to read a new value.
// This will notify its mediator (GroupMediator) which will forward the message 
// to Jim and Tom.
humidity.ValueChanged(10);
// Output: 
// User:Jim, Received From:Sensor(humidity), Message:10.
// User:Tom, Received From:Sensor(humidity), Message:10.

// Issue a command from bob to temperature sensor.
bob.Send(temperature, "measure"); 
// After the measurment, Alice and Bob will be notified from humidity sensor.
// Output: 
// User:Alice, Received From:Sensor(temperature), Message:0.4535193085338123.
// User:Bob, Received From:Sensor(temperature), Message:0.4535193085338123.


tom.Send(humidity, "measure"); 
// After the measurment, Jim and Tom will be notified from humidity sensor.
// Output: 
// User:Jim, Received From:Sensor(humidity), Message:0.4686073200408952.
// User:Tom, Received From:Sensor(humidity), Message:0.4686073200408952.

In the image below, you can see the connections between users and sensors and the mediators each one is associated with.

Connections between Participants and Mediators. Humidity icons created by Freepik – Flaticon Hub icons created by Freepik – Flaticon User icons created by Freepik – Flaticon Anemometer icons created by photo3idea_studio – Flaticon

Add a constrain between different Participants

In this section, we introduce a new constrain where, when a measurement is made in a particular sensor, other sensors must also update their values by making a new measurement. This can be achieved by allowing some sensors to send commands to other sensors through the Mediator. Thus, a sensor can send a measure command to another sensor, triggering its measurement.

For this requirement, we will create a new extension of the existing GroupSensor named SensorCommandrMediator. This mediator forwards commands to the sensors in the group, while other messages are propagated to the users of that group. This ensures that when a sensor belonging to a group sends a command (e.g., measure), the mediator forwards this command to the other sensors in the group, synchronizing their measurements. In addition, a new type of sensor, the AccelerometerSensor, will be created that every time we instruct it to make a measurement it will notify its mediator not only with its changed value, but also with a separate measure command.

// Sensor that we want to trigger other sensor's measurement immediately.
public class AccelerometerSensor : Sensor
{
   public AccelerometerSensor() : base("acceleration") { }

   public override void Receive(Participant sender, object args)
   {
      if (args == "measure")
      {
         LastValue = new Random().NextDouble(); // Make measurement.
         // Before we notify the mediator with its updated value, 
         // the sensor issues a measure command first to the mediator.
         Mediator.Notify(this, "measure");
         Mediator.Notify(this, LastValue);
      }
   }

   public override void ValueChanged(object value)
   {
      LastValue = value;
      Mediator.Notify(this, "measure"); // Same logic is applied here as in Receive method
      Mediator.Notify(this, LastValue);
   }
}

The new SensorCommandrMediator forwards commands to sensors and messages to users within the same group. It’s essentially an extension of the GroupMediator.

// Hub that forwards commands to sensors, and messages to users inside the same group.
public class SensorCommandrMediator : GroupMediator
{
   public override void Notify(Participant sender, object senderArgs)
   {
      var groupsThatParticipantBelongsTo = Groups.Where(x => x.ParticipantExists(sender))
         .ToList();
      var receivers = groupsThatParticipantBelongsTo
         .SelectMany(x => x.Participants)
         .Where(x => x != sender)
         .Distinct()
         .ToList();
      // If its a command, like measure, forward it only to the sensors of the group.
      if (senderArgs == "measure")
         receivers = receivers.Where(x => x is not User).ToList();
      else
         receivers = receivers.Where(x => x is not Sensor).ToList();
      receivers.ForEach(x => x.Receive(sender, senderArgs));
   }
}

Class Diagram

The updated class diagram now includes the new mediator and sensor:

Usage Example

The new AccelerometerSensor will trigger measurements for humidity and temperature sensors. To achieve this, we will create a new instance of SensorCommandrMediator and create a group containing those three sensors. Also, we want the values of those sensors to notify Jim and Bob, so we will add these two users to the group as well.

var alice = new User("Alice");
var bob = new User("Bob");
var jim = new User("Jim");
var tom = new User("Tom");

var temperature = new Sensor("temperature");
var wind = new Sensor("wind");
var humidity = new Sensor("humidity");

var acceleration = new AccelerationSensor();
var sensorCmndrHub = new SensorCommandrMediator();
sensorCmndrHub.Groups.Add(new Group() { Participants = new List<Participant>() { acceleration, humidity, temperature, jim, bob, tom } });
acceleration.Mediator = sensorCmndrHub;

var groupHub = new GroupMediator();
groupHub.Groups.Add(new Group() { Participants = new List<Participant>() { alice, bob, temperature } });
groupHub.Groups.Add(new Group() { Participants = new List<Participant>() { humidity, jim, tom } });

temperature.Mediator = groupHub;
humidity.Mediator = groupHub;

var directHub = new DirectMediator();
bob.Mediator = directHub;
jim.Mediator = directHub;

// When the acceleration sensors updates its value it triggers also
// the humidity and temperature sensors to update their values as well.
acceleration.ValueChanged(0.5);
// Output:
// User:Jim, Received From:Sensor(humidity), Message:0.5330086489846427.
// User:Tom, Received From:Sensor(humidity), Message:0.5330086489846427.
// User:Alice, Received From:Sensor(temperature), Message:0.23881147639207234.
// User:Bob, Received From:Sensor(temperature), Message:0.23881147639207234.
// User:Jim, Received From:Sensor(acceleration), Message:0.5.
// User:Bob, Received From:Sensor(acceleration), Message:0.5.
// User:Tom, Received From:Sensor(acceleration), Message:0.5.

// Issue a command from Jim to accelerometer sensor
jim.Send(acceleration, "measure"); // This will have the same effect as when the 
// sensor updates its value by its own.
// Output:
// User:Jim, Received From:Sensor(humidity), Message:0.12145959031368603.
// User:Tom, Received From:Sensor(humidity), Message:0.12145959031368603.
// User:Alice, Received From:Sensor(temperature), Message:0.007248347536896849.
// User:Bob, Received From:Sensor(temperature), Message:0.007248347536896849.
// User:Jim, Received From:Sensor(acceleration), Message:0.8051039464232773.
// User:Bob, Received From:Sensor(acceleration), Message:0.8051039464232773.
// User:Tom, Received From:Sensor(acceleration), Message:0.8051039464232773.

The new connections between Participants and Mediators, after the new AccelerometerSensor and SensorCommandrMediator, are shown below:

Connections between Participants and Mediators. Humidity icons created by Freepik – Flaticon Hub icons created by Freepik – Flaticon User icons created by Freepik – Flaticon Anemometer icons created by photo3idea_studio – Flaticon

Using Dependency Injection

Dependency injection is a powerful technique that enables flexible and loosely coupled code. In this section, we’ll explore how to use dependency injection for implementing the Mediator Pattern.

Autofac

The code with dependency injection is available on GitHub

C#

We can use Dependency Injection to register Mediators, Requests, and Handlers. This way the Mediator will know which handlers to instantiate depending on the requests it receives. We can do this in the following three ways:

Register every Request and Handler in a registrar inside the Mediator class manually:

var builder = new ContainerBuilder();

builder.RegisterType<RequestsAndHandlersExample.Mediator>()
   .As<RequestsAndHandlersExample.IMediator>();
builder.RegisterInstance(new MediatorRegistrar(new Dictionary<Type, Type>(){
   { typeof(GetUsersRequest), typeof(GetUsersRequestHandler) },
   { typeof(CreateUserRequest), typeof(CreateUserRequestHandler) }
}));

var container = builder.Build();
using var scope = container.BeginLifetimeScope();
var mediator = scope.Resolve<RequestsAndHandlersExample.IMediator>();
var result = mediator.Send(new GetUsersRequest(10));

Register every Request and Handler in a registrar automatically by using reflection and scanning the assembly:

var builder = new ContainerBuilder();
builder.RegisterType<RequestsAndHandlersExample.Mediator>()
   .As<RequestsAndHandlersExample.IMediator>();

// Automatically scan the assembly and load all request and handlers
var requestInterfaceType = typeof(RequestsAndHandlersExample.IRequest);
var assemblyTypes = Assembly.GetExecutingAssembly().GetTypes();
// Find all requests that implements the IRequest interface
var requestTypes = assemblyTypes
   .Where(x => requestInterfaceType.IsAssignableFrom(x))
   .Except(new List<Type>() { requestInterfaceType });

var requestHandlersMap = new Dictionary<Type, Type>();
// Foreach request find its handler.
foreach(var requestType in requestTypes)
{
   var handlerInterfaceType = typeof(RequestsAndHandlersExample.IRequestHandler<>)
      .MakeGenericType(requestType);
   var handlerType = assemblyTypes.FirstOrDefault(x => 
      handlerInterfaceType.IsAssignableFrom(x));
   requestHandlersMap.Add(requestType, handlerType);
}

builder.RegisterInstance(new MediatorRegistrar(requestHandlersMap));
var container = builder.Build();
using var scope = container.BeginLifetimeScope();
var mediator = scope.Resolve<RequestsAndHandlersExample.IMediator>();

var users = mediator.Send(new GetUsersRequest(count: 10));
var userIsCreated = mediator.Send(new CreateUserRequest(username: "newUser"));

Use the MediatR library and register it as shown below:

var builder = new ContainerBuilder();
var configuration = MediatRConfigurationBuilder
   .Create(Assembly.GetExecutingAssembly())
   .WithAllOpenGenericHandlerTypesRegistered()
   .Build();
builder.RegisterMediatR(configuration);
var container = builder.Build();
var mediator = container.Resolve<IMediator>();
// Requests and Handler must implement from MediatR types.
IServiceCollection services = new ServiceCollection();
services.AddMediatR(cfg => 
   cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

Similar Patterns

Facade Design Pattern – Abstracts the complexity of communicating with multiple components in a system, by providing an easy and convenient interface to interact with.

Observer Design Pattern – Multiple components can communicate and notify each other, by using Observer and Observable abstractions.

Further Reading

Looking to dive deeper into design patterns and take your programming skills to the next level? Check out the following highly recommended books:

clean code cover

Clean Code

Author: Robert C. Martin
Publication: August 1, 2008

head first design patterns

Head First – Design Patterns

Authors: Eric Freeman, Elisabeth Robson
Publication: January 12, 2021

* This site contains product affiliate links. We may receive a commission if you make a purchase after clicking on one of these links.

Write A Comment

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.