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.
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.
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);
}
}
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.
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.
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.
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?
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
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
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.