Dependency Inversion Principle (DIP)


"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions."

In simpler terms, DIP encourages us to avoid tight coupling between high-level and low-level components by making both depend on abstractions (like interfaces or abstract classes) instead of concrete implementations. This leads to more flexible and scalable designs, as changes to low-level components won’t require changes to high-level components.

Example: Notification System

Imagine you are building a notification system that sends alerts via different channels such as email and SMS. Without dependency injection or the Dependency Inversion Principle, your NotificationService class might depend directly on concrete implementations like EmailSender or SMSSender. This creates a tight coupling between the high-level class (NotificationService) and the low-level classes (EmailSender, SMSSender).


        // Low-level module: EmailSender
        class EmailSender {
            public void sendEmail(String message) {
                System.out.println("Sending email with message: " + message);
            }
        }
        
        // Low-level module: SMSSender
        class SMSSender {
            public void sendSMS(String message) {
                System.out.println("Sending SMS with message: " + message);
            }
        }
        
        // High-level module: NotificationService
        class NotificationService {
            private EmailSender emailSender;
            private SMSSender smsSender;
        
            public NotificationService() {
                this.emailSender = new EmailSender();  // Tight coupling to low-level module
                this.smsSender = new SMSSender();      // Tight coupling to low-level module
            }
        
            public void sendNotification(String message) {
                emailSender.sendEmail(message);
                smsSender.sendSMS(message);
            }
        }
        
        // Client code
        public class Main {
            public static void main(String[] args) {
                NotificationService notificationService = new NotificationService();
                notificationService.sendNotification("Hello, Dependency Inversion Principle!");
            }
        }
        
    
Problem (DIP Violation):
  1. Tight Coupling: NotificationService depends directly on EmailSender and SMSSender. If we want to change how these notifications are sent (e.g., introduce a PushNotificationSender), we must modify the NotificationService class, which violates DIP.
  2. Rigid Design: Since the NotificationService class is tightly coupled to the low-level modules (EmailSender and SMSSender), adding new notification types or changing the behavior of notifications is difficult and error-prone.

Refactored Design (Complying with DIP using Dependency Injection):

  1. Create an interface MessageSender that abstracts the sending logic.
  2. Implement EmailSender, SMSSender, and any other sender classes as low-level modules that implement this interface.
  3. Use dependency injection to pass the desired sender to the NotificationService class.

        // High-level abstraction: MessageSender interface
        interface MessageSender {
            void send(String message);
        }
        
        // Low-level module: EmailSender
        class EmailSender implements MessageSender {
            @Override
            public void send(String message) {
                System.out.println("Sending email with message: " + message);
            }
        }
        
        // Low-level module: SMSSender
        class SMSSender implements MessageSender {
            @Override
            public void send(String message) {
                System.out.println("Sending SMS with message: " + message);
            }
        }
        
        // High-level module: NotificationService (now depends on abstraction)
        class NotificationService {
            private MessageSender messageSender;
        
            // Constructor injection (dependency injection)
            public NotificationService(MessageSender messageSender) {
                this.messageSender = messageSender;  // Depends on abstraction, not concrete implementation
            }
        
            public void sendNotification(String message) {
                messageSender.send(message);
            }
        }
        
        // Client code demonstrating Dependency Injection
        public class Main {
            public static void main(String[] args) {
                // Injecting EmailSender into NotificationService
                MessageSender emailSender = new EmailSender();
                NotificationService emailNotification = new NotificationService(emailSender);
                emailNotification.sendNotification("Hello via Email!");
        
                // Injecting SMSSender into NotificationService
                MessageSender smsSender = new SMSSender();
                NotificationService smsNotification = new NotificationService(smsSender);
                smsNotification.sendNotification("Hello via SMS!");
            }
        }        
    
Key Points in the Refactored Design