DI impact on OO
Dependency injection impairs object-orientation?! Isn't dependency injection considered as an object-orientated design pattern? Yes, indeed! It is considered as a creational pattern, related to the SOLID principles, widely used as a part of the architecture in todays large business applications, implemented by famous frameworks, and so on. So the title of this article sounds somewhat provocative - but that's on purpose. This article will point out why dependency injection has been widely misused in todays large business applications and why that impacts on the object-oriented design of such an application with extensive consequences including increased costs for implementations and maintenance.
Let's say a given client class depends on a given service class. This is because the client class is implemented to call operations on that service class. Thus a client instance must obtain a service instance before the client instance calls those service operations.
In order to obtain a service instance, in many if not most large business applications, they use a dependency injection framework and do something like the following:
- Declaration of the service in the frameworks configuration (class, lifecycle, etc.). Depending on the framework this declaration can be done by plain code, annotations or in separate configuration files.
- Declaration of the client in the frameworks configuration (class, lifecycle, etc.) the same way as done for the service.
- Declaration of the injection / injector (and the type of injection like constructor injection, setter injection or interface injection) to inject the service instance into the client instance. This is done the same way as the declarations above and possibly inside the declaration of the client.
- It might be required to adapt the service class to allow the dependency injection framework to create new instances by the default constructor.
- It might be required to adapt the client class as well to allow the dependency injection framework to create new instances by the default constructor.
- It might be required to adapt the service class to implement an additional interface for dependency injections (interface injection).
- It might be required to adapt the client class as well to implement an additional interface for dependency injections (interface injection).
- It might be required to add a field to the client class to keep the service instance at runtime.
- It might be required to add a setter to the client class (setter injection) to receive the service instance at runtime.
Please note: for the purposes of this article this list of "things to do" shall just remind on how dependency injection works usually, but no code samples are provided here, since the conclusions of this article do not depend on any concrete framework and it is assumed that the reader is familiar with dependency injection in practice.
At application startup time the dependency injection framework will create a service instance and a client instance and inject the service instance into the client instance or inject the client instance into the service instance which then injects itself into the client instance.
This way the coupling between client and service is decreased and we can exchange the service implementation, e.g. with a mock, without any changes to the client.
This procedure is circuitous, but works well for the purposes mentioned above, so what is the issue with object-oriented design about?
The root cause is the underlying dependency injection pattern itself!
There is a lot to say about all these things to do listed above, which we have to do just to replace the keyword "new" with an externalized object creation. Certainly there is no need to argue why an externalized object creation is mandatory for any serious business application with some implemented behavior. And certainly it is obvious why it makes sense to organize this infrastructure code for externalized object creations in a reusable framework. But many developers criticize dependency injection containers for these things to do, e.g. they feel its too cumbersome or too much indirection or the encapsulation is violated by setter injection or the entire dependency graph might be loaded into the heap upfront, etc.
While all these concerns and criticism might be valid, this article is not going to discuss them. Much of it depends just on the framework in use and not on the dependency injection pattern itself. Even if encapsulation is violated by setter injection, there is still the possibility to use constructor injection instead. The point of this article is an impact on object-orientation by much lower lying reasons. The root cause is the underlying dependency injection pattern itself!