DEV Community

Cover image for What exactly are Inversion of Control and Dependency Injection? How do they correlate with each other?
gsbc
gsbc

Posted on

What exactly are Inversion of Control and Dependency Injection? How do they correlate with each other?

I have always been confused about these two concepts, but I think it's time to finally tackle them.

Inversion of Control (IoC) and Dependency Injection (DI) are software design patterns. Those concepts help improve modularity, testability and maintainability of code by promoting loose coupling between components. Those principles are often used in the context of object-oriented programming (OOP).

Let's take a closer look:

  • Inversion of Control is a design principle that involves inverting the flow of control in a system. High-level components, in tradicional software design, directly call low-level components and manage their dependencies. With IoC, high-level components do not explicitly call or create low-level components: instead, high-level components define their dependencies and rely on an external mechanism in order to provide the dependencies needed. The change in control flow allows greater flexibility and modularity.
  • Dependency Injection is an implementation of the design principle above. It is a way of providing the dependencies of one component (or object) to another without having the dependent object create or manage the dependency itself. In DI, an external source, such as a framework, factory, or container, creates and injects the dependencies into the dependent object, usually through constructor arguments, properties or methods. This technique reduces tight coupling between the system components, thus making them easier to maintain, test and extend.

Of course, let's exemplify these concepts by making use of the Spring Framework, which provides Dependency Injection.

Consider a simple scenario where a MessageService interface has two implementations, EmailService and SMSService. There is also a NotificationService that depends on the MessageService.

public interface MessageService {
    void sendMessage(String message, String recipient);
}

public class EmailService implements MessageService {
    public void sendMessage(String message, String recipient) {
        // Send an email
    }
}

public class SMSService implements MessageService {
    public void sendMessage(String message, String recipient) {
        // Send an SMS
    }
}
Enter fullscreen mode Exit fullscreen mode

Constructor-based Dependency Injection:

public class NotificationService {
    private MessageService messageService;

    public NotificationService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notify(String message, String recipient) {
        messageService.sendMessage(message, recipient);
    }
}
Enter fullscreen mode Exit fullscreen mode

There is some ways to achieve and implement Inversion of Control using Dependency Injection in Spring: defining the beans in a XML config file or using Java-based configuration with annotations, which is the method I'm going to use:

@Configuration
public class AppConfig {

    @Bean
    public MessageService emailService() {
        return new EmailService();
    }

    @Bean
    public NotificationService notificationService() {
        return new NotificationService(emailService());
    }
}
Enter fullscreen mode Exit fullscreen mode

Setter-based Dependency Injection for the NotificationService:

public class NotificationService {
    private MessageService messageService;

    public void setMessageService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notify(String message, String recipient) {
        messageService.sendMessage(message, recipient);
    }
}
Enter fullscreen mode Exit fullscreen mode

Spring configuration using Java-based configuration with annotations:

@Configuration
public class AppConfig {

    @Bean
    public MessageService emailService() {
        return new EmailService();
    }

    @Bean
    public NotificationService notificationService() {
        NotificationService service = new NotificationService();
        service.setMessageService(emailService());
        return service;
    }
}
Enter fullscreen mode Exit fullscreen mode

Both constructor-based and setter-based dependency injection have their pros and cons. The choice depends whether you need immutable or mutable objects, among other characteristics, which are going to be explained as follows.

Constructor-based Dependency Injection

Pros

  • Immutable objects: By injecting dependencies through the constructor, you can make your objects immutable, which can lead to safer and more reliable code, especially in multi-threaded environments.
  • Explicit dependencies: Constructor-based injection makes it clear which dependencies are required for an object to function correctly, since they must be provided when the object is created.
  • Fail-fast behavior: If a required dependency is not provided, the object instantiation will fail, making it easy to spot and fix the issue early in the application lifecycle.

Cons

  • Verbosity: Constructor-based injection can become verbose when there are many dependencies, leading to long constructor parameter lists. This can make the code harder to read and maintain.
  • Inflexibility: With constructor-based injection, dependencies are set at the time of object creation and cannot be changed later. This can be limiting if you need to modify dependencies during the runtime of your application.

Setter-based Dependency Injection

Pros

  • Flexibility: Setter-based injection allows you to change the dependencies of an object during its lifetime, which can be useful in certain scenarios where dependencies need to be modified at runtime.
  • Less verbose: Setter-based injection can lead to less verbose code, especially when there are many optional dependencies.

Cons

  • Mutable objects: Setter-based injection can result in mutable objects, which may lead to less predictable behavior and potential issues in multi-threaded environments.
  • Hidden dependencies: With setter-based injection, dependencies may not be as explicitly required as with constructor-based injection, making it harder to understand the dependencies of a class at a glance.
  • Late failure: If a required dependency is not provided, the error might not be caught until the method relying on the dependency is called, making it harder to spot and fix issues.

In general, constructor-based dependency injection is recommended when the dependencies are required for the object to function correctly and when immutability is desired. Setter-based dependency injection can be used when you need more flexibility to change dependencies at runtime or when dealing with optional dependencies.

It's worth noting that you can also combine both approaches in your application, using constructor-based injection for required dependencies and setter-based injection for optional or modifiable dependencies.

Hope you folks enjoyed it! Any feedback is appreciated.

Next up: streams and how these tools correlate with parallelism and concurrency.

Top comments (0)