The Dependency Injection Revolution : From Chaos to Clean Code

Mohammad Robiul Alam

14 January, 2025

Imagine you’re building a house. For each part of the house, like doors, windows, and walls, you could create everything yourself or hire experts (carpenters, plumbers) to do that for you. Instead of doing everything manually, you can assign (inject) the experts into building the house.

Similarly, Dependency Injection (DI) is a design pattern that relieves the burden of managing dependencies in programming. Instead of creating objects (dependencies) inside a class, we provide them from outside. This is like telling the class, “Hey, focus on your job, and I’ll take care of what you need.

Why was Dependency Injection Introduced?

In traditional programming, classes can be tightly coupled. This means one class creates objects of other classes inside its methods. This can cause several issues:

  • It’s hard to maintain: Changing one class requires changing many others because they are tightly linked.
  • Challenging to test: Testing individual parts becomes complicated because they depend on other classes.
  • Inflexibility: If you want to use a different version of a class (e.g., a new way to handle logging), you’d need to modify many places.

The central concept behind Dependency Injection (DI) is the inversion of control (IoC). This means a class shouldn’t be responsible for setting up its dependencies (objects it needs to work with). Instead, another class should provide these dependencies from the outside. In other words, the control over creating and managing dependencies is ‘inverted’ from the class to an external entity, promoting a more modular and maintainable codebase.

This concept is part of the S.O.L.I.D principles of object-oriented programming, precisely the fifth one, which says a class should depend on abstractions (general contracts like interfaces) rather than concretions (specific, hard-coded implementations).

The class should focus on doing its job rather than creating the necessary tools (objects). DI helps with this by loosening the connection between classes. Instead of directly creating and controlling dependencies, the class receives them outside, making it easier to swap, test, and maintain.

Let’s consider an example to understand DI better. Consider a simple notification system that sends an Email or an SMS.

The Problem without Dependency Injection

Without DI, you might hard-code the notification service inside a class, leading to tightly coupled code. Let’s look at how this might be written without DI:

				
					class NotificationService {
    public void sendNotification(String message) {
        EmailService emailService = new EmailService();
       emailService.sendEmail(message);
    }
 }
 class EmailService {
    public void sendEmail(String message) {
        System.out.println("Sending email: " + message);
    }
 }
 public class Main {
    public static void main(String[] args) {
        NotificationService notificationService = new NotificationService();
        notificationService.sendNotification("Hello, World!");
    }
 }

				
			

In this case, the NotificationService is tightly coupled to the EmailService. If we wanted to send an SMS instead of an email, we’d have to modify the NotificationService class.

Solution with Dependency Injection

Using DI, we can inject the email or SMS service into NotificationService. Here’s how:

Step 1: Define a standard interface for both email and SMS services.

				
					interface MessageService {
    void sendMessage(String message);
 }
				
			

Step 2: Implement the EmailService and SMSService.

				
					 class EmailService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
 }

 class SMSService implements MessageService {
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
 }
				
			

Step 3: Inject the service using a constructor.

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

    public void sendNotification(String message) {
        messageService.sendMessage(message);
    }
 }
				
			

Step 4: Inject the EmailService and SMSService into NotificationService as constructor

				
					 public class Main {
    public static void main(String[] args) {
       
        MessageService emailService = new EmailService();
        NotificationService notificationService = new NotificationService(emailService);
        notificationService.sendNotification("Hello via Email!");

        MessageService smsService = new SMSService();
        NotificationService smsNotificationService = new NotificationService(smsService);
        smsNotificationService.sendNotification("Hello via SMS!");
    }
 }
				
			
What’s happening?
  • Interface-based Design: We use a standard MessageService interface that both EmailService and SMSService implement.
  • Constructor Injection: The NotificationService gets its dependency (MessageService) injected via the constructor.
  • Loose coupling: NotificationService doesn’t care which specific service it uses. You can easily switch between email and SMS by injecting a different implementation without changing the notification service.
So what’s now?
  • You can easily swap the service from EmailService to SMSService without touching the NotificationService class.
  • It’s testable! You can create mock versions of MessageService to test how NotificationService works in isolation.
  • The code is now more flexible and maintainable, adhering to good software engineering practices.
In summary, the benefits of DI
  • Loose coupling, a key principle in software design, is achieved through Dependency Injection. It reduces the dependency of one class on another. Instead of a class controlling its dependencies, it gets them externally. This means that classes are less reliant on each other, making the code more flexible and easier to maintain.
  • Easier testing: Since dependencies can be provided externally, you can swap them with mock objects during testing, simplifying unit testing.
  • Better maintainability: You don’t have to change the entire system to swap or modify a dependency. You can inject a new one quickly.
  • Improved code readability and flexibility: DI promotes cleaner and more modular code. Each class has a single responsibility and gets what it needs from the outside.
Conclusion

Dependency Injection (DI) is a powerful design pattern that can be applied in various scenarios. Whether you’re building a simple notification system or a complex enterprise application, DI can help write flexible, maintainable, and testable code. By separating the creation of dependencies from their usage, DI allows for greater modularity, especially in larger systems. As shown in the example, using DI can turn tightly coupled code into a more flexible solution where swapping out dependencies is simple and testing becomes a breeze.

So, next time you build a project, remember how dependency injection can help you keep your code clean, flexible, and easy to maintain!

Mohammad Robiul Alam

14 January, 2025

Continue reading

Picture of Md Sajjad Hosen Noyon
Md Sajjad Hosen Noyon

14 January, 2025

Picture of Dewan Fazle Ayaz
Dewan Fazle Ayaz

1 January, 2025

Picture of Dewan Fazle Ayaz
Dewan Fazle Ayaz

1 January, 2025