Introduction to Design Patterns and Dependency Injection

Profile picture Antoine Veuiller HackerNoon

Antoine Veiller

Software engineer specialized in Backend and DevOps.

Dependency Injection (DI) is a very simple concept that aims to decouple your software components and make them easier to integrate and test. It does this by requesting their subcomponents instead of creating them.

During this article, we will also mention inversion of control (IoC), which is commonly used with dependency injection. This model aims to avoid asking for implementations but rather for interfaces while injecting dependencies.

This article will use a simple example in Java to introduce dependency injection, but aims for a technology-agnostic explanation of the concept and its benefits. Also, even though it’s an object-oriented design pattern, you can still adapt the behavior in many programming languages.

We’re going to introduce a weather service that displays an intelligible representation of the weather. In the current implementation, we only rely on a thermometer.

Let’s start without dependency injection.

picture

As you can see in the diagram, the Weather Service is based on a Thermometerwhich can be configured with a Temperature unit. Not using dependency injection will result in code creating a new instance of Thermometer in the service, and a Thermometer configuring the Temperature unit utilize:

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 imagine that we want to use a Thermometer configured to use degrees Fahrenheit instead of Celsius. For this we add a parameter to switch between the two units.

public Thermometer(boolean useCelsius) {
  if (useCelsius) {
    this.unit = TemperatureUnit.CELSIUS;
  } else {
    this.unit = TemperatureUnit.FAHRENHEIT;
  }
}

It can also be argued that the user of our program will not always have access to an actual thermometer on their device, so you may want to be able to fall back on another implementation in this case. For example, an API sending the current temperature in your area. Integrating multiple implementations into the service can 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);
  }
}

Therefore, service initialization can be performed as follows:

public static void main(String[] args) {
  // Not using dependency injection
  WeatherContract weather = new WeatherService(true, true, null);
}

Although easy to use, our current version of the Weather Service is not scalable. If we take a closer look at its builder, we can see multiple design flaws that will haunt us in the long run:

  • The manufacturer chooses his Thermometer. Adding a new type of thermometer would require some parameterization tricks to guess which implementation to use.
  • The builder manages Thermometer builder settings. Added the ThermometerWebService forced us to add a new API key parameter, even if it is not related to the Weather Service.

Accordingly, any change to any Thermometer Implementation may require changes to the Weather Service builders. This behavior is undesirable and breaks the Separation of concerns principle.

Will dependency injection improve my project?

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

picture

the inversion of control is represented on this diagram by the fact that our Weather Service implementation is linked to ThermometerContract rather than one of its implementations. It’s nothing more than that.

As for dependency injection, Weather Service will now take a ThermometerContract in its constructor, causing the block using the service to construct an instance fulfilling 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;
  }
}

Therefore, initializing a Weather Service for both constructors will look like this:

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 importantly, therefore, the Weather Service does not need to know about available implementations of ThermometerContractthus decoupling your software packages.

It may not seem like a big deal, but this simple shift in responsibility is a critical lever for multiple aspects of software design. It allows you to control instance creation from your software entry point by chaining dependencies. You won’t have to deal with instantiation until it’s needed. This behavior could be compared to throwing exceptions, which are ignored until they are handled in a meaningful context.

It’s important to know that while you can find libraries that help you manage your dependency injection, it’s not always necessary to use them.

These libraries tend to cover many cases, which discourages developers who aren’t comfortable with the model in the first place. In reality, they just make it easier to instantiate complex dependency trees and aren’t necessary at all.

The following section is an example of injecting our service using Guice, a dependency injection framework for Java created by Google. The concept is to reference the bindings of each component that you can inject into your program, so that the library can automatically generate a class of any type.

Consider that we have two implementations with the following constructors:

public class WeatherService implements WeatherContract {
  private final ThermometerContract thermometer;

  @Inject
  public WeatherService(ThermometerContract thermometer) {
    this.thermometer = thermometer;
  }
}

public class Thermometer implements ThermometerContract {
  private final TemperatureUnit unit;
  
  @Inject
  public Thermometer(@Named(WeatherModule.TEMPERATURE_UNIT) 
                     TemperatureUnit unit) {
    this.unit = unit;
  }
}

the injection module must be configured to bind all necessary interfaces for a given implementation. It should also be able to inject any object without a specific interface, like enumeration Temperature unit. The injection will then be linked to a specific name, “temp_unit” in this case.

public class WeatherModule extends AbstractModule {
  public static final String TEMPERATURE_UNIT = "temp_unit";

  @Override
  protected void configure() {
    // Named input configuration bindings
    bind(TemperatureUnit.class)
      .annotatedWith(Names.named(TEMPERATURE_UNIT))
      .toInstance(TemperatureUnit.CELSIUS);
      
    // Interface - Implementation bindings
    bind(ThermometerContract.class).to(Thermometer.class);
    bind(WeatherContract.class).to(WeatherService.class);
  }
}

In the end, the module can be used as follows, here instantiating a WeatherContract.

public static void main(String[] args) {
  // Creating the injection module configured above.
  Injector injector = Guice.createInjector(new WeatherModule());

  // We ask for the injection of a WeatherContract, 
  // which will create an instance of ThermometerContract
  // with the named TemperatureUnit under the hood.
  WeatherContract weather = injector.getInstance(WeatherContract.class);
}

Such modules generally offer good customization power to the injected elements, so one can consider configuring the injection according to the available implementations.

Therefore, the use of a library is not necessary when integrating dependency injection. However, it could save a lot of time and tedious code in large projects.

A side effect of decoupling your code, the dependency injection pattern is a real asset for improving the unit testability of each component. This section contains an example of unit tests for our Weather Service.

As said above, do Weather Service ask for a ThermometerContract allows us to use any implementation we want. So we can send a fake in the constructor, then control its behavior from the outside.

public void testTemperatureStatus() {
  ThermometerContract thermometer = Mockito.mock(ThermometerContract.class);
  Mockito.doReturn(TemperatureUnit.CELSIUS).when(thermometer).getUnit();
  WeatherContract weather = new WeatherService(thermometer);
  
  Mockito.doReturn(-50f).when(thermometer).getTemperature();
  assertEquals(
    TemperatureStatus.COLD,
    weather.getTemperatureStatus()
  );
  
  Mockito.doReturn(10f).when(thermometer).getTemperature();
  assertEquals(
    TemperatureStatus.MODERATE,
    weather.getTemperatureStatus()
  );
}

As you can see, we can then control our thermometer without difficulty from outside our tested class.

Dependency Injection is a way to think about your code architecture and can be simple to implement on your own. In larger projects, integrating a dependency injection framework can save you a lot of time in the long run.

Dependency Injection offers many significant advantages such as:

  • Code decoupling: use contracts and ignore implementation specifics.
  • Improved testability: Unit tests become almost a pleasure to write.
  • Configurability: You can more easily swap the injected instances.

You can find the full sample code in my design tutorials repository on GitHub.

This article was first seen on: https://aveuiller.github.io/about_design_patterns-dependency_injection.html

Keywords

Related stories

Abdul J. Gaspar