Dependency Inversion Principle
The Dependency Inversion Principle (DIP) states that:
- 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.
This principle helps create flexible, maintainable systems by inverting the traditional dependency flow.
Understanding the Principle
Section titled “Understanding the Principle”The Dependency Inversion Principle ensures that:
- High-level modules (business logic) don’t depend on low-level modules (implementation details)
- Both depend on abstractions (interfaces)
- Details depend on abstractions, not the other way around
- Flexibility - Easy to swap implementations
- Testability - Easy to mock dependencies
In simple terms: Depend on what something can do (interface), not how it does it (implementation)!
Example 1: Light Switch Problem
Section titled “Example 1: Light Switch Problem”Consider a simple light switch system. Let’s see how direct dependencies create problems.
Violating DIP (Bad Approach)
Section titled “Violating DIP (Bad Approach)”class LightBulb: """Low-level module - concrete implementation""" def turn_on(self): print("Light bulb turned on")
def turn_off(self): print("Light bulb turned off")
class LightSwitch: """❌ High-level module - depends on concrete LightBulb""" def __init__(self): self.bulb = LightBulb() # ❌ Direct dependency on concrete class!
def toggle(self): if self.bulb.is_on: # Assuming we track state self.bulb.turn_off() else: self.bulb.turn_on()
# Problem: What if we want to use LED instead of LightBulb?class LED: """New type of light""" def turn_on(self): print("LED turned on")
def turn_off(self): print("LED turned off")
# ❌ Can't use LED with LightSwitch without modifying LightSwitch!# LightSwitch is tightly coupled to LightBulb// Low-level module - concrete implementationpublic class LightBulb { private boolean isOn = false;
public void turnOn() { isOn = true; System.out.println("Light bulb turned on"); }
public void turnOff() { isOn = false; System.out.println("Light bulb turned off"); }
public boolean isOn() { return isOn; }}
// ❌ High-level module - depends on concrete LightBulbpublic class LightSwitch { private LightBulb bulb; // ❌ Direct dependency on concrete class!
public LightSwitch() { this.bulb = new LightBulb(); }
public void toggle() { if (bulb.isOn()) { bulb.turnOff(); } else { bulb.turnOn(); } }}
// Problem: What if we want to use LED instead of LightBulb?class LED { // New type of light public void turnOn() { System.out.println("LED turned on"); }
public void turnOff() { System.out.println("LED turned off"); }}
// ❌ Can't use LED with LightSwitch without modifying LightSwitch!// LightSwitch is tightly coupled to LightBulbFollowing DIP (Good Approach)
Section titled “Following DIP (Good Approach)”from abc import ABC, abstractmethod
class Switchable(ABC): """Abstraction - defines what a switchable device can do""" @abstractmethod def turn_on(self): """Turn the device on""" pass
@abstractmethod def turn_off(self): """Turn the device off""" pass
class LightBulb(Switchable): """Low-level module - concrete implementation""" def __init__(self): self.is_on = False
def turn_on(self): self.is_on = True print("Light bulb turned on")
def turn_off(self): self.is_on = False print("Light bulb turned off")
class LED(Switchable): """Low-level module - another concrete implementation""" def __init__(self): self.is_on = False
def turn_on(self): self.is_on = True print("LED turned on")
def turn_off(self): self.is_on = False print("LED turned off")
class LightSwitch: """✓ High-level module - depends on abstraction""" def __init__(self, device: Switchable): # ✓ Depends on abstraction! self.device = device self.is_on = False
def toggle(self): if self.is_on: self.device.turn_off() self.is_on = False else: self.device.turn_on() self.is_on = True
# Usage - Flexible and testable!bulb = LightBulb()switch1 = LightSwitch(bulb) # ✓ Works with LightBulbswitch1.toggle() # Turns on bulb
led = LED()switch2 = LightSwitch(led) # ✓ Works with LED - no code changes!switch2.toggle() # Turns on LED
# Easy to add new devices without modifying LightSwitch!class Fan(Switchable): def turn_on(self): print("Fan turned on")
def turn_off(self): print("Fan turned off")
fan = Fan()switch3 = LightSwitch(fan) # ✓ Works with Fan too!switch3.toggle()// Abstraction - defines what a switchable device can dopublic interface Switchable { // Turn the device on void turnOn();
// Turn the device off void turnOff();}
// Low-level module - concrete implementationpublic class LightBulb implements Switchable { private boolean isOn = false;
public LightBulb() { this.isOn = false; }
@Override public void turnOn() { isOn = true; System.out.println("Light bulb turned on"); }
@Override public void turnOff() { isOn = false; System.out.println("Light bulb turned off"); }}
// Low-level module - another concrete implementationpublic class LED implements Switchable { private boolean isOn = false;
public LED() { this.isOn = false; }
@Override public void turnOn() { isOn = true; System.out.println("LED turned on"); }
@Override public void turnOff() { isOn = false; System.out.println("LED turned off"); }}
// ✓ High-level module - depends on abstractionpublic class LightSwitch { private Switchable device; // ✓ Depends on abstraction! private boolean isOn = false;
public LightSwitch(Switchable device) { this.device = device; }
public void toggle() { if (isOn) { device.turnOff(); isOn = false; } else { device.turnOn(); isOn = true; } }}
// Easy to add new devices without modifying LightSwitch!class Fan implements Switchable { @Override public void turnOn() { System.out.println("Fan turned on"); }
@Override public void turnOff() { System.out.println("Fan turned off"); }}
// Usage - Flexible and testable!public class Main { public static void main(String[] args) { LightBulb bulb = new LightBulb(); LightSwitch switch1 = new LightSwitch(bulb); // ✓ Works with LightBulb switch1.toggle(); // Turns on bulb
LED led = new LED(); LightSwitch switch2 = new LightSwitch(led); // ✓ Works with LED - no code changes! switch2.toggle(); // Turns on LED
Fan fan = new Fan(); LightSwitch switch3 = new LightSwitch(fan); // ✓ Works with Fan too! switch3.toggle(); }}Why this follows DIP:
LightSwitchdepends onSwitchableabstraction, not concrete classes- Can easily swap implementations without modifying
LightSwitch - Easy to test - can create mock
Switchableimplementations - New devices can be added without changing existing code
Example 2: Order Processing System
Section titled “Example 2: Order Processing System”Consider an e-commerce order processing system that needs to send notifications and process payments.
Violating DIP (Bad Approach)
Section titled “Violating DIP (Bad Approach)”class EmailService: """Low-level module - concrete email implementation""" def send_email(self, to: str, subject: str, body: str): print(f"Sending email to {to}: {subject}") # Actual email sending logic...
class StripePaymentProcessor: """Low-level module - concrete payment implementation""" def charge(self, amount: float, card_token: str): print(f"Charging ${amount} via Stripe") # Actual Stripe API call...
class OrderProcessor: """❌ High-level module - depends on concrete implementations""" def __init__(self): self.email_service = EmailService() # ❌ Direct dependency! self.payment_processor = StripePaymentProcessor() # ❌ Direct dependency!
def process_order(self, order: dict): """Process an order""" # Process payment self.payment_processor.charge( order['amount'], order['card_token'] )
# Send confirmation email self.email_service.send_email( to=order['email'], subject="Order Confirmation", body=f"Your order for ${order['amount']} has been processed" )// Low-level module - concrete email implementationpublic class EmailService { public void sendEmail(String to, String subject, String body) { System.out.println("Sending email to " + to + ": " + subject); // Actual email sending logic... }}
// Low-level module - concrete payment implementationpublic class StripePaymentProcessor { public void charge(double amount, String cardToken) { System.out.println("Charging $" + amount + " via Stripe"); // Actual Stripe API call... }}
// ❌ High-level module - depends on concrete implementationspublic class OrderProcessor { private EmailService emailService; // ❌ Direct dependency! private StripePaymentProcessor paymentProcessor; // ❌ Direct dependency!
public OrderProcessor() { this.emailService = new EmailService(); this.paymentProcessor = new StripePaymentProcessor(); }
// Process an order public void processOrder(Order order) { // Process payment paymentProcessor.charge(order.getAmount(), order.getCardToken());
// Send confirmation email emailService.sendEmail( order.getEmail(), "Order Confirmation", "Your order for $" + order.getAmount() + " has been processed" ); }}
class Order { private double amount; private String cardToken; private String email;
public Order(double amount, String cardToken, String email) { this.amount = amount; this.cardToken = cardToken; this.email = email; }
public double getAmount() { return amount; } public String getCardToken() { return cardToken; } public String getEmail() { return email; }}Why this violates DIP:
OrderProcessordirectly depends on concreteEmailServiceandStripePaymentProcessor- Can’t swap implementations without modifying
OrderProcessor - Hard to test - can’t inject mock dependencies
- Tight coupling reduces flexibility
Following DIP (Good Approach)
Section titled “Following DIP (Good Approach)”from abc import ABC, abstractmethod
# Abstractions - define what services can doclass NotificationService(ABC): """Abstraction for notification services""" @abstractmethod def send_notification(self, to: str, message: str): """Send a notification""" pass
class PaymentProcessor(ABC): """Abstraction for payment processors""" @abstractmethod def process_payment(self, amount: float, details: dict) -> bool: """Process a payment and return success status""" pass
# Low-level modules - concrete implementationsclass EmailService(NotificationService): """Low-level module - email implementation""" def send_notification(self, to: str, message: str): print(f"Sending email to {to}: {message}") # Actual email sending logic...
class SMSService(NotificationService): """Low-level module - SMS implementation""" def send_notification(self, to: str, message: str): print(f"Sending SMS to {to}: {message}") # Actual SMS sending logic...
class StripePaymentProcessor(PaymentProcessor): """Low-level module - Stripe implementation""" def process_payment(self, amount: float, details: dict) -> bool: print(f"Processing ${amount} via Stripe") # Actual Stripe API call... return True
class PayPalPaymentProcessor(PaymentProcessor): """Low-level module - PayPal implementation""" def process_payment(self, amount: float, details: dict) -> bool: print(f"Processing ${amount} via PayPal") # Actual PayPal API call... return True
# High-level module - depends on abstractionsclass OrderProcessor: """✓ High-level module - depends on abstractions""" def __init__( self, notification_service: NotificationService, # ✓ Abstraction! payment_processor: PaymentProcessor # ✓ Abstraction! ): self.notification_service = notification_service self.payment_processor = payment_processor
def process_order(self, order: dict): """Process an order""" # Process payment success = self.payment_processor.process_payment( order['amount'], order['payment_details'] )
if success: # Send confirmation notification self.notification_service.send_notification( to=order['email'], message=f"Your order for ${order['amount']} has been processed" )
# Usage - Flexible and testable!# Can easily swap implementationsemail_service = EmailService()stripe_processor = StripePaymentProcessor()order_processor1 = OrderProcessor(email_service, stripe_processor)
# Switch to SMS and PayPal - no code changes needed!sms_service = SMSService()paypal_processor = PayPalPaymentProcessor()order_processor2 = OrderProcessor(sms_service, paypal_processor)
# Easy to test with mocksclass MockNotificationService(NotificationService): def send_notification(self, to: str, message: str): print(f"[MOCK] Would send to {to}: {message}")
class MockPaymentProcessor(PaymentProcessor): def process_payment(self, amount: float, details: dict) -> bool: print(f"[MOCK] Would process ${amount}") return True
mock_notification = MockNotificationService()mock_payment = MockPaymentProcessor()test_processor = OrderProcessor(mock_notification, mock_payment)# Easy to test without real services!import java.util.Map;
// Abstractions - define what services can dopublic interface NotificationService { // Send a notification void sendNotification(String to, String message);}
public interface PaymentProcessor { // Process a payment and return success status boolean processPayment(double amount, Map<String, String> details);}
// Low-level modules - concrete implementationspublic class EmailService implements NotificationService { // Low-level module - email implementation @Override public void sendNotification(String to, String message) { System.out.println("Sending email to " + to + ": " + message); // Actual email sending logic... }}
public class SMSService implements NotificationService { // Low-level module - SMS implementation @Override public void sendNotification(String to, String message) { System.out.println("Sending SMS to " + to + ": " + message); // Actual SMS sending logic... }}
public class StripePaymentProcessor implements PaymentProcessor { // Low-level module - Stripe implementation @Override public boolean processPayment(double amount, Map<String, String> details) { System.out.println("Processing $" + amount + " via Stripe"); // Actual Stripe API call... return true; }}
public class PayPalPaymentProcessor implements PaymentProcessor { // Low-level module - PayPal implementation @Override public boolean processPayment(double amount, Map<String, String> details) { System.out.println("Processing $" + amount + " via PayPal"); // Actual PayPal API call... return true; }}
// High-level module - depends on abstractionspublic class OrderProcessor { // ✓ High-level module - depends on abstractions private NotificationService notificationService; // ✓ Abstraction! private PaymentProcessor paymentProcessor; // ✓ Abstraction!
public OrderProcessor( NotificationService notificationService, PaymentProcessor paymentProcessor ) { this.notificationService = notificationService; this.paymentProcessor = paymentProcessor; }
// Process an order public void processOrder(Order order) { // Process payment Map<String, String> paymentDetails = order.getPaymentDetails(); boolean success = paymentProcessor.processPayment( order.getAmount(), paymentDetails );
if (success) { // Send confirmation notification notificationService.sendNotification( order.getEmail(), "Your order for $" + order.getAmount() + " has been processed" ); } }}
class Order { private double amount; private Map<String, String> paymentDetails; private String email;
public Order(double amount, Map<String, String> paymentDetails, String email) { this.amount = amount; this.paymentDetails = paymentDetails; this.email = email; }
public double getAmount() { return amount; } public Map<String, String> getPaymentDetails() { return paymentDetails; } public String getEmail() { return email; }}
// Usage - Flexible and testable!public class Main { public static void main(String[] args) { // Can easily swap implementations EmailService emailService = new EmailService(); StripePaymentProcessor stripeProcessor = new StripePaymentProcessor(); OrderProcessor orderProcessor1 = new OrderProcessor(emailService, stripeProcessor);
// Switch to SMS and PayPal - no code changes needed! SMSService smsService = new SMSService(); PayPalPaymentProcessor paypalProcessor = new PayPalPaymentProcessor(); OrderProcessor orderProcessor2 = new OrderProcessor(smsService, paypalProcessor);
// Easy to test with mocks // (Mock classes would implement the interfaces) }}Why this follows DIP:
OrderProcessordepends onNotificationServiceandPaymentProcessorabstractions- Can easily swap implementations (Email → SMS, Stripe → PayPal)
- Easy to test - can inject mock implementations
- New services can be added without modifying
OrderProcessor - Loose coupling makes the system flexible and maintainable
Dependency Injection
Section titled “Dependency Injection”Dependency Injection (DI) is a technique that helps implement DIP by:
- Injecting dependencies instead of creating them internally
- Making dependencies explicit through constructor parameters
- Enabling easy swapping of implementations
Without Dependency Injection (Bad)
Section titled “Without Dependency Injection (Bad)”class UserService: """❌ Creates dependencies internally""" def __init__(self): self.repository = DatabaseRepository() # ❌ Hard-coded dependency! self.logger = FileLogger() # ❌ Hard-coded dependency!
def get_user(self, user_id: int): self.logger.log(f"Getting user {user_id}") return self.repository.find(user_id)// ❌ Creates dependencies internallypublic class UserService { private DatabaseRepository repository; // ❌ Hard-coded dependency! private FileLogger logger; // ❌ Hard-coded dependency!
public UserService() { this.repository = new DatabaseRepository(); this.logger = new FileLogger(); }
public User getUser(int userId) { logger.log("Getting user " + userId); return repository.find(userId); }}With Dependency Injection (Good)
Section titled “With Dependency Injection (Good)”class UserService: """✓ Receives dependencies via constructor""" def __init__(self, repository: UserRepository, logger: Logger): self.repository = repository # ✓ Injected dependency! self.logger = logger # ✓ Injected dependency!
def get_user(self, user_id: int): self.logger.log(f"Getting user {user_id}") return self.repository.find(user_id)
# Usage - Easy to swap implementationsdatabase_repo = DatabaseRepository()file_logger = FileLogger()service1 = UserService(database_repo, file_logger)
# Switch to in-memory repo and console loggermemory_repo = InMemoryRepository()console_logger = ConsoleLogger()service2 = UserService(memory_repo, console_logger) # ✓ No code changes!// ✓ Receives dependencies via constructorpublic class UserService { private UserRepository repository; // ✓ Injected dependency! private Logger logger; // ✓ Injected dependency!
public UserService(UserRepository repository, Logger logger) { this.repository = repository; this.logger = logger; }
public User getUser(int userId) { logger.log("Getting user " + userId); return repository.find(userId); }}
// Usage - Easy to swap implementationspublic class Main { public static void main(String[] args) { // Use database repo and file logger DatabaseRepository databaseRepo = new DatabaseRepository(); FileLogger fileLogger = new FileLogger(); UserService service1 = new UserService(databaseRepo, fileLogger);
// Switch to in-memory repo and console logger InMemoryRepository memoryRepo = new InMemoryRepository(); ConsoleLogger consoleLogger = new ConsoleLogger(); UserService service2 = new UserService(memoryRepo, consoleLogger); // ✓ No code changes! }}Benefits of Following DIP
Section titled “Benefits of Following DIP”Key Takeaways
Section titled “Key Takeaways”Remember: The Dependency Inversion Principle ensures that your system is flexible, testable, and maintainable by depending on abstractions rather than concrete implementations! 🎯