7

Service Locator is an Anti-Pattern

 3 years ago
source link: https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Service Locator is a well-known pattern, and since it was described by Martin Fowler, it must be good, right?

No, it's actually an anti-pattern and should be avoided.

Let's examine why this is so. In short, the problem with Service Locator is that it hides a class' dependencies, causing run-time errors instead of compile-time errors, as well as making the code more difficult to maintain because it becomes unclear when you would be introducing a breaking change.

OrderProcessor example #

As an example, let's pick a hot topic in DI these days: an OrderProcessor. To process an order, the OrderProcessor must validate the order and ship it if valid. Here's an example using a static Service Locator:

public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        var validator = Locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper = Locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

The Service Locator is used as a replacement for the new operator. It looks like this:

public static class Locator
{
    private readonly static Dictionary<Type, Func<object>>
        services = new Dictionary<Type, Func<object>>();
 
    public static void Register<T>(Func<T> resolver)
    {
        Locator.services[typeof(T)] = () => resolver();
    }
 
    public static T Resolve<T>()
    {
        return (T)Locator.services[typeof(T)]();
    }
 
    public static void Reset()
    {
        Locator.services.Clear();
    }
}

We can configure the Locator using the Register method. A ‘real' Service Locator implementation would be much more advanced than this, but this example captures the gist of it.

This is flexible and extensible, and it even supports replacing services with Test Doubles, as we will see shortly.

Given that, then what could be the problem?

API usage issues #

Let's assume for a moment that we are simply consumers of the OrderProcessor class. We didn't write it ourselves, it was given to us in an assembly by a third party, and we have yet to look at it in Reflector.

This is what we get from IntelliSense in Visual Studio:

image

Okay, so the class has a default constructor. That means I can simply create a new instance of it and invoke the Process method right away:

var order = new Order();
var sut = new OrderProcessor();
sut.Process(order);

Alas, running this code surprisingly throws a KeyNotFoundException because the IOrderValidator was never registered with Locator. This is not only surprising, it may be quite baffling if we don't have access to the source code.

By perusing the source code (or using Reflector) or consulting the documentation (ick!) we may finally discover that we need to register an IOrderValidator instance with Locator (a completely unrelated static class) before this will work.

In a unit test test, this can be done like this:

var validatorStub = new Mock<IOrderValidator>();
validatorStub.Setup(v => v.Validate(order)).Returns(false);
Locator.Register(() => validatorStub.Object);

What is even more annoying is that because the Locator's internal store is static, we need to invoke the Reset method after each unit test, but granted: that is mainly a unit testing issue.

All in all, however, we can't reasonably claim that this sort of API provides a positive developer experience.

Maintenance issues #

While this use of Service Locator is problematic from the consumer's point of view, what seems easy soon becomes an issue for the maintenance developer as well.

Let's say that we need to expand the behavior of OrderProcessor to also invoke the IOrderCollector.Collect method. This is easily done, or is it?

public void Process(Order order)
{
    var validator = Locator.Resolve<IOrderValidator>();
    if (validator.Validate(order))
    {
        var collector = Locator.Resolve<IOrderCollector>();
        collector.Collect(order);
        var shipper = Locator.Resolve<IOrderShipper>();
        shipper.Ship(order);
    }
}

From a pure mechanistic point of view, that was easy - we simply added a new call to Locator.Resolve and invoke IOrderCollector.Collect.

Was this a breaking change?

This can be surprisingly hard to answer. It certainly compiled fine, but broke one of my unit tests. What happens in a production application? The IOrderCollector interface may already be registered with the Service Locator because it is already in use by other components, in which case it will work without a hitch. On the other hand, this may not be the case.

The bottom line is that it becomes a lot harder to tell whether you are introducing a breaking change or not. You need to understand the entire application in which the Service Locator is being used, and the compiler is not going to help you.

Variation: Concrete Service Locator #

Can we fix these issues in some way?

One variation commonly encountered is to make the Service Locator a concrete class, used like this:

public void Process(Order order)
{
    var locator = new Locator();
    var validator = locator.Resolve<IOrderValidator>();
    if (validator.Validate(order))
    {
        var shipper = locator.Resolve<IOrderShipper>();
        shipper.Ship(order);
    }
}

However, to be configured, it still needs a static in-memory store:

public class Locator
{
    private readonly static Dictionary<Type, Func<object>>
        services = new Dictionary<Type, Func<object>>();
 
    public static void Register<T>(Func<T> resolver)
    {
        Locator.services[typeof(T)] = () => resolver();
    }
 
    public T Resolve<T>()
    {
        return (T)Locator.services[typeof(T)]();
    }
 
    public static void Reset()
    {
        Locator.services.Clear();
    }
}

In other words: there's no structural difference between the concrete Service Locator and the static Service Locator we already reviewed. It has the same issues and solves nothing.

Variation: Abstract Service Locator #

A different variation seems more in line with true DI: the Service Locator is a concrete class implementing an interface.

public interface IServiceLocator
{
    T Resolve<T>();
}
 
public class Locator : IServiceLocator
{
    private readonly Dictionary<Type, Func<object>> services;
 
    public Locator()
    {
        this.services = new Dictionary<Type, Func<object>>();
    }
 
    public void Register<T>(Func<T> resolver)
    {
        this.services[typeof(T)] = () => resolver();
    }
 
    public T Resolve<T>()
    {
        return (T)this.services[typeof(T)]();
    }
}

With this variation it becomes necessary to inject the Service Locator into the consumer. Constructor Injection is always a good choice for injecting dependencies, so OrderProcessor morphs into this implementation:

public class OrderProcessor : IOrderProcessor
{
    private readonly IServiceLocator locator;
 
    public OrderProcessor(IServiceLocator locator)
    {
        if (locator == null)
        {
            throw new ArgumentNullException("locator");
        }
 
        this.locator = locator;
    }
 
    public void Process(Order order)
    {
        var validator =
            this.locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper =
                this.locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}

Is this good, then?

From a developer perspective, we now get a bit of help from IntelliSense:

image

What does this tell us? Nothing much, really. Okay, so OrderProcessor needs a ServiceLocator - that's a bit more information than before, but it still doesn't tell us which services are needed. The following code compiles, but crashes with the same KeyNotFoundException as before:

var order = new Order();
var locator = new Locator();
var sut = new OrderProcessor(locator);
sut.Process(order);

From the maintenance developer's point of view, things don't improve much either. We still get no help if we need to add a new dependency: is it a breaking change or not? Just as hard to tell as before.

Summary #

The problem with using a Service Locator isn't that you take a dependency on a particular Service Locator implementation (although that may be a problem as well), but that it's a bona-fide anti-pattern. It will give consumers of your API a horrible developer experience, and it will make your life as a maintenance developer worse because you will need to use considerable amounts of brain power to grasp the implications of every change you make.

The compiler can offer both consumers and producers so much help when Constructor Injection is used, but none of that assistance is available for APIs that rely on Service Locator.

You can read more about DI patterns and anti-patterns in my book.

Update 2014-05-20: Another way to explain the negative aspects of Service Locator is that it violates SOLID.

Update 2015-10-26: The fundamental problem with Service Locator is that it violates encapsulation.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK