/ DEPENDENCY INJECTION, INVERSION OF CONTROL

Back to basics: Dependency Injection

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:

simple design

However, we have learned that one shall program by interface:

interface design

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:

true design

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
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.

Nicolas Fränkel

Nicolas Fränkel

Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Also double as a trainer and triples as a book author.

Read More
Back to basics: Dependency Injection
Share this