After years of near-ubiquitous usage of Dependency Injection, I see more and more posts and talks questioning its value. Some even go to the point where they argue against it. Most of it however is based on a whole lot of misconceptions, half-truths and blatant lies.
In this post, I’d like to go back to the roots of DI, describe some related features and lists available frameworks.
Explain it like I’m 5
Imagine a very basic class design.
A class Car
depends on class CarEngine
:
However, we have learned that one shall program by interface:
The implementation could look like the following:
interface Engine {
fun start(): Boolean
}
class CarEngine: Engine {
override fun start() = ...
}
class Car {
fun start() {
val engine = CarEngine()
when (engine.start()) {
true -> proceed()
false -> throw NotStartedException()
}
...
}
However, the true class diagram now looks like this:
In order to test class Car
in isolation, it’s not enough to introduce the Engine
interface.
The Car
code should also be prevented to instantiate a new CarEngine
instance:
class Car(private val engine: Engine) {
fun start() {
when (engine.start()) {
true -> proceed()
false -> throw EngineNotStartedException()
}
}
...
}
With this design, it’s possible to create different Car
instances, depending on the context:
val car = Car(CarEngine())
val mockEngine = mock(CarEngine::class.java)
val unitTestableCar = Car(mockEngine)
The concept of Dependency Injection is to move the object instantiation from inside the method to outside, and pass it to the later: that’s all!
I guess there are hardly any argument against DI itself. Then how comes the trend against DI is trending?
My wild guess is that is, as often, arises from a lack of knowledge of the different DI frameworks.
Typology
DI frameworks can be grouped according to different axes. Here are some of them.
Runtime vs compile-time
Most readers of this blog are familiar with DI containers. At startup, it takes care of injecting dependencies.
Because injecting at runtime increases the time required to start the application, it’s not suitable for all kind of applications: those that are started many times, and run for short period. In that case, it’s more relevant to inject dependencies at compile time.
Such is the case of Android apps.
Constructor vs setter vs field injection
The design above shows constructor-based injection: the dependency is injected in the constructor.
This is not the only way, though. Alternatives include:
- Setter-based injection
-
class Car(var engine: Engine)
This approach is not a great idea, as there’s no reason why the dependency should change during the injected object lifecycle.
- Field-based injection
-
class Car { @Inject private lateinit var foo: Engine }
This way is even worse, because it requires not only reflection, but also bypasses security checks.
Those above approaches have no benefits. While some DI frameworks - as well as some testing frameworks, allow those, they should be avoided at all costs.
Explicit vs implicit wiring
Some frameworks allow implicit dependency injection, also called autowiring. In order to satisfy a dependency, such frameworks will search in the context for a matching candidate. And will fail if they find none - or more than one.
Other frameworks allow explicit dependency injection: in that case, the developer needs to configure injection by binding the relationship between injected object and dependency.
Configuration options
Each framework allows one or more configuration approach.
Let’s first talk about the elephant in the room. The Spring framework is so ubiquitous that I’ve seen it used interchangeably with DI. This is absolutely not the case! As was just shown in the above section, DI doesn’t require any framework. And there are more DI frameworks than just Spring, even if the latter has a huge share of the DI pie on the server. |
The Spring framework allows the highest number of different configuration options:
- XML
- Self-annotated classes
- Java configuration classes
- Groovy
- Kotlin, through the Bean definition DSL
Here’s an example using it (copied from the Spring blog):
beans {
bean<UserHandler>()
bean<Routes>()
bean<WebHandler>("webHandler") {
RouterFunctions.toWebHandler(
ref<Routes>().router(),
HandlerStrategies.builder().viewResolver(ref()).build()
)
}
bean("messageSource") {
ReloadableResourceBundleMessageSource().apply {
setBasename("messages")
setDefaultEncoding("UTF-8")
}
}
bean {
val prefix = "classpath:/templates/"
val suffix = ".mustache"
val loader = MustacheResourceTemplateLoader(prefix, suffix)
MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply {
setPrefix(prefix)
setSuffix(suffix)
}
}
profile("production") {
bean<Foo>()
}
}
While DI cannot be limited to the Spring framework, the latter can also not be reduced to the former! It builds upon DI to offer a whole set of capabilities, via reusable components. |
Summary
Here’s a quick summary of frameworks and their features according to the above criteria:
- Spring framework
-
- De facto standard in the Java server space
- Runtime
- Constructor, setter and field injection
- See above for configuration options
- Explicit wiring and autowiring
- Context and Dependency Injection
-
- Part of the Java EE specification
- Runtime
- Constructor, setter and field injection
- Self-annotated classes only
- Explicit wiring and autowiring, with a taste for the latter
- Google Guice
-
- Runtime
- Constructor, setter and field injection
- Self-annotated classes only
- Autowiring only
- PicoContainer
-
- Lightweight, as its name implies
I have no experience with it, nor saw it used previously
- Lightweight, as its name implies
- Dagger 2
-
- De facto standard on Android
- Compile-time
- Constructor, field and method injection, with an inclination toward the first 2
- Combination of self-annotated classes and external classes
- Autowiring
Conclusion
So, why use DI? The question should be: why not use DI? It makes your code more modular and more component-oriented, leading to easier evolutive maintenance as well as better testability.
In this post, I tried to describes a set of available options. One can code the injection, or configure a framework to do so. One can select a runtime, or a compile-time DI framework. One can choose a lightweight container, or a full-fledged one with a complete set of additional features. There’s for sure one relevant to your context.