Description: dependency injection2e.jpg

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