Decouple Your Code With Dependency Injection

 · 8 min
Icons8 Team on Unsplash

Not many components live on their own, without any dependencies on others. Instead of creating tightly coupled components, we can improve the separation of concerns by utilizing dependency injection (DI).

This article will introduce you to the core concept of dependency injection, without the need for third-party frameworks. All code examples will be in Java, but the general principles apply to any other language, too.


Example: DataProcessor

To better visualize how to use dependency injection, we start with a simple type:

java
public class DataProcessor {

    private final DbManager manager = new SqliteDbManager("db.sqlite");
    private final Calculator calculator = new HighPrecisionCalculator(5);

    public void processData() {
        this.manager.processData();
    }

    public BigDecimal calc(BigDecimal input) {
        return this.calculator.expensiveCalculation(input);
    }
}

The DataProcessor has two dependencies: DbManager and Calculator.

Creating them directly in our type has several apparent disadvantages:

  • The constructor calls can crash.
  • Constructor signatures might change.
  • Tightly bound to explicit implementation type.

It’s time to improve it!


Dependency Injection

James Shore, the author of The Art of Agile Development, put it quite nicely:

“Dependency injection is a 25-dollar term for a 5-cent concept.”

The concept is actually really simple: Giving a component all the things it needs to do its job.

In general, it means decoupling components by providing their dependencies from the outside, instead of creating them directly, which would create adhesion.

There are different ways how we can provide an instance with its necessary dependencies:

  • Constructor injection
  • Property injection
  • Method injection

Constructor injection

Constructor, or initializer-based dependency injection, means providing all required dependencies during the initialization of an instance, as constructor arguments:

java
public class DataProcessor {

    private final DbManager manager;
    private final Calculator calculator;

    public DataProcessor(DbManager manager, Calculator calculator) {
        this.manager = manager;
        this.calculator = calculator;
    }

    // ...
}

Thanks to this simple change, we can offset most of the initial disadvantages:

  • Easily replaceable: DbManager and Calculator are no longer bound to the concrete implementations, and are now mockable for unit-testing.

  • Already initialized and “ready-to-go”: We don’t need to worry about any sub-dependencies required by our dependencies (e.g., database filename, significant digits), or that they might crash during initialization.

  • Mandatory requirements: The caller knows exactly what’s needed to create a DataProcessor.

  • Immutability: Dependencies are still final.

Even though constructor injection is the preferred way of many DI frameworks, it has its obvious disadvantages, too. The most significant one is that all dependencies must be provided at initialization.

Sometimes, we don’t initialize a component ourselves or we aren’t able to provide all dependencies at that point. Or we need to use another constructor. And once the dependencies are set, they can’t be changed.

But we can mitigate these problems by using one of the other injection types.

Property injection

Sometimes we don’t have access to the actual initialization of type, and only have an already initialized instance. Or the needed dependency is not explicitly known at initialization as it would be later on.

In these cases, instead of relying on a constructor, we can use property injection:

java
public class DataProcessor {

    // Either public fields, or getter/setter ith private fields
    public DbManager manager = null;
    public Calculator calculator = null;

    // ...

    public void processData() {
        // WARNING: Possible NPE
        this.manager.processData();
    }

    public BigDecimal calc(BigDecimal input) {
        // WARNING: Possible NPE
        return this.calculator.expensiveCalculation(input);
    }
}

No constructor is needed anymore, we can provide the dependencies at any time after initialization. But this way of injection also comes with drawbacks: Mutability.

Our DataProcessor is no longer guaranteed to be “ready-to-go” after initialization. Being able to change the dependencies at will might give us more flexibility, but also the disadvantage of more runtime-checks.

We now have to deal with the possibility of a NullPointerException when accessing the dependencies.

Method injection

Even though we decoupled the dependencies with constructor injection and/or property injection, by doing so, we still only have a single choice. What if we need another Calculator in some situations?

We don’t want to add additional properties or constructor arguments for a second Calculator, because there might be a third one needed in the future. And changing the property every time before we call calc(...) isn’t feasible either, and will most likely lead to bugs using the wrong one.

A better way is to parameterize the method call itself with its dependency:

java
public class DataProcessor {

    // ...

    public BigDecimal calc(Calculator calculator, BigDecimal input) {
        return calculator.expensiveCalculation(input);
    }
}

Now the caller of calc(...) is responsible for providing an appropriate Calculator instance, and DataProcessor is completely decoupled from it.

Even more flexibility can be gained by mixing different types of injection, and providing a default Calculator:

java
public class DataProcessor {

    // ...

    private final Calculator defaultCalculator;
    
    public DataProcessor(Calculator calculator) {
        this.defaultCalculator = calculator;
    }

    // ...

    public BigDecimal calc(Calculator calculator, BigDecimal input) {
        return Optional.ofNullable(calculator)
                       .orElse(this.calculator)
                       .expensiveCalculation(input);
    }
}

The caller could provide a different kind of Calculator, but it doesn’t have to. We still have a decoupled, ready-to-go DataProcessor, with the ability to adapt to specific scenarios.


Which Injection Type to Choose?

Every type of dependency injection has its own merits, and there isn’t a “right way”. It all depends on your actual requirements and the circumstances.

Constructor injection

Constructor injection is my favorite, and often preferred by DI frameworks.

It clearly tells us all the dependencies needed to create a specific component, and that they are not optional. Those dependencies should be required throughout the component.

Property injection

Property injection matches better for optional parameters, like listeners or delegates. Or if we can’t provide the dependencies at initialization time.

Some other languages, like Swift, make heavy use of the delegation pattern with properties. So, using it will make our code more familiar to other developers.

Method injection

Method injection is a perfect match if the dependency might not be the same for every call. It will decouple the component even more because now, just the method itself has a dependency, not the whole component.

Remember that it’s not either-or. We can freely mix the different types where it’s appropriate.


Inversion of Control Containers

We can cover a lot of use cases with these simple implementations of dependency injection. It’s an excellent tool for decoupling, but we actually still need to create the dependencies at some point.

But as our applications and codebases grow, we might need a more complete solution that simplifies the creation and assembling process, too.

Inversion of control (IoC) is an abstract principle of the flow of control. And dependency injection is one of its more concrete implementations.

An IoC container is a special kind of object that knows how to instantiate and configure other objects, including doing the dependency injection for you.

Some containers can detect relationships via reflection, others have to be configured manually. Some are runtime-based, others generate all the code needed at compile-time.

Comparing all the different options is beyond the scope of this article, but let’s check out a small example to get a better understanding of the concept.

Example: Dagger 2

Dagger is a lightweight, compile-time dependency injection framework. We need to create a Module that knows how to build our dependencies, that later can be injected by merely adding an @Inject annotation:

java
@Module
public class InjectionModule {

    @Provides
    @Singleton
    static DbManager provideManager() {
        return manager;
    }

    @Provides
    @Singleton
    static Calculator provideCalculator() {
        return new HighPrecisionCalculator(5);
    }
}

The @Singleton ensures that only one instance of a dependency will be created.

To get injected with a dependency, we simply add @Inject to a constructor, field, or method:

java
public class DataProcessor {

    @Inject
    DbManager manager;
    
    @Inject
    Calculator calculator;

    // ...
}

These are just the absolute basics, and might not seem impressive at first. But IoC containers and frameworks allow us to not just decouple our component, but also to maximize the flexibility of dependency creation.

The creation process becomes more configurable and enables new ways of using the dependencies, thanks to the advanced features provided.

Advanced features

The features vary widely between the different kinds of IoC containers and the underlying languages, like:

  • Proxy pattern and lazy-loading.
  • Lifecycle scopes (e.g., singleton vs. one per thread).
  • Auto-wiring.
  • Multiple implementations for a single type.
  • Circular dependencies.

These features are the real power of IoC containers. You might think features like “circular dependencies” are not a good idea. And you’re right.

But if we actually need such weird code constructs due to legacy code, or unchangeable bad design decisions in the past, we now have the power to do so.


Conclusion

We should design our code against abstractions, like interfaces, and not concrete implementations, to reduce adhesion.

The only information our code should need must be available in the interface, we can’t assume anything about the actual implementation.

“One should depend upon abstractions, [not] concretions.”
- Robert C. Martin (2000), Design Principles and Design Patterns

Dependency injection is a great way to do that by decoupling our components. It allows us to write cleaner and more concise code that’s easier to maintain and refactor.

Which of the three dependency injection types to choose depends much on the circumstances and requirements, but we can also mix the types to maximize the benefit.

IoC containers can provide another layout of convenience by simplifying the component creation process, sometimes in an almost magical way.

Should we use it everywhere? Of course not.

Just like other patterns and concepts, we should apply them if appropriate, not only because we can.

Never restrict yourself to a single way of doing things. Maybe the factory pattern or even the widely-loathed singleton pattern might be a better solution for your requirements.


Resources

IoC Containers

Java

Kotlin

Swift

C#