Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (part of the SOLID principles) aims to create flexible and maintainable code by ensuring that dependencies rely on abstractions rather than concrete implementations.

What Does This Mean?

Bad Design (Without Dependency Inversion):
Imagine a class that directly depends on the concrete implementation of another class. If we want to make changes, we have to modify the main class, which can lead to problems.

Better Design (With Dependency Inversion):
Instead of having the main class depend directly on a concrete class, it should depend on an abstraction (like an interface or an abstract class). This way, we can switch implementations without modifying the main class.

Bad Example in Python

Let’s say we have a system that sends notifications. Here’s an initial, poorly designed implementation:

class EmailSender:
    def send(self, message):
        print(f"Sending email: {message}")


class Notification:
    def __init__(self):
        self.email_sender = EmailSender()  # Direct dependency on EmailSender

    def notify(self, message):
        self.email_sender.send(message)

If we want to send notifications via SMS or another channel, we would have to modify the Notification class, which violates the Dependency Inversion Principle.

Applying the Dependency Inversion Principle

The solution is to introduce an abstraction:

from abc import ABC, abstractmethod

# Abstract base class (interface for sending messages)
class MessageSender(ABC):
    @abstractmethod
    def send(self, message):
        pass


# Concrete implementations
class EmailSender(MessageSender):
    def send(self, message):
        print(f"Sending email: {message}")


class SMSSender(MessageSender):
    def send(self, message):
        print(f"Sending SMS: {message}")


# The Notification class now depends on an abstraction
class Notification:
    def __init__(self, sender: MessageSender):  # Depends on abstraction
        self.sender = sender

    def notify(self, message):
        self.sender.send(message)


# Usage
email_sender = EmailSender()
sms_sender = SMSSender()

# Notification is flexible and can use different implementations
notification = Notification(email_sender)
notification.notify("Hello via Email!")

notification = Notification(sms_sender)
notification.notify("Hello via SMS!")

Advantages of This Approach:

  • Flexibility: We can easily switch implementations (e.g., from EmailSender to SMSSender) without modifying the Notification class.
  • Testability: The Notification class is easier to test because we can pass a mock implementation.
  • Maintainability: Changes in one concrete class (e.g., EmailSender) do not affect the Notification class.

Conclusion:

The Dependency Inversion Principle ensures that our code is more flexible, easier to modify, and maintainable because it depends on abstractions rather than concrete classes.

About the author

Vili M, PhD

With an extensive experience in programming, Vili has dedicated his career to developing innovative solutions and advancing technology. As an expert in programming, electromagnetic fields, robotics, and teaching skills, he combines academic knowledge with practical expertise to deliver impactful results.