This post is part of the series SOLID Wash Tunnel.
"Mediator is a behavioral design pattern that lets you reduce chaotic dependencies between objects. The pattern restricts direct communications between the objects and forces them to collaborate only via a mediator object."
- Refactoring Guru
The mediator pattern lets you extract all the relationships between components (classes, modules, libraries) into a separate coordinator component. By doing so it we isolate any changes to one specific part of the codebase. Individual components become unaware of each others existance, although they still communicate with each other, but only through the mediator.
The mediator pattern enables capabilities like:
- Promotes loose coupling between components.
- Facilitates reuse of components.
- Improves code readability and maintainability.
But it also comes with some drawbacks like:
- The mediator object contains references to all of the components that it coordinates. Which in turn couples the mediator to everything.
- Over time a mediator object can evolve into a God Object.
Our implementation of the mediator pattern overcomes the drawbacks though 😉. It does so by only handling the instantiation of a command handler for a given command.
I want to preface up front that I do not suggest building your own generic mediator library. There is an excellent implementation of it available for .NET called MediatR. But there are also many available for different languages. Our implementation is very basic and is for learning purposes only.
We have already introduced the
ISignalTransmitter interface when we talked about the command message pattern. As we saw the motherboard was the component by which other (electronic) components like the
UserPanel communicated to
IMemory or to a specific
ISignalHandler, which would represent a small processing unit in a real world scenario like the wash tunnel.
ISignalTransmitter interface is analogous to
Mediator in the UML diagram. It is an abstraction of an object that invokes a command handler, and contains one method
T is any object which is an
public interface ISignalTransmitter
void Transmit<T>(T signal) where T : ISignal;
Motherboard is analogous to
ConcreteMediator in the UML diagram. As the word implies, this is the mediator object. The implementation looks scary at first but it is quite simple.
For a given
ISignal we do the following:
- Get the executing assembly.
- Get all the available types in this assembly.
- Filter the available types to include only
Tis of type of the supplied
ISignal. We also make sure that the types can not be interfaces or abstract classes. This check is needed to ensure that we can create an instance of the type.
- For each of the types that passed all checks, we use the injected
IContainerto resolve an instance of the type. Wether we get a brand new instance or the same, dependes on how we have registered the compontent in the IoC container.
- For each of the resolved handlers we pass in the
signaland let the handler/s perform whatever they need to do.
public class Motherboard : ISignalTransmitter
private readonly IContainer _container;
public Motherboard(IContainer container)
_container = container;
public void Transmit<T>(T signal) where T : ISignal
var signalHandlers = GetSignalHandlers<T>();
foreach (var handler in signalHandlers)
private IEnumerable<ISignalHandler<T>> GetSignalHandlers<T>() where T : ISignal
.Where(type => typeof(ISignalHandler<T>).IsAssignableFrom(type)
&& !type.IsInterface && !type.IsAbstract)
.Select(type => (ISignalHandler<T>)_container.GetService(type));
If you did not notice by now, we are doing two things which are not considered best practices.
Multiple signal handers for the same signal
In software engineering, it is considerd a bad practise to have multiple command handers for the same command. The main reason is that if any of the handlers fails to handle the request, this will put the overall system in an inconsistent state. This is especially true in case of distributed systems because of the loose coupling there is not a good way to tell all other participants to roll back their transaction because you do not know who the receivers of the request are.
This is not the case for notifications though! It is perfectly valid to have multiple notification handlers that can listen to a specific notification that has happend.
Since our mediator does not target a specific type of request (command or notification) it is not a problem. An easy fix for this would be to sepparate the
ISignalinto two interfaces, an
If the request is of type
ICommandwe can simply resolve the last handler registered to the IoC container in case of multiple handlers for the same request. If there is no handler at all we can throw exception to notify the developer.
If the request is of type
INotificationwe dispatch the signal to all notification handlers.
IContainerand using it to resolve the signal handlers
This is something known as the Service Locator anti-pattern. We will elaborate it more in-depth in the next post.
|OCP||✅||Different signals and signal handlers can be added, yet the |
|LSP||✅||You can supply any implementation of |
|DIP||✅||High level modules like |
Continue the series on Service Locator.
If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.