Dependency Injection and Inversion of Control

Dependency Injection and Inversion of Control

Table Of Contents

Inversion of control (IoC) is simply providing a callback (reaction) to an event that might happen in a system. In other words, instead of executing some logic directly, we invert the control to that callback whenever a specific event occurs. This pattern allows us to separate what we want to do from when we want to do it with each part knowing as little as possible about the other, thus simplifying our design.

Example Code

This article is accompanied by a working code example on GitHub.

Use Cases for Inversion of Control

IoC offers us the ability to separate the concern of writing the code to take action from the concern of declaring when to take that action. This comes in handy when we are developing a complex system and we want to keep it clean and maintainable. Let’s take a look at some concrete usages.

Framework

A framework is the best example of IoC because we invert so much control into it. Let’s take the Spring framework for example. Instead of going through the trouble of writing code to configure and start a web server we just use the Spring @SpringBootApplication annotation that tells Spring to take control and start a web server.

@SpringBootApplication  
public class MyApplication {  
   public static void main(String[] args) {  
      SpringApplication.run(MyApplication.class, args);  
  } 
}

Spring also uses IoC to facilitate all related back-end development tasks, such as creating HTTP request handlers.

@GetMapping("/hello")  
public void createPost() {  
    // handle request  
}

The @GetMapping annotation is Spring’s IoC pattern to tell us not to worry about how to intercept the GET request to the endpoint /hello but worry about what to do with it.

Message Handling

Messaging systems are another good example of inversion of control where we subscribe to a certain message queue (topic) and then we simply write the code that handles what to do with that message. In other words, we invert the control of fetching the messages to the messaging system and ask it to handle the message.

Let’s look at an example using Kafka:

@KafkaListener(topics = "myTopic", groupId = "myGroup")  
public void consumeMessage(String message) {  
   System.out.println("Received Message in myGroup : " + message);  
}

Dependency Injection

Simply put, dependency injection (DI) is having a framework that provides a component with its dependencies, so you don’t have to construct the objects with all their dependencies yourself.

In this sense, dependency injection is a subtype of inversion of control because we invert the control of constructing objects with their dependencies to a framework.

Reasons to Use Dependency Injection

Using dependency injection has major benefits that make it a widely-used pattern. Let’s discuss two of them.

Simplifies Code Design

Using dependency injection allows a component not to worry about how to instantiate its dependencies, which might be quite complicated and might require method calls to other helper utilities. This way the component only asks for the dependency rather than creating it which makes the component itself smaller and simpler.

Let’s look at an example where we have a ShippingService that only sends a shipment after making some checks using REST calls and Database operations.

First, let’s do it without dependency injection, where we construct the RestTemplate and DataSource objects inside the ShippingService.

public class ShippingService {
  private RestTemplate restTemplate;
  private DataSource dataSource;

  public ShippingService() {
    RestTemplate restTemplate =
        new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofMillis(1000))
            .setReadTimeout(Duration.ofMillis(2000))
            .build();
    restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("http://payment-service-uri:8080"));

    this.restTemplate = restTemplate;

    DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
    dataSourceBuilder.driverClassName("org.h2.Driver");
    dataSourceBuilder.url("jdbc:h2:file:C:/temp/test");
    dataSourceBuilder.username("shipping-user");
    dataSourceBuilder.password("superSecretPassword");
    DataSource dataSource = dataSourceBuilder.build();

    this.dataSource = dataSource;
  }

  private boolean packageIsShippable(String id) {
    // business logic that make REST and database calls  
    return true;
  }

  public void ship(String shipmentId) {
    if (packageIsShippable(shipmentId)) {
      // ship the thing  
    }
  }
}

One thing that immediately catches our attention is the big amount of code we had to write without even starting with the ShippingServie core business logic.

Now let’s use dependency injection to create a simpler design.

public class ShippingService {
  private RestTemplate restTemplate;
  private DataSource dataSource;

  public ShippingService(RestTemplate restTemplate, DataSource dataSource) {
    this.restTemplate = restTemplate;
    this.dataSource = dataSource;
  }

  private boolean packageIsShippable(String id) {
    // business logic that makes REST and database calls  
    return true;
  }

  public void ship(String shipmentId) {
    if (packageIsShippable(shipmentId)) {
      // ship the thing  
    }
  }
}

Note that now the ShippingService doesn’t concern itself with how to construct the RestTemplate and DataSource dependencies, rather it just asks for them and expects them to be fully configured.

So, who will create the dependencies and pass them to the ShippingService ? In the dependency injection world, this is known as the dependency injection Container.

public class DIContainer {
  private RestTemplate getRestTemplate() {
    RestTemplate restTemplate =
        new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofMillis(1000))
            .setReadTimeout(Duration.ofMillis(2000))
            .build();
    restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("http://payment-service-uri:8080"));

    return restTemplate;
  }

  private DataSource getDataSource() {
    DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
    dataSourceBuilder.driverClassName("org.h2.Driver");
    dataSourceBuilder.url("jdbc:h2:file:C:/temp/test");
    dataSourceBuilder.username("shipping-user");
    dataSourceBuilder.password("superSecretPassword");
    return dataSourceBuilder.build();
  }

  public ShippingService getShipmentService() {
    return new ShippingService(getRestTemplate(), getDataSource());
  }
}

