I have to admit to something. For the last 2 years, I was using Service Locator Pattern in one of my projects while being completely convinced that I was using Dependency Injection, but it turns out that I'm not alone in making such a mistake. What are the differences, what are the consequences of being wrong, and how to distinguish these two patterns?
Let's start with a quick primer, as it turns out definitions do matter, and they can be very helpful, if, and when one understands them correctly.
Dependency Injection 101
By Wikipedia: Dependency injection is a technique in which an object receives other objects that it depends on. The injection can happen in several ways, from which the most important and most often used is constructor injection.
Without DI - building the
Service is done by the
class Client: def __init__(self): self.my_service = Service() def method(self): self.my_service.use()
With DI -
Service class instance is built outside and passed to the
class Client: def __init__(self, my_service): self.my_service = my_service def method(self): self.my_service.use()
The essential goal of DI is dividing the build phase of an object from the usage phase, which in return gives us a much more clearer code and greater reusability, as we can use polymorphism thanks to this pattern.
Let me stress that the single most pronounced feature of DI is passing the dependency from the outside.
Service Locator Pattern 101
It's yet another technique for providing client classes with proper dependencies. It consists of using the in client class a static registry which then the client class asks for the needed dependency.
class Client: def __init__(self, service_locator): self.service_locator = service_locator def method(self): my_service = self.service_locator.get('my_service') my_service.use()
So in the case of the Service Locator pattern, the most pronounced feature is that the
Client class does not receive the dependency but in turn asks for it the service locator also known as a registry.
Service Locator pattern is commonly considered as an anti-pattern, because the dependencies are not explicit, they cannot be checked by static type checkers; the pattern encourages to build god-classes, and it is very hard to test such code because you need to mock the service locator itself to be able to pass a particular fake-dependency.
The pattern, however, does have some pros: using it allows for quick development of the application, as the developer does not need to carefully craft the dependency graph of the objects - as the dependencies are built usually lazy - when required, as opposed to DI fashion, which needs an assembly (aka. application bootstrap) phase. But really, the pros definitely do not weigh out the cons, so how did it happen that I was using it?
How to distinguish them then?
First of all, you need to know the two patterns and the differences between them, to be honest, I haven't heard about Service Locator Pattern until I didn't start wondering why is it so hard to test the code in our project? We were using a library that claimed to be a Dependency Injection container, and because I did not know the second pattern I believed in what it claimed.
I did know the name Service Locator, but because I also knew it was an anti-pattern I did not invest in really understanding what that name means - lessons learned: know the anti-patterns to not make them by yourself and do not believe in everything that is written on the Internet ;)
I've mentioned before that the definitions are often useful, and I think that this is such a case, so for better understanding let me present a metaphor so it sticks with you:
imagine manufacture with many positions with different roles and needs - particularly a set of tools. Positions are our client classes and the tools are the dependencies. In the case of the Service Locator Pattern, there is a manager which runs from position to position and delivers the required set of tools "on the go". In the case of Dependency Injection, there is a manager who knows the needs of each position and puts the right tools at the start of the workday.
It's not always that plain and simple...
...and that's the problem! The main reason for me not realizing sooner that we aren't using DI but SL was lack of knowledge, but the second was that the library which we were using most likely introduced SL pattern also without the knowledge that it stopped being DI.
So first our classes looked like so:
class Client: _ @inject def __init__(self, service: Service): self.my_service = service def method(self): self.my_service.use()
@inject decorator which is responsible for injecting the dependencies from the container to the client class (based on the given type annotation).
But the author of the library made a design twist, which made the code prettier but had disastrous consequences:
class Client: my_service: Service = Inject() def method(self): self.my_service.use()
notice how the
__init__ method is not even needed anymore because the solution resolved to use the descriptor, which works lazily - implicitly it uses a static registry under the hood, making it an example of a service locator pattern. Knowing what I know now it's quite obvious to me, but at that time it really wasn't.
To support the argument that the accidental introduction of a Service Locator Pattern is an easy thing to miss here is a link to a Github issue, in which author of a dependency injection container was thinking about a similar solution to the one above, also using a descriptor.
Also, notice how misusing a dependency container makes it a service locator:
class Client: def __init__(self, di_container): self.di_container = di_container def method(self): my_service = self.di_container.get('my_service') my_service.use()
So whenever one passes the whole container to a client class it actually introduces a completely different pattern.
Things to remember:
- Dependency Injection is all about passing the dependencies from the outside into the client class, preferable while constructing the client class
- Service Locator Pattern, in turn, resolves to the client class itself getting the dependencies with the help of a special service locator, usually a global or static registry.
- It's possible to introduce the Service Locator pattern while being in good faith of using Dependency Injection, often by injecting the whole container, or by attaching the SL pattern to the client class with a construct like e.g. a descriptor.
- It's good to know and understand some anti-patterns, to know what not to do.
- Service Locator isn't always bad and can be useful for wiring up the whole application but that's a longer topic.