http://www.codinghelmet.com/  

Wear a helmet. Even when coding.

howto > reduce-cyclomatic-complexity-service-locator-pros-and-cons

How to Reduce Cyclomatic Complexity: Service Locator Pros and Cons
by Zoran Horvat @zoranh75

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.

See also:

Published: Jun 30, 2015; Modified: Jul 7, 2015

ZORAN HORVAT

Zoran is software architect dedicated to clean design and CTO in a growing software company. Since 2014 Zoran is an author at Pluralsight where he is preparing a series of courses on object-oriented and functional design, design patterns, writing unit and integration tests and applying methods to improve code design and long-term maintainability.

Follow him on Twitter @zoranh75 to receive updates and links to new articles.

Watch Zoran's video courses at pluralsight.com (requires registration):

Making Your C# Code More Object-Oriented

This course will help leverage your conceptual understanding to produce proper object-oriented code, where objects will completely replace procedural code for the sake of flexibility and maintainability. More...

Advanced Defensive Programming Techniques

This course will lead you step by step through the process of developing defensive design practices, which can substitute common defensive coding, for the better of software design and implementation. More...

Tactical Design Patterns in .NET: Creating Objects

This course sheds light on issues that arise when implementing creational design patterns and then provides practical solutions that will make our code easier to write and more stable when running. More...

Tactical Design Patterns in .NET: Managing Responsibilities

Applying a design pattern to a real-world problem is not as straight-forward as literature implicitly tells us. It is a more engaged process. This course gives an insight to tactical decisions we need to make when applying design patterns that have to do with separating and implementing class responsibilities. More...

Tactical Design Patterns in .NET: Control Flow

Improve your skills in writing simpler and safer code by applying coding practices and design patterns that are affecting control flow. More...

Writing Highly Maintainable Unit Tests

This course will teach you how to develop maintainable and sustainable tests as your production code grows and develops. More...

Improving Testability Through Design

This course tackles the issues of designing a complex application so that it can be covered with high quality tests. More...

Share this article

webmasters