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!
[...] objects are composed of cohesive data and the behavior that uses these data [...]
Flashback: What is object-oriented and what for it has been introduced?
In brief: We have to distinguish between object-oriented programming and object-based programming. While objects in the latter are simply units which may have an identity and attributes / properties, as known from records in procedural programming, the object-oriented concept of an object includes additionally polymorphism, inheritance and information hiding / encapsulation.
Therefore, a unit without polymorphism, inheritance or information hiding / encapsulation can be an object technically (actually a record in object-based programming), but cannot be an object conceptually (in object-oriented programming), which is normally meant to represent a real world object with a state and behavior. Hence, in object-oriented programming objects are composed of cohesive data and the behavior that uses these data as well.
Please note: The use of a programming language that supports object-oriented programming does not ensure that a programming is actually object-oriented.
The conceptual object with the integration of state and behavior in the same unit, has been introduced to allow developers to change the implementation of a unit without having to change other units (clients), as long as the units interface is not altered. Instead of a direct access to the units data, a client communicates with the unit by messages (operation calls, passing parameters and returning values). This may sound like a slight advantage, but it is an absolutely crucial advantage with a huge impact on an applications architecture. In a project with a limited budget (that means all projects), it makes a difference if they have to adapt just one unit or maybe thousends... A consequently applied information hiding / encapsulation gives the developer a powerful lever to trigger behavior and to alter the state of objects indirectly. Without that lever the developer will have to take care of every detail each time again. More reasons for information hiding / encapsulation could be enumerated, but this article is not about the advantages or disadvantages of information hiding / encapsulation. This article simply reminds of the fact that object-oriented programming requires information hiding / encapsulation by definition.
We do not inject domain data.
What has that to do with dependency injection?
Usually we inject services or the like, more generally spoken we inject stateless objects. Stateless means here the objects have no domain state (such as domain values), but may have a purely technical state (such as dependencies to other stateless objects). These stateless objects are a set of procedures or functions. To work on domain data, the stateless objects must receive the data as actual parameters for their procedures or functions. Therefore these stateless objects cannot represent real world objects, which have their own internal state, and hence cannot be a part of the domain model. The stateless objects aka services, managers, controllers, DAO or the like are procedural / functional code working on separate data structures. This is what we inject. We do not inject domain data.
To extend dependency injection to stateful objects (objects having a domain state) will not work well, since the dependency injection is performed at startup time where we cannot predict which objects will be needed, how many objects will be needed and with which state these objects will be needed by operations which use them later on. Even if the dependency injection would happen just in time when the dependency is needed, we still have to predict the number and the state of these objects beforehand (e.g. in the configuration of the dependency injection framework in use), since the injection necessarily happens before the receiving object can use the injected object. Due to this obviously necessary order, the dependency injection is based on static information about relationships between classes at compile time and is not based on dynamically determined information about relationships between objects at runtime. This could be fixed partially with some uggly extra efforts based on lazy loading proxies with individual factories etc., just to allow to pass some actual parameters to the container / injector, but still leaves us in the dark when it comes to the allocation of more instances dynamically inside a clients operation. In this context see also the approach of the "AssistedInject" in Google's Guice. Moreover, the object receiving a dependency injection has to keep the dependency as an instance variable in the heap instead of pushing it onto the stack just when it is really needed.
This is independent from the concrete dependency injection framework (with partial exceptions in Google's Guice) and a consequence of the dependency injection pattern itself.
To set the initial state into an already injected object is not an option at all. The injected object will be at least temporarily in an invalid state. And the next developer who works on that code might not know about that initial state passing...
Somebody might think, why not querying the dependency injection container just in time and same time passing the actual parameters? Indeed, dependency injection containers usually provide to pull an dependency from the container by the client itself. This approach could make sense possibly, but it is not dependency injection - dependency injection is injection! It means using a dependency injection container for something that is anything else, but not dependency injection. If we have to pull all dependencies, then what is the point in using that dependency injection container anyway? Moreover, to refer to a dependency, dependency injection frameworks typically force us to use plain Strings as parameters. Maybe not the best idea considering refactorings...
This throws away a bigger part of the benefits
Since we inject stateless objects only, these objects (services) do not comply with the concept of an object in object-oriented programming. This is not really an issue if the dependency injection is restricted to a few central services which implement e.g. a thin layer of stateless activity classes which orchestrate the functionality implemented in the domain model, implement transaction handling and maybe cross cutting functionality beyond the domain model.
But remember: we use dependency injection mainly because we want to be able to exchange implementations without having to adapt anything else. If we use dependency injection just for a few central services, then is it worth it? How do we exchange the implementations in our rich domain model? In order to be able to exchange all implementations we will have to move the behavior from the domain model to stateless services.
This means we separate data from behavior and leave the domain model as an (almost) pure data structure, an anemic domain model, which does not comply with the concepts of object-oriented programming. As a natural consequence, these stateless services will have to operate directly on the data of the domain model and therefore cannot avoid to chain getter calls and to nest iterations over and over. In object-oriented design this is a violation of the law of demeter at least.
And this is exactly what happens in so many business applications: an (almost) pure data structure aka anemic domain model on one side and stateless services with procedural code on the other side. This throws away a bigger part of the benefits provided by an object-oriented language, but runs at the cost of the overhead of that object-oriented language. Implementing procedural code is probably easier and faster with a procedural language.
While this may happen for a couple of reasons (including but not limited to the challenge to get used to the way of thinking a "new" paradigm requires) dependency injection plays its role here because it does not allow for an object-oriented design if used throughout the entire application.
Thus, for the purpose of exchanging implementations in a rich domain model another pattern must be choosen.
And again: If we use dependency injection just for a few central services, then is it worth it?
Of course I'm not the first one and certainly not last one writing about this topic, although it seems hard to find publications in this direction. Also, most people blame the dependency injection frameworks, but not the pattern itself. While writing this article I found just very few publications about this topic. (Please note, that I do not fully agree with everything written in the following publications.)
An interesting introduction to the topic can be found in the Blog of Adam Warski:
Dependency injection discourages object-oriented programming?
More to the point and more entertaining to read is an article from David Green:
Your DI framework is killing your code
See also this thread on stackexchange.com:
Does Inversion of Control promote Anemic Domain Model?
Knowing that dependency injection destroys object-oriented design, which are the alternatives?
Well, choosing the right creational pattern should depend on the applications functional and nonfunctional requirements, already given application frameworks etc.
There are some older patterns like abstract factory or service locator. These patterns had been subject to some solid criticism as well, what might be one reason why dependency injection became more famous. This article is not going to warm-up these discussions. Instead let's have a look at newer alternatives:
A matured project which still uses dependency injection could probably migrate quite easily to the factory injection pattern. For the factory injection pattern it is not required to abandon the dependency injection framework in use, the project can indirectly still exchange implementations by inversion of control on a higher level, but the client classes get back control over the dependencies itself. The latter means that factory injection is not dependency injection. To be able to manage rich domain model objects by the container, the actual parameters must be passed to the factory for the object creation. If the framework does not support this, then you cannot create a container managed object that requires parameters for its construction without violating the encapsulation (e.g. by setting the initial state inside the factory). This is not a restriction of the pattern but the framework. With that restriction the container could be used for a thin layer of activities which orchestrate the functionality implemented in the domain model and implement transaction handling and holders which hold the domain model. The domain model objects itself are then created directly by the factories (e.g. like in the abstract factory pattern) and not pulled from the container, but derive their lifecycle from the holders. The domain model could use stateless objects (without construction arguments) from the container which implement cross-cutting functionality. This way the factory injection pattern enables to integrate container managed objects with unmanaged objects allowing to exchange the implementations of both managed and unmanaged ones.
A new or younger project could choose an even better pattern: the creator pattern.
In the creator pattern universe there is no issue with object-oriented design. All kinds of objects are created in a very natural way under the full control by the client classes, but allowing for exchanging implementations, even dynamically by using contexts made of dynamic values or compositions. This makes it a very powerful creational pattern! Creator classes and factories are implemented or generated with full compiler support instead of writing proprietary configurations in a DSL. There's also a creator framework available for this pattern.
About the author