Introduction: What is Dependency Injection and Why You Need It in Java
In modern Java development, code cleanliness and testability are among the key requirements for a professional. Dependency Injection (DI) is a design pattern that allows you to make your code loosely coupled, flexible, and easily maintainable. Instead of an object creating its own dependencies (for example, via new), DI "injects" them from the outside. This is a fundamental part of the Inversion of Control (IoC) principle.
In this article, we will break down how Dependency Injection works in Java, look at three main ways to implement it (via constructor, setters, and interface), and show how DI is used in real projects, including the popular Spring framework.
Why Does Your Code Become Fragile Without DI?
Imagine a Car class that creates an engine inside itself:
public class Car {
private Engine engine;
public Car() {
this.engine = new PetrolEngine(); // Tight coupling
}
public void start() {
engine.run();
}
}
The problems with this approach are obvious:
- Impossible to reuse: You won't be able to easily replace
PetrolEnginewithElectricEngine. - Difficult to test: For unit tests, you'll have to create a real engine instead of a mock.
- SRP violation: The
Carclass is not only responsible for managing the car but also for creating the engine.
Dependency Injection solves these problems by delegating the creation of dependencies to external code (a container or another class).
Three Ways to Inject Dependencies in Java
In Java, there are three main approaches to implementing DI. Let's look at each one using a notification system as an example.
1. Constructor Injection
This is the most preferred method. Dependencies are passed through constructor parameters. This guarantees that the object will always be created in a valid state and that dependencies cannot be changed after creation (immutability).
public interface MessageService {
void sendMessage(String message, String recipient);
}
public class EmailService implements MessageService {
@Override
public void sendMessage(String message, String recipient) {
System.out.println("Email sent to " + recipient + ": " + message);
}
}
public class NotificationSender {
private final MessageService messageService;
// Dependency injection via constructor
public NotificationSender(MessageService messageService) {
this.messageService = messageService;
}
public void notifyUser(String userEmail, String msg) {
messageService.sendMessage(msg, userEmail);
}
}
// Usage
public class Main {
public static void main(String[] args) {
MessageService emailService = new EmailService();
NotificationSender sender = new NotificationSender(emailService); // DI
sender.notifyUser("user@example.com", "Hello!");
}
}
2. Setter Injection
This is suitable for optional dependencies or when a dependency can be changed during the object's lifetime. However, the object can be created in an incomplete state if you forget to call the setter.
public class NotificationSender {
private MessageService messageService;
// Setter for dependency injection
public void setMessageService(MessageService messageService) {
this.messageService = messageService;
}
public void notifyUser(String userEmail, String msg) {
if (messageService != null) {
messageService.sendMessage(msg, userEmail);
} else {
System.o