The Service Locator pattern (or anti-pattern) is one of the many Dependency Injection patterns that allow us to create loosely-coupled code. I mentioned this pattern just a bit in my review of Mark Seemann's excellent book Dependency Injection in .NET (Jeremy's Book Review).
Seemann refers to the Service Locator as an anti-pattern -- meaning that we probably don't want to use it in most cases because there are better patterns out there. But he also notes that he used the pattern for many years before coming to grips with its shortcomings.
As mentioned in the book review, I have also used the Service Locator, and I was a little surprised when it was referred to as an anti-pattern. But in reading Seemann's description of the shortcomings, I had no choice but to agree. I had actually come across these shortcomings in my own code, but in that environment, it made sense to continue to use the pattern.
So let's compare the Service Locator to one of the other DI patterns (specifically, the Constructor Injection pattern). This will point out the problems that I ran across -- which also happen to be the problems that Seemann mentions in his book.
Constructor Injection
We'll start by taking a look at the Constructor Injection pattern. For a detailed description of Constructor Injection, refer to my presentation "Dependency Injection: A Practical Introduction".
The basics of Constructor Injection is that we pass the dependencies that a class needs as parameters in the constructor. Here's an example:
In this constructor, we are injecting an IPersonService and a CatalogOrder. The constructor then assigns these to local fields that can be used by the class. Whatever creates this object is responsible for supplying these dependencies. Generally, this is done in the composition root and often involves a Dependency Injection container.
Now, let's compare this to the Service Locator.
Service Locator
The Service Locator varies a bit from Constructor Injection. As mentioned above, with Constructor Injection, something else is responsible for resolving the dependencies that are needed by the class.
In contrast, when using the Service Locator, the class itself is responsible for resolving its dependencies by asking for them from the Service Locator. This can be a DI container or some other object or method that can return dependencies. In our sample, we'll use a DI container (Unity).
Here's a sample constructor:
Notice that the constructor is passed the entire DI container (although there are other ways to get the service locator into the object). Then the object uses the service locator to pick out its own dependencies. This takes the control away from the composition root and gives it to the object itself.
Note that just because you are using a DI container doesn't mean that you are using the Service Locator pattern. The Service Locator is defined by how the container is used (i.e., whether the object is resolving its own dependencies).
On the surface the Service Locator appears to hit all of the points of Dependency Injection: it allows for extensibility; it allows for the swapping out of test doubles; it allows for late binding. This is why it is considered by many to be a valid DI pattern.
But it has a major shortcoming: It hides the dependencies that are being used.
This might not seem like a big problem, but let's take a look at some unit tests to show the shortcoming in action.
Initial Unit Tests
We'll start by unit testing the constructor that uses Constructor Injection. Here's our test:
Notice here that when we instantiate our CatalogViewModel, we must provide the both the IPersonService (_personService) and the CatalogOrder (_currentOrder). If we didn't provide both of these parameters, the test code would not build. As a side note, both of these dependencies (_personService and _currentOrder) are mock objects that are created in the test setup (not shown).
This test builds and passes with our current code.
Now let's look at the same test for the constructor that uses the Service Locator:
In this test, we must pass in a populated Unity container (called "_locator" in this case). As noted in the comments, our "_locator" has both an IPersonService and a CurrentOrder registered in its catalog (these are added in the test setup).
This test builds and passes with our current code.
The Problem with Hiding Dependencies
With Constructor Injection, we can easily see that we have 2 dependencies just by looking at the constructor. However, with the Service Locator, we don't have visibility to the dependencies from the outside (meaning, looking at the public API signatures of the class).
Let's see what happens when we add a 3rd dependency. Our Constructor Injection implementation now looks like this:
The first thing that happens is that we get a build failure of our unit test. Note the squigglies below:
Build failures are good. They give you immediate feedback that something is wrong. In this case, we can see that we have a missing dependency since the constructor wants a 3rd parameter. We would see this same error throughout our code base wherever we try to construct a CatalogViewModel. And we know exactly what to fix.
But what about the Service Locator. Let's add another dependency:
If we rebuild our application, everything builds fine -- including the existing unit test for the constructor that uses the Service Locator. The problem is when we run the tests.
Since our tests do not know about the 3rd dependency, the test setup does not load it into the Unity container. So, this test will throw an exception (when it tries to resolve the "CurrentUser" from the container). In addition, all other tests we have that are creating this object will fail with the same exception.
This is the equivalent of a runtime error. Whenever we have a choice between a compile-time error and and runtime error, we should pick the compile-time error.
Wrap Up
As we've seen the Service Locator pattern is a valid DI pattern in that it addresses the issues that we are trying to resolve with Dependency Injection. However, it does have a flaw: it keeps the actual dependencies hidden.
When we have a choice between the Service Locator and Constructor Injection, we should favor Constructor Injection. Constructor Injection makes the dependencies completely obvious to whomever is using the class. And if the dependencies need to change, the result is build failures -- we don't have errors accidentally slipping into compiled code.
I have used the Service Locator pattern in production code. And it did work. But I did run into the issue described here. When we added another dependency to an object, the unit tests would build successfully but then all fail (it's a bit disconcerting when you have 30 unit tests fail at once). The test failures were not a problem with the code, they were a problem with the test setup. Since the dependency was hidden by the Service Locator, it wasn't until we received these failures that we realized we needed to go in and add the dependencies in the setup.
So, if you're willing to live with the shortcoming, you can consider the Service Locator a DI pattern. For everyone else, consider it an anti-pattern. As with everything, we need to understand the pros and cons of each tool that we use so that we can make informed choices. This is how we build maintainable, extensible code.
Happy Coding!
Awesome...
ReplyDeletethe best post I have seen on the topic.thanks
ReplyDeleteTwo years later and it's still very helpful. Thanks!
ReplyDeleteAggree with Sothy Chan. Thank you Jeremy!
ReplyDeleteThree years later and it's still very helpful. Thanks! :D
ReplyDeleteFour years later and it's still very helpful. Thanks a lot ;D
ReplyDeleteFive years later and it's still very helpful. Thank you! :D
ReplyDeleteSeven years later and it's still very helpful. Thanks! :D
ReplyDelete