Decoupling Your Code: The Power of the Dependency Inversion Principle in Python

Omifarhan
3 min readDec 7, 2024

--

Introduction

In the world of software design, the SOLID principles are a set of guidelines that help developers create flexible, maintainable, and scalable systems. One of these principles, the Dependency Inversion Principle (DIP), shifts the focus from low-level details to high-level abstractions, ensuring that code remains flexible and decoupled. Understanding and applying DIP can significantly improve the robustness of your software architecture.

Understanding Dependency Inversion Principle (DIP)

The Dependency Inversion Principle is about reversing the typical direction of dependencies in a system. It states that high-level modules should not depend on low-level modules; instead, both should depend on abstractions (e.g., interfaces or abstract classes). Additionally, abstractions should not depend on details, but details should depend on abstractions.

This principle is crucial because it helps reduce the coupling between different parts of a system, making it easier to modify or extend the code without affecting the entire application. By depending on abstractions, we create a more flexible and testable system.

For example code, follow this repository — https://github.com/fermions75/SOLID-Principles

Python Example

Let’s consider an example of a notification system.

Violating DIP

In this example, we have a notification service that sends notifications via email. The high-level NotificationService class directly depends on a low-level EmailSender class.

class EmailSender:
def send(self, message):
print(f"Sending email with message: {message}")
class NotificationService:
def __init__(self):
self.email_sender = EmailSender()
def notify_by_email(self, message):
self.email_sender.send(message)
# Usage
email_sender = EmailSender()
notification_service = NotificationService(email_sender)
notification_service.notify("Hey how do you do?") # Works fine

The NotificationService class is tightly coupled to the EmailSender class. If we want to add a new way to send notifications, we must modify the NotificationService class. This violates the Dependency Inversion Principle.

Now, let’s say we want to add a new way to send notifications, for example, via SMS. We can add a new class for sending and modify the NotificationService class to accept the new sender. This violates the Open Close Principle.

class SMSSender:
def send(self, message):
print(f"Sending SMS with message: {message}")
class NotificationService:    def __init__(self):
self.email_sender = EmailSender()
self.sms_sender = SMSSender()
def notify_by_email(self, message):
self.email_sender.send(message)
def notify_by_sms(self, message):
self.sms_sender.send(message)

# Usage
notification_service = NotificationService()
notification_service.notify_by_email("Hello, via Email!")
notification_service.notify_by_sms("Hello, via SMS!")

Adhering to DIP

Let’s refactor the code to adhere to the Dependency Inversion Principle by introducing an abstraction.

from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, message):
pass
class EmailSender(Notifier):
def send(self, message):
print(f"Sending email with message: {message}")
class SmsSender(Notifier):
def send(self, message):
print(f"Sending SMS with message: {message}")
class NotificationService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def notify(self, message):
self.notifier.send(message)
# Usage
email_sender = EmailSender()
notification_service = NotificationService(email_sender)
notification_service.notify("Hello, World!")
sms_sender = SmsSender()
notification_service = NotificationService(sms_sender)
notification_service.notify("Hello, via SMS!")

In this refactored example:

  1. We introduced an abstract class Notifier that defines the send method.
  2. The EmailSender and SmsSender classes implement the Notifier interface.
  3. The NotificationService class depends on the Notifier abstraction rather than concrete implementations.

By adhering to DIP, we decouple the NotificationService from specific notification mechanisms, making extending the system (e.g., adding push notifications) easier without modifying the existing code.

Conclusion

The Dependency Inversion Principle is vital for creating modular, flexible, and testable software systems. By depending on abstractions rather than concrete implementations, developers can design easier to maintain and extend systems. Embracing DIP, along with other SOLID principles, can significantly enhance the quality and adaptability of your software projects.

For example code, follow this repository — https://github.com/fermions75/SOLID-Principles

--

--

Omifarhan
Omifarhan

No responses yet