In this tutorial, we’ll review two patterns: Dependency Injection and Service Locator. They solve the same problem differently and often use terminology that can be applied to both. The goal is to find out their essential differences and the pros and cons of each approach.
2. Working Example
Imagine we have a set of classes with dependencies. Let’s outline the initial layout:
The diagram shows only the dependencies between classes. However, it doesn’t explain how these classes are populated with dependencies. Let’s use a naive approach and instantiate needed dependencies inside each class directly:
Allowing the class to define its dependencies would break the Single Responsibility principle and make it rigid and fragile. Another problem is the new operator, which explicitly creates dependencies for concrete implementations, as seen in the diagram above. Thus, we have two problems: classes are responsible for defining their dependencies, and the dependency is on concrete implementations.
3. Service Locator
The main idea of the Service Locator is to create a registry containing all the dependencies and get components from it whenever we need them. Every object that needs something from this registry will interact with it rather than trying to instantiate a dependency itself. This way, we can remove all the dependencies to the concrete implementation from our components:
However, the object will still have a dependency, but only for the Service Locator itself, and it will provide us with the needed implementation transparently. Unfortunately, too transparent because it will hide all the dependencies from a client. Technically, it breaks encapsulation, as any interaction with an object will require looking inside components, especially true while testing.
Another problem is that the Service Locator defers compile-time errors to runtime. Using the Service Locator directly in methods creates an even more significant problem. Because the Service Locator registry is populated at runtime, it’s hard to tell if the needed classes will be there when we ask for them. The Dependency Injection container may introduce the same issue for the dynamic parts created later in the application lifecycle.
Also, the Service Locator is accused of breaking the Interface Segregation Principle. However, it’s arguable as the “registry” itself might be considered part of metaprogramming, and usual design principles should be applied with caution. Unlike the Service Locator, an Abstract Factory and a Factory state their dependencies more explicitly.
Additionally, people point out the problem with testability, but it’s not a real issue if the Service Locator is implemented to be flexible enough. In most cases, the issue arises if the Service Locator is implemented as a Singleton or doesn’t allow a simple way to configure the registry.
The Service Locator requires a dedicated place to populate the registry with components. This way, we can separate the creational logic from the classes that will use them. This separation is essential, especially if we have a complex dependency graph.
4. Dependency Injection
This is another approach for the same problem, which is quite intuitive and straightforward. If we don’t want to create the dependencies inside the class, let’s ask someone else to provide us with them. This approach creates a contract. The class, for its functioning, requires a certain number of dependencies passed to it:
The Dependency Injection resolves the two problems we discussed previously. However, we still have a dedicated class configuring and creating all the needed dependencies. Often the Dependency Injection is combined with a Dependency Injection Container, which, if not used correctly, can lead to all the problems discussed in the Service Locator section.
There are several ways to implement it: constructor and setter injection (there is also an interface injection, but it’s not widely used and more cumbersome.) Overall the approach with constructor injection is preferable. At the same time, setter injection has its benefits and can also be used. The main thing is to remember that setter injection may allow non-fully constructed objects and circular dependencies; thus, it should be used wisely.
5. Dependency Injection vs. Service Locator
One of the main differences between these approaches is their transparency about the dependencies they introduce. The Dependency Injection is explicit about everything used inside a class, and with constructor injection, it’s clearly shown in the class API. While the Service Locator hides everything inside, and it’s hard to tell which classes depend on which, the only thing is clear all of them depend on the Service Locator. Implementing the Service Locator with segregated interfaces is possible, but it might lead to an interface explosion. Although modern IDE can show the dependency graphs and build clear UML diagrams, the API does not know the dependencies.
Another point is that the Dependency Injection is less intrusive and allows reusability. The classes depend on other classes directly, allowing them to function correctly. With the Service Locator, the only way to reuse classes is to reuse them with the Service Locator itself; thus, they go in a pack. Even though both patterns rely on the classes which construct and configure application objects, the Dependency Injection allows better reusability and more straightforward testing.
Overall, Dependency Injection is the common way to achieve loose coupling and allow the reusing of the components. It’s pretty intuitive and allows us to declare dependencies more explicitly. The Service Locator can allow more flexibility and often too much. This extensive flexibility can break all the boundaries between components and encourage bad practices because all the components are available all the time, whenever we need them. The Service Locator can often be considered an anti-pattern, as it may introduce more problems and code smells than benefits.
The Service Locator and the Dependency Injection are patterns that try to resolve the same issues. However, they have quite different approaches, and each has its benefits and drawbacks. Although there are specific scenarios when the Service Locator would be more applicable in general, the Dependency Injection provides more flexibility and extensibility. Dependency Injection is the most common way to achieve loose coupling in an application.