|
From Dependency Injection, Principles, Practices, and Patterns by Steven van Deursen and Mark Seemann This articles explains the Service Locator anti-pattern: what it is, what effects it has on code, and why it’s a problem. |
Take 37% off Dependency Injection, Principles, Practices, and Patterns. Just enter code fccseemann into the discount code box at checkout at manning.com.
It can be difficult to give up on the idea of directly controlling Dependencies, and many developers take Static Factories to new levels. This leads to the Service Locator anti-pattern.
Definition |
A Service Locator supplies application components outside the Composition Root with access to an unbounded set of Dependencies. |
As its most commonly implemented, the Service Locator is a Static Factory that can be configured with concrete services before the first consumer begins to use it. (But you’ll equally also find abstract Service Locators.) This could conceivably happen in the Composition Root. Depending on the particular implementation, the Service Locator can be configured with code by reading a configuration file or by using a combination thereof. The following listing shows the Service Locator anti-pattern in action.
Listing 1 Using the Service Locator anti-pattern (BAD CODE!)
public class HomeController : Controller { public HomeController() { } ❶ public ViewResult Index() { IProductService service = Locator.GetService<IProductService>(); ❷ var products = service.GetFeaturedProducts(); ❸ return this.View(products); } }
❶ HomeController
has a parameterless constructor.
❷ HomeController
requests an IProductService
instance from the static Locator
class.
❸ Uses the requested IProductService
, as usual.
Instead of statically defining the list of required Dependencies using Constructor Injection, HomeController
has a parameterless constructor, requesting its Dependencies later. This hides these Dependencies from HomeController
’s consumers and makes HomeController
harder to use and test. Let’s review an example that shows Service Locator in action.
Example: ProductService
using a service locator
Let’s look at a ProductService
that requires an instance of the IProductRepository
interface. Assuming we were to apply the Service Locator anti-pattern, ProductService
would use the static GetService
method, as shown in the following listing.
Listing 2 Using a Service Locator inside a constructor (BAD CODE!)
public class ProductService : IProductService { private readonly IProductRepository repository; public ProductService() { this.repository = Locator.GetService<IProductRepository>(); } public IEnumerable<DiscountedProduct> GetFeaturedProducts() { ... } }
In this example, we implement the GetService
method using generic type parameters to indicate the type of service being requested. You could also use a Type
argument to indicate the type, if that’s more to your liking.
Clients such as ProductService
can use the GetService
method to request an instance of the abstract type T
. The GetService
method can only return an instance of the requested type if it has previously been inserted in the internal dictionary.
Classes like ProductService
rely on the service to be available in the Service Locator, and it’s important that it’s previously configured. In a unit test, this could be done with a Test Double implemented by a Stub, as can be seen in the following listing.
Listing 4 A unit test depending on a Service Locator (BAD CODE!)
[Fact] public void GetFeaturedProductsWillReturnInstance() { // Arrange var stub = ProductRepositoryStub(); ❶ Locator.Reset(); ❷ Locator.Register<IProductRepository>(stub); ❸ var sut = new ProductService(); // Act var result = sut.GetFeaturedProducts(); ❹ // Assert Assert.NotNull(result); }
❶ Creates a Stub
for the IProductRepository
interface.
❷ Resets the Locator
to its default settings to prevent previous tests from influencing this test.
❸ Uses the static Register
method to configure the Service Locator with the Stub
instance.
❹ Executes the required task for the test at hand; GetFeaturedProducts
will now use ProductRepositoryStub
.
The example shows how the static Register
method is used to configure the Service Locator with the Stub instance. If this is done before ProductService
is constructed, as shown in the example, ProductService
uses the configured Stub to work against ProductRepository
. In the full production application, the Service Locator is configured with the correct ProductRepository
implementation in the Composition Root.
This way of locating Dependencies from the ProductService
class works if our only success criterion is the Dependency used and replaced at will. But it has some serious shortcomings.
Service Locator is a dangerous pattern because it almost works. You can locate Dependencies from consuming classes, and you can replace those Dependencies with different implementations — even with Test Doubles from unit tests. There’s only one area where Service Locator falls short, and that shouldn’t be taken lightly.
The main problem with Service Locator’s the impact of reusability of the classes consuming it. This manifests itself in two ways:
- The class drags along the Service Locator as a redundant Dependency.
- The class makes it non-obvious what its Dependencies are.
Let’s first look at the Dependency graph for the ProductService
from the example earlier in the article, shown in figure 2.
Figure 2 Dependency graph of ProductService
using the service locator anti-pattern
In addition to the expected reference to IProductRepository
, ProductService
also depends on the Locator
class. This means that to reuse the ProductService
class, you must redistribute not only it and its relevant Dependency IProductRepository
, but also the Locator
Dependency, which only exists for mechanical reasons. If the Locator
class is defined in a different module than ProductService
and IProductRepository
, new applications wanting to reuse ProductService
must accept that module too. Perhaps we could even tolerate that extra Dependency on Locator
if it was truly necessary. We’d accept it as a tax to be paid to gain other benefits. But there are better options (such as Constructor Injection) available, so this Dependency is redundant. Moreover, neither this redundant Dependency nor IProductRepository
, its relevant counterpart, is explicitly visible to developers wanting to consume the ProductService
class.
If you want to create a new instance of the ProductService
class, your IDE can only tell you that the class has a parameterless constructor. But if you subsequently attempt to run the code, you get a runtime error if you forgot to register an IProductRepository
instance with the Locator
class. This is likely to happen if you don’t intimately know the ProductService
class.
The problem with Service Locator is that any component using it is being dishonest about its level of complexity. It looks simple as seen through the public API, but it turns out to be complex — and you won’t find out until you try to run it.
The ProductService
class is far from self-documenting: you can’t tell which Dependencies must be present before it’ll work. In fact, the developers of ProductService
may even decide to add more Dependencies in future versions. That would mean that code that works for the current version can fail in a future version, and you aren’t going to get a compiler error that warns you. Service Locator makes it easy to inadvertently introduce breaking changes.
When unit testing, you have the additional problem that a Test Double registered in one test case will lead to the Interdependent Tests code smell, because it remains in memory when the next test case is executed. It’s therefore necessary to perform Fixture Teardown after every test by invoking Locator.Reset()
.This is something that you must manually remember to do, and it’s easy to forget.
A Service Locator may seem innocuous, but it can lead to all sorts of nasty runtime errors. How do you avoid those problems? The default approach should be to apply Constructor Injection, unless another DI pattern provides a better fit.
That’s all for this article. If you want to learn more about the Service Locator anti-pattern, check it out on liveBook here.
You can also see other articles on common DI-related topics:
Writing Maintainable, Loosely-Coupled Code
Understanding Property Injection
Understanding Method Injection
Understanding the Composition Root
Understanding Constructor Injection
Abuse of Abstract Factories
The Ambient Context Anti-Pattern
The Transient Lifestyle
The Scoped Lifestyle
The Singleton Lifestyle