In the Dependency Inversion Principle (DIP), the key idea is to let something else create objects for you, and you use those objects without having to create them directly. You "invert" the control of object creation.
Imagine it like this: Instead of going to the store to buy groceries (creating objects directly), you order groceries online (letting someone else create and deliver them) and use them as needed.
In the same way, DIP encourages you to let a framework or other class create objects and inject (provide) them where you need them in your code. This makes your code more flexible and easier to change because you're not responsible for creating and managing everything yourself.
Let's explain DIP to a novice with an example where dependencies are injected manually via a configuration class:
Example (Dependency Injection with Manual Configuration):
Imagine you are building a simple notification system, and you want to send notifications via different channels such as email and SMS. Traditionally, you might create classes that directly use specific notification channels.
Violation of DIP (Without Dependency Injection):
class EmailNotification {
public void sendEmail(String to, String message) {
// Code to send an email
}
}
class SMSNotification {
public void sendSMS(String to, String message) {
// Code to send an SMS
}
}
In this code, the EmailNotification
and SMSNotification
classes directly depend on specific notification channels, violating DIP because they tightly couple to low-level modules.
Now, let's adhere to DIP using manual dependency injection through a configuration class:
Adhering to DIP (With Dependency Injection via Configuration):
interface NotificationService {
void sendNotification(String to, String message);
}
class EmailNotification implements NotificationService {
public void sendNotification(String to, String message) {
// Code to send an email
}
}
class SMSNotification implements NotificationService {
public void sendNotification(String to, String message) {
// Code to send an SMS
}
}
class NotificationApp {
private NotificationService notificationService;
public NotificationApp(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void sendNotification(String to, String message) {
notificationService.sendNotification(to, message);
}
}
In this adhering-to-DIP example:
- We define an interface
NotificationService
that defines the common behavior for sending notifications. - The
EmailNotification
andSMSNotification
classes implement this interface, specifying how they send notifications. - The
NotificationApp
class depends on theNotificationService
interface and can work with any class that implements this interface.
Configuration Class (Manual Dependency Injection):
Now, let's manually configure the dependencies using a configuration class:
public class AppConfig {
public NotificationService emailNotificationService() {
return new EmailNotification();
}
public NotificationService smsNotificationService() {
return new SMSNotification();
}
public NotificationApp notificationApp() {
return new NotificationApp(emailNotificationService());
}
}
In this AppConfig
class:
- We define two methods,
emailNotificationService
andsmsNotificationService
, which create instances ofEmailNotification
andSMSNotification
. - We also define a method
notificationApp
that creates an instance ofNotificationApp
and injects theemailNotificationService
into it.
This manual configuration allows us to inject dependencies into the NotificationApp
class without changing its code. We can switch notification services by modifying the AppConfig
class while adhering to the Dependency Inversion Principle.
Benefits of Manual Dependency Injection:
- The
NotificationApp
class is no longer tightly coupled to specific notification channels, making it more flexible and maintainable. - We can easily switch between different notification services by modifying the configuration class, promoting adherence to DIP.
In summary, the Dependency Inversion Principle suggests that high-level modules should not depend on low-level modules. Instead, they should both depend on abstractions. Manual dependency injection, as shown in this example, helps achieve this separation of concerns and promotes flexibility in your code.
"Your feedback and ideas are invaluable โ drop a comment, and let's make this even better!"
๐ If you enjoy the content, please ๐ like, ๐ share, and ๐ฃ follow for more updates!
Join me on a professional journey through my LinkedIn profile: Linkedin Profile
Top comments (0)