Let’s clarify all this using an example!

We will present a weather service that shows an intelligible representation of the weather. In the current implementation, we rely solely on a thermometer.

Let’s start without dependency injection.

As you can see on the diagram, the WeatherService is relying on a Thermometer, which can be configured with a TemperatureUnit. Not using dependency injection will result in a code creating a new instance of Thermometer in the service, and a Thermometer configuring the TemperatureUnit to use:

public class Thermometer {

private final TemperatureUnit unit; public Thermometer() {

this.unit = TemperatureUnit.CELSIUS;

}

} public class WeatherService implements WeatherContract {

private final Thermometer thermometer; // This constructor is not using dependency injection

public WeatherService() {

this.thermometer = new Thermometer();

}

}

Now let’s imagine that we want to use a Thermometer configured to use Fahrenheit degrees instead of Celsius. For this, we add a parameter to switch between both units.

public Thermometer(boolean useCelsius) {

if (useCelsius) {

this.unit = TemperatureUnit.CELSIUS;

} else {

this.unit = TemperatureUnit.FAHRENHEIT;

}

}

One can also argue that the user of our program won’t always have access to an actual thermometer on their device, thus you may want to be able to fall back to another implementation in this case. For instance, an API sending the current temperature in your area. Integrating multiple implementations inside the service could be done as shown below.

public WeatherService(boolean useRealDevice,

boolean useCelsius,

String apiKey) {

if (useRealDevice) {

this.thermometer = new Thermometer(useCelsius);

} else {

this.thermometer = new ThermometerWebService(useCelsius,

apiKey);

}

}

As a result, initializing the service can be done as follows:

public static void main(String[] args) {

// Not using dependency injection

WeatherContract weather = new WeatherService(true, true, null);

}

Even if it is easy to use, our current version of the WeatherService is not evolutive. If we take a closer look at its constructor, we can see multiple design flaws that will haunt us in the long run:

The constructor is choosing its Thermometer. Adding a new type of Thermometer would require some parameter tricks to guess the implementation to use.

The constructor is managing the Thermometer constructor parameters. Adding the ThermometerWebService forced us to add a new apiKey parameter to it, even if unrelated to the WeatherService.

As a result, any change to any Thermometer implementation may require changes on the WeatherService constructors. This behaviour is unwanted and breaks the Separation of Concerns principle.

Will dependency injection improve my project?

Dependency injection, associated with inversion of control, is a good way to cover this use case. It allows you to choose which kind of thermometer you want in your program depending on the situation. The following diagram gives a quick overview of our new architecture:

The inversion of control is represented in this diagram by the fact that our WeatherService implementation is linked to ThermometerContract rather than any of its implementations. That’s nothing more than this.

As for dependency injection, WeatherService will now take a ThermometerContract in its constructor, requiring the block using the service to build an instance filling this contract:

public class WeatherService implements WeatherContract {

// We now use the Interface

private final ThermometerContract thermometer; // New constructor using dependency injection

public WeatherService(ThermometerContract thermometer) {

this.thermometer = thermometer;

}

}

As a result, the initialization of a WeatherService for both constructors will look like the following:

public static void main(String[] args) {

// Using dependency injection

TemperatureUnit celsius = TemperatureUnit.CELSIUS;

ThermometerContract thermometer = new Thermometer(celsius);

WeatherContract weather = new WeatherService(thermometer);

}

Now, our ThermometerContract can be fully configured by an external part of the software. More important so, the WeatherService doesn’t need to know any of the available implementations of ThermometerContract, thus decoupling your software packages.

This could seem like nothing important, but this simple switch of responsibility is critical leverage for multiple aspects of software design. It enables you to control the instance creation from your software entry point by chaining dependencies. You won’t have to take care of the instantiation until it is necessary. This behaviour could be compared to raised exceptions, that are ignored until taken care of in a significant context.