How to Reduce Cyclomatic Complexity Part 13: Service Locator Pros and Cons

by Zoran Horvat

Service locator design pattern is widely considered an anti-pattern. But that statement requires some explanation. There are clearly some design problems when this pattern is used, but the design pattern itself is not the cause of problems. Real cause is the way the pattern is used, and that is what we will discuss in this article.

Root Cause of Problems With Service Locator

When a class depends on the service locator, it hides its real dependencies. It is as simple as that.

We cannot tell the dependencies by looking at the class declaration. Instead, we have to read the complete class, and possibly its collaborators as well, to figure what other classes are required for them to work.

Simple Example

Suppose that we are modeling a car production plant. Plant produces cars and delivers them to the selling location:

class Car
{

}

class CarProducer
{
    public void DeliverTo(int carsCount, string town)
    {
        Car[] cars = new Car[carsCount];
        ...
    }
}

Now the producer needs help of another entity, transporter, which will help it deliver cars to the specified location:

class Transporter
{

    public string Name { get; private set; }

    public Transporter(string name)
    {
        this.Name = name;
    }

    public void Deliver(Car[] cars, string town)
    {
        Console.WriteLine("Delivering {0} car(s) to {1} by {2}",
                            cars.Length, town, this.Name);
    }
}

Using the Service Locator

How do we make producer meet transporter in this solution? One way to accomplish that is to use the service locator:

static class TransporterLocator
{
    static IList<Transporter> transporters = new List<Transporter>();

    public static void Register(Transporter transporter)
    {
        transporters.Add(transporter);
    }

    public static Transporter Locate(string name)
    {
        return
            transporters
                .Where(transporter => transporter.Name == name)
                .Single();
    }
}

This is the static class which maintains the collection of available transporters. Each transporter is uniquely identified by its name. Therefore, when the client (car producer in this case) needs a transporter, it only has to tell its name:

class CarProducer
{
    public void DeliverTo(int carsCount, string town)
    {

        Car[] cars = new Car[carsCount];

        Transporter transporter = null;
        if (carsCount <= 12)
            transporter = TransporterLocator.Locate("truck");
        else
            transporter = TransporterLocator.Locate("train");

        transporter.Deliver(cars, town);

    }
}

In this solution, car producer simply asks the transporter locator for the appropriate transportation method. For small number of cars, producer would like to use a truck. Otherwise, most appropriate transportation mean is the train.

Identifying Problems With Service Locator

To understand what is wrong with previous solution, we should try to use it:

TransporterLocator.Register(new Transporter("truck"));
TransporterLocator.Register(new Transporter("train"));

CarProducer producer = new CarProducer();
producer.DeliverTo(7, "carville");
producer.DeliverTo(74, "carville");

As you can see, we cannot use the CarProducer class if we didn't prepare the locator before. CarProducer class is not autonomous, and it breaks one of the fundamental principles of software design: If we have a reference to an object, that object is correctly formed.

If we just forgot to setup the locator before using the car producer, this very same object would fail:

TransporterLocator.Register(new Transporter("truck"));

CarProducer producer = new CarProducer();
producer.DeliverTo(7, "carville");
producer.DeliverTo(74, "carville");

This piece of code fails because it expects the train to be registered with the service locator as well.

Bottom line is that the same object may work properly or fail only because some other class was prepared in one way or the other. It is much better to design the CarProducer class in such way that it cannot be instantiated if objects it requires are not setup properly. That is the next solution we will demonstrate.

Removing the Service Locator

As already said, well designed class will make sure that its instances are well formed whenever the initialization passes without an error. If we have a reference to an object, we want to be sure that the object is well formed. We do not expect basic errors thrown back from the object, telling us that something is not set up.

One way to implement a class is to list all mandatory dependencies in its constructor. That way, there will be no legal way to construct an object if dependencies are not available.

class CarProducer
{

    private Transporter truck;
    private Transporter train;

    public CarProducer(Transporter truck, Transporter train)
    {

        if (truck == null)
            throw new ArgumentNullException("truck");

        if (train == null)
            throw new ArgumentNullException("train");

        this.truck = truck;
        this.train = train;

    }

    public void DeliverTo(int carsCount, string town)
    {

        Car[] cars = new Car[carsCount];

        Transporter transporter = this.truck;
        if (carsCount > 12)
            transporter = this.train;

        transporter.Deliver(cars, town);

    }
}

In this implementation CarProducer requires all dependencies upfront. It is impossible to instantiate the CarProducer class without explicitly providing two transportation means it depends on.

Even more than that - constructor implementation begins with two guard clauses. If any of the two Transporters is null, CarProducer constructor will throw an exception and object will not be created. With this implementation we are sure that object is valid if it exists. That is a very important concept which guards us from inconsistent system states.

But then, is there a situation in which Service Locator is an acceptable solution? It turns that in some cases it might be better to have the locator rather than to make all dependencies explicit.

Service Locator vs. Object Orientation

Previous example could be viewed through a different set of lenses. We could say that the problem with car producer and transporter is that they are both full blown objects. But when connection between them was established using Service Locator, it turned that they were connected by means which are definitely not object-oriented.

Techniques that we usually apply to classes fail in presence of the Service Locator. For example, we cannot tell what conditions must be satisfied by just looking at the class's constructor. We cannot tell whether an object we have somehow obtained is operational or it will blow up when its method is invoked. We cannot instantiate this class in a test harness because it depends on some obscure objects that are set up elsewhere.

All these are serious issues. Those are basic reasons why Service Locator is considered an anti-pattern. But... I will use this opportunity to add: this is anti-pattern in object-oriented code. But not all our code is truly object-oriented.

Persistence logic is by definition not object-oriented when relational database is in used. Persistence logic then mainly consists of mapping model data to plain tables. User interface logic is also not object-oriented, because it mainly consists of mappings between plain data and user interface elements.

Common element in both cases is mapping, and that is precisely what Service Locator does - it maps keys to objects. So why don't we use the Service Locator in layers that are notoriously not object-oriented?

Conclusion

In this article we have paid attention to one design pattern which is widely avoided in practice - Service Locator. The problem with Service Locator is that it violates principles of object-oriented design.

But at the same time, there are regions of code which are not object-oriented by their nature. Presentation layer and persistence layer are not object-oriented. Instead, they primarily deal with mapping the model to other things - tables and columns in the database or user interface elements.

Those are the places where Service Locator design pattern can safely be applied without violating any of the object-oriented guidelines, simply because those places are not object-oriented at all.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub