I’ve struggled with explaining Service Locator as an anti-pattern. I’ve addressed certain cons of service locator and pros of constructor injection. However, I think I overlooked a fundamental misconception that would reasonably push developers away from constructor injection: a belief that constructor injection exposes dependency chains across the system.
Let’s dig into that misconception with a concrete example.
Suppose we have a component
The misconception is that some consumer of MidService would need to know about and provide Dependency1 and Dependency2.
If this were true, it would make for a fragile and nasty code base. Higher level services would need to know about a compounding list of dependencies. The high level services would be fragile due to necessary change if any lower level dependency is modified. Our whole system would be coupled to our dependency chains.
It’s understandable a developer would prefer service locator if this is the consequence they expect from constructor injection. Fortunately, this isn’t true.
Misconception Debunked by Example
Constructor injection is truly a form of dependency injection. It expects a component to define it’s direct dependencies. Instances of those dependencies are then provided ready-for-use. The consuming component has no knowledge of dependency configuration or construction, including any dependencies of it’s dependencies.
Consider the following dependency chain. This sample is also available to clone and run if you want to experiment.
Each layer only knows about it’s direct dependencies. For example,
TopLevel knows it needs
Mid2. However, it does not know that
Mid2 have dependencies of their own. It knows nothing of
A running system can then be composed in the top level of the application. This is called the composition root. Such a root usually only needs defined once per application, but how the root is referenced depends on the application model.
Like Service Locator, each component knows only about it’s direct dependencies.
Unlike Service Locator, each component makes it’s dependencies clear through the constructor. This means that we can flexibly use components in different contexts. We can compose them into a system with a composition root, or we can use subsets of the dependency tree on their own for other applications. Diverse consumers know what they must provide for a working component without looking at a component’s code.
Constructor injection does not require consumers to know about 2nd+ order dependencies and couple our application to it’s dependency chain. Each component only knows about it’s direct dependencies and should expect to receive ready-made instances. Thus, constructor injection provides the same about of information hiding between components as service locator with the added benefit of clear component requirements (via the constructor) across any context.