Java tutorials > Frameworks and Libraries > General Concepts > What is dependency injection?
What is dependency injection?
Dependency Injection (DI) is a design pattern in which an object receives other objects that it depends on (its dependencies). The "injection" refers to the passing of a dependency to a dependent object. This is usually done as a constructor parameter, a "setter" method, or a more general interface. Dependency Injection promotes loose coupling, making code more testable, reusable, and maintainable.
Understanding Dependency Inversion Principle (DIP)
Dependency Injection is closely related to the Dependency Inversion Principle. DIP states that: DI helps in achieving DIP by decoupling high-level modules from low-level modules through the use of abstractions.
Traditional Tight Coupling (Without DI)
In this example, NotificationService
is tightly coupled with EmailService
. If we wanted to switch to a different email service or mock EmailService
for testing, we would need to modify NotificationService
.
class EmailService {
public void sendEmail(String message, String recipient) {
// Logic to send email
System.out.println("Sending email to " + recipient + ": " + message);
}
}
class NotificationService {
private EmailService emailService = new EmailService(); // Tight coupling
public void sendNotification(String message, String recipient) {
emailService.sendEmail(message, recipient);
}
}
public class Main {
public static void main(String[] args) {
NotificationService notificationService = new NotificationService();
notificationService.sendNotification("Hello", "test@example.com");
}
}
Dependency Injection Example (Constructor Injection)
In this example, NotificationService
depends on an abstraction (MessageService
) instead of a concrete implementation (EmailService
). The MessageService
dependency is injected through the constructor. This allows us to easily switch between different message services (e.g., EmailService
, SMSService
) without modifying NotificationService
. It also makes it easier to test NotificationService
by injecting a mock MessageService
.
interface MessageService {
void sendMessage(String message, String recipient);
}
class EmailService implements MessageService {
@Override
public void sendMessage(String message, String recipient) {
// Logic to send email
System.out.println("Sending email to " + recipient + ": " + message);
}
}
class SMSService implements MessageService {
@Override
public void sendMessage(String message, String recipient) {
// Logic to send SMS
System.out.println("Sending SMS to " + recipient + ": " + message);
}
}
class NotificationService {
private MessageService messageService;
public NotificationService(MessageService messageService) { // Constructor Injection
this.messageService = messageService;
}
public void sendNotification(String message, String recipient) {
messageService.sendMessage(message, recipient);
}
}
public class Main {
public static void main(String[] args) {
MessageService emailService = new EmailService();
NotificationService notificationService = new NotificationService(emailService); // Injecting EmailService
notificationService.sendNotification("Hello", "test@example.com");
MessageService smsService = new SMSService();
NotificationService notificationService2 = new NotificationService(smsService); // Injecting SMSService
notificationService2.sendNotification("Hi", "+15551234567");
}
}
Types of Dependency Injection
There are three main types of Dependency Injection:
Setter Injection Example
Here, the MessageService
dependency is injected using the setMessageService
method. This allows for optionally setting the MessageService
. If the setter is never called, the messageService
field would remain null
and the sendNotification
method would need to handle that potential null value. This illustrates a possible disadvantage of setter injection - dependencies are not always guaranteed to be available.
interface MessageService {
void sendMessage(String message, String recipient);
}
class EmailService implements MessageService {
@Override
public void sendMessage(String message, String recipient) {
// Logic to send email
System.out.println("Sending email to " + recipient + ": " + message);
}
}
class NotificationService {
private MessageService messageService;
public void setMessageService(MessageService messageService) { // Setter Injection
this.messageService = messageService;
}
public void sendNotification(String message, String recipient) {
messageService.sendMessage(message, recipient);
}
}
public class Main {
public static void main(String[] args) {
NotificationService notificationService = new NotificationService();
MessageService emailService = new EmailService();
notificationService.setMessageService(emailService); // Injecting EmailService using setter
notificationService.sendNotification("Hello", "test@example.com");
}
}
Real-Life Use Case: Database Connections
Consider a system where you need to connect to different databases (MySQL, PostgreSQL, etc.). Instead of hardcoding the database connection details within the classes that need the connection, you can inject a DatabaseConnection
interface implementation. This allows you to switch databases easily by simply changing the injected dependency.
Best Practices
Interview Tip
When discussing Dependency Injection in an interview, be prepared to explain the advantages of DI, different types of DI (constructor, setter, interface), and how DI relates to the Dependency Inversion Principle.
When to Use Dependency Injection
Use Dependency Injection when:
Memory Footprint
Dependency Injection, in itself, doesn't significantly increase the memory footprint. The memory used depends on the size of the injected dependencies. However, frameworks that implement DI might have a larger footprint due to reflection and other mechanisms used for managing dependencies.
Alternatives to Dependency Injection
Alternatives to Dependency Injection include:
Pros of Dependency Injection
Cons of Dependency Injection
FAQ
-
What is the difference between Dependency Injection and Dependency Inversion?
Dependency Inversion is a principle, while Dependency Injection is a design pattern that helps in implementing that principle. DI is one way to achieve DIP.
-
What are the benefits of using a DI framework like Spring?
DI frameworks automate the process of dependency injection, providing features like dependency resolution, lifecycle management, and aspect-oriented programming (AOP).
-
Can Dependency Injection be used without a DI framework?
Yes, Dependency Injection can be implemented manually without using a DI framework. The examples above demonstrate manual DI.