And now, whoever wants to use the ShippingService can simply as the DIContainer for it and start using it out of the box.

Simplifies Testing

Testing often includes testing a component that has dependencies that we don’t necessarily want to test as well. That’s where the concept of mocking comes in to help us mock the behavior of those dependencies.

Dependency injection allows dependencies to be passed into the component under test. Those dependencies could be the actual implementation or mocks that we create to simulate them during the test.

Let’s look at an example using Mockito as our mocking library.

public class ShippingServiceTest {
  @Test
  void testShipping() {
    RestTemplate restTemplateMock = Mockito.mock(RestTemplate.class);
    DataSource dataSourceMock = Mockito.mock(DataSource.class);
    when(restTemplateMock.getForEntity("url", String.class))
        .thenReturn(ResponseEntity.ok("What Ever"));

    ShippingService shippingService = new ShippingService(restTemplateMock, dataSourceMock);
    shippingService.ship("some Id");
    // assert stuff  
  }
}

While we’re not using a dependency injection framework here, we are injecting the (mocked) dependencies into the constructor of ShippingService.

Dependency Injection Frameworks

In the Java world, we have three main frameworks that handle DI.

Spring

It’s an OpenSource framework developed and maintained by Pivotal. It’s a widely used framework with lots of integrations which makes quite heavyweight.

Guice

It’s an OpenSource framework that is developed and maintained by Google. It’s lightweight in comparison with Spring, however, it has fewer integrations.

Dagger

Just like Guice, it’s also an OpenSource framework maintained by Google. However, it’s more lightweight with very few integrations.

Dependency Injection in Spring

Spring makes it pretty straightforward to declare components and their dependencies and it handles the injection process itself, leaving us the task of declaring what dependencies to be injected in which components.

Spring Bean

Spring offers us the concept of Beans, which are just Java objects that get registered in the Spring Bean Registry.

Spring Beans are objects that we define in a configuration class.

@Configuration
public class ShipmentConfiguration {

  @Bean
  public RestTemplate restTemplate() {
    RestTemplate restTemplate =
        new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofMillis(1000))
            .setReadTimeout(Duration.ofMillis(2000))
            .build();
    restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("http://payment-service-uri:8080"));

    return restTemplate;
  }
}

By doing this we are telling Spring to register a Bean of type RestTemplate with a name of restTemplate. Spring then allows us to inject this Bean in any other registered Spring Bean or in a Spring Component.

To have Spring inject the RestTemplate object into our ShippingService all we have to do is to accept it as a constructor argument:

@Component  
public class ShippingService {  
      
    private final RestTemplate restTemplate;  
    
    public ShippingService(RestTemplate restTemplate){
        this.restTemplate = restTemplate;
    }
    
	public void ship(String shipmentId) {  
	   // do stuff  
	 }  
}

Note that we annotated the ShippingService class with the @Component annotation which tells Spring to make a bean of this class and to inject whatever dependencies it has.

Dependency Injection Types in Spring

Spring offers us different ways to inject dependencies into our components. Let’s get to know them.

Field Injection

We declare the dependency as a field in the component and simply annotate it with @Autowired:

@Component
public class ShippingService {

    @Autowired
    RestTemplate restTemplate;

    public void ship(String shipmentId) {
        // do stuff  
    }
}

Setter Injection

We can annotate a setter method with @Autowired which tells Spring to inject the Bean of the type declared in the parameter.

@Component
public class ShippingService {

    RestTemplate restTemplate;

    @Autowired
    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void ship(String shipmentId) {
        // do stuff  
    }
}

Constructor Injection

Spring also allows us to inject the dependencies through the constructor of the component class, which we have seen in the first example:

@Component
public class ShippingService {

    RestTemplate restTemplate;

    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void ship(String shipmentId) {
        // do stuff  
    }
}

Constructor injection is the preferred way of injecting dependencies, because it makes the code less dependent on the framework. We can just as well use the constructor without Spring to create an object with mocked dependencies for a unit test, for example.

Conclusion

Inversion of control (IoC) is a design pattern in which we declare an action to be taken when a certain event happens in our system. It is heavily used in software because it allows us to write clean and maintainable code.

Dependency injection (DI) is one form of IoC where we delegate the responsibility of creating and injecting components' dependencies to some other party outside of the components themselves.

Written By:

Abdulcelil Cercenazi

Written By:

Abdulcelil Cercenazi

Software developer and craftsman. Loves working with Java | Spring | Docker. Always looking for new adventures

Recent Posts

Demystifying Transactions and Exceptions with Spring

One of the most convincing justifications for using the Spring Framework is its extensive transaction support. For transaction management, the Spring Framework offers a stable abstraction.

Read more

JUnit 5 Parameterized Tests

If you’re reading this article, it means you’re already well-versed with JUnit. Let me give you a summary of JUnit - In software development, we developers write code which does something simple as designing a person’s profile or as complex as making a payment (in a banking system).

Read more

Building a CSV Importer with Node.js

Consider the case where someone provides us with a CSV file containing employee details that have been exported from an employee management application.

Read more