This post is part of the series SOLID Wash Tunnel.
Definition
"A Service Locator supplies application components outside the Composition Root with access to an unbounded set of Dependencies."
- Steven van Deursen, Mark Seemann
Service Locator allows one to get hold of a reference to a dependecy that you depend on. This is done in cases where:
- You can not (somehow) inject said dependecy.
- The amount of dependecies you need to inject is overwhelming.
The reason why it is considered by many to be an anti-pattern is because it hides the dependencies of a class, causing run-time errors instead of compile-time errors. It also becomes unclear when you would be introducing a breaking change.
Implementation
Motherboard
We have already introduced the Motherboard
when we talked about the mediator pattern. The Motherboard
is the concrete implementation of ISignalTransmitter
which was used to find the appropriate ISignalHandler
/s for a given ISignal
.
When a new signal is transmitted the motherboard class use the injected IContainer
(the service locator in this case) in order to resolve the appropriate ISignalHandler
/s based on the generic T
parameter.
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)
{
handler.Handle(signal);
}
}
private IEnumerable<ISignalHandler<T>> GetSignalHandlers<T>() where T : ISignal
=> Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(type => typeof(ISignalHandler<T>).IsAssignableFrom(type)
&& !type.IsInterface && !type.IsAbstract)
.Select(type => (ISignalHandler<T>)_container.GetService(type));
}
Alternatives
"What would be some alternative ways to deliver the same functionality without the service locator, and what would be their drawbacks?"
There are two ways I can think of to remove the IContainer
dependecy, so that we would remove the service locator.
Constructor Injection
Injecting an IEnumerable<ISignalHandler<T>>
in the Motherboard
constructor. This would require modification of the ISignalTransmitter
interface too.
public interface ISignalTransmitter<T>
where T : ISignal
{
void Transmit(T signal);
}
public class Motherboard<T> : ISignalTransmitter<T>
where T : ISignal
{
private readonly IEnumerable<ISignalHandler<T>> _signalHandlers;
public Motherboard(IEnumerable<ISignalHandler<T>> signalHandlers)
{
_signalHandlers = signalHandlers;
}
public void Transmit(T signal)
{
foreach (var handler in _signalHandlers)
{
handler.Handle(signal);
}
}
}
The drawback would be that the clients which transmit multiple signals like UserPanel
would require multiple dependecies of ISignalTransmitter<T>
, and also would have to specify the type T
.
- To transmit an
WashProgramSelectedSignal
signal, theUserPanel
would require a dependecy onISignalTransmitter<WashProgramSelectedSignal>
. - To transmit an
VehicleWashingStartedSignal
signal, theUserPanel
would require a dependecy onISignalTransmitter<VehicleWashingStartedSignal>
. - And on and on...
This would make the clients life miserable, as opposed to having a need for a single dependecy on the ISignalTransmitter
.
Method Injection
Injecting an IEnumerable<ISignalHandler<T>>
in the Transmit
method along with the actual signal
. This would require modification of the ISignalTransmitter
interface too.
public interface ISignalTransmitter
{
void Transmit<T>(T signal, IEnumerable<ISignalHandler<T>> signalHandlers)
where T : ISignal;
}
public class Motherboard : ISignalTransmitter
{
public void Transmit<T>(T signal, IEnumerable<ISignalHandler<T>> signalHandlers)
where T : ISignal
{
foreach (var handler in signalHandlers)
{
handler.Handle(signal);
}
}
}
The drawback would be that the clients would have to supply the implementations of the ISignalHandler
's themselves, which in turn throws loose coupling out the window.
Closing Thoughts
In both alternatives, the benefit of removing the service locator from Motherboard
simply don't outweigh the drawbacks in my opinion!
In general my advise would be to avoid the Service Locator. But in the other hand, just because it is considered to be an anti-pattern does not neccessarily make it "the evil". There are usecases like the one with the motherboard that makes it the best choice. Always considering what 'best choice' means to you the reader!
Continue the series on Fluent Builder (part 1/3).
If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.