Dependency Inversion Principle in SOLID Principles

savant aakash
4 min readJul 25, 2024

--

Dependency Inversion Principle (DIP) in the context of a food delivery application like Zomato. The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.

Here’s an example demonstrating the Dependency Inversion Principle:

```python
from abc import ABC, abstractmethod

# Abstractions

class OrderRepository(ABC):
@abstractmethod
def save(self, order):
pass

class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, order):
pass

class NotificationService(ABC):
@abstractmethod
def send_notification(self, user, message):
pass

# High-level module
class OrderService:
def __init__(self, order_repository: OrderRepository, payment_processor: PaymentProcessor, notification_service: NotificationService):
self.order_repository = order_repository
self.payment_processor = payment_processor
self.notification_service = notification_service

def place_order(self, order):
# Process the order
payment_result = self.payment_processor.process_payment(order)
if payment_result.is_successful:
self.order_repository.save(order)
self.notification_service.send_notification(order.user, “Your order has been placed successfully!”)
return True
else:
self.notification_service.send_notification(order.user, “Payment failed. Please try again.”)
return False

# Low-level modules (implementations)

class SQLOrderRepository(OrderRepository):
def save(self, order):
print(f”Saving order {order.id} to SQL database”)

class StripePaymentProcessor(PaymentProcessor):
def process_payment(self, order):
print(f”Processing payment for order {order.id} with Stripe”)
return PaymentResult(True)

class EmailNotificationService(NotificationService):
def send_notification(self, user, message):
print(f”Sending email to {user.email}: {message}”)

# Usage
order_repo = SQLOrderRepository()
payment_processor = StripePaymentProcessor()
notification_service = EmailNotificationService()

order_service = OrderService(order_repo, payment_processor, notification_service)

# Simulating an order
class Order:
def __init__(self, id, user):
self.id = id
self.user = user

class User:
def __init__(self, email):
self.email = email

class PaymentResult:
def __init__(self, is_successful):
self.is_successful = is_successful

user = User(“customer@example.com”)
order = Order(“12345”, user)

order_service.place_order(order)
```

In this example:

1. We define abstract base classes (`OrderRepository`, `PaymentProcessor`, `NotificationService`) that act as interfaces.

2. The high-level `OrderService` depends on these abstractions, not on concrete implementations.

3. We create concrete implementations (`SQLOrderRepository`, `StripePaymentProcessor`, `EmailNotificationService`) that inherit from the abstract base classes.

4. The `OrderService` is instantiated with specific implementations, but it doesn’t know or care about the details of these implementations.

This adheres to the Dependency Inversion Principle because:

- The high-level `OrderService` module depends on abstractions (`OrderRepository`, `PaymentProcessor`, `NotificationService`), not concrete implementations.
- The low-level modules (concrete implementations) also depend on these abstractions.
- We can easily swap out implementations without changing the `OrderService` code.

Benefits of this approach:

1. Flexibility: We can easily change the order storage mechanism, payment processor, or notification method without modifying the `OrderService` code.

2. Testability: We can easily mock the dependencies for unit testing.

3. Decoupling: The high-level and low-level modules are decoupled, making the system more maintainable and easier to evolve.

4. Extensibility: We can add new implementations (e.g., a new payment processor) without changing existing code.

For Zomato, this principle allows for great flexibility:

- Different restaurants could use different payment processors or notification services.
- The system could easily adapt to new technologies or services.
- It’s easier to implement A/B testing for different features.

For example, if Zomato wants to test a new push notification service alongside the email service:

```python
class PushNotificationService(NotificationService):
def send_notification(self, user, message):
print(f”Sending push notification to {user.device_id}: {message}”)

# We can easily create a new OrderService with the new notification service
push_notification_service = PushNotificationService()
new_order_service = OrderService(order_repo, payment_processor, push_notification_service)
```

By adhering to the Dependency Inversion Principle, Zomato can create a flexible, maintainable, and easily extensible system that can adapt to changing business needs and technological advancements in the food delivery industry.

Real-World Use Case: Ola (Ride-Sharing App)

Imagine you are developing a ride-sharing app like Ola. Here are some of the main features you need to handle:

1. **Ride Booking:** Booking different types of rides.
2. **Payment Processing:** Handling various payment methods (credit card, wallet, cash).
3. **Notification Service:** Sending notifications to users (SMS, email, push notifications).

**Dependency Inversion Principle in Ola Software:**

1. **High-Level Modules:**
— Modules that perform core functionalities, like booking rides and processing payments.
2. **Low-Level Modules:**
— Specific implementations of services, like SMS notifications or credit card payment processing.

**Implementing DIP:**

1. **Define Abstractions:**
— Create interfaces for the services needed by high-level modules.

```python
from abc import ABC, abstractmethod

class PaymentService(ABC):
@abstractmethod
def process_payment(self, amount):
pass

class NotificationService(ABC):
@abstractmethod
def send_notification(self, message, user):
pass
```

2. **Implement Low-Level Modules:**
— Implement the interfaces with specific details for each service.

```python
class CreditCardPayment(PaymentService):
def process_payment(self, amount):
print(f”Processing credit card payment of {amount}.”)

class WalletPayment(PaymentService):
def process_payment(self, amount):
print(f”Processing wallet payment of {amount}.”)

class SMSNotification(NotificationService):
def send_notification(self, message, user):
print(f”Sending SMS to {user}: {message}”)

class EmailNotification(NotificationService):
def send_notification(self, message, user):
print(f”Sending email to {user}: {message}”)
```

3. **High-Level Modules Depending on Abstractions:**
— High-level modules should use the abstractions (interfaces) rather than specific implementations.

```python
class RideBooking:
def __init__(self, payment_service: PaymentService, notification_service: NotificationService):
self.payment_service = payment_service
self.notification_service = notification_service

def book_ride(self, user, destination, amount):
print(f”Ride booked for {user} to {destination}.”)
self.payment_service.process_payment(amount)
self.notification_service.send_notification(“Your ride is booked!”, user)
```

4. **Injecting Dependencies:**
— Use dependency injection to pass the appropriate implementations to the high-level module.

```python
# Example usage
payment_service = CreditCardPayment()
notification_service = SMSNotification()
ride_booking = RideBooking(payment_service, notification_service)

user = “John Doe”
destination = “Airport”
amount = 100

ride_booking.book_ride(user, destination, amount)
```

**Why Dependency Inversion Principle is Important:**

- **Decoupling:** High-level modules are decoupled from low-level module implementations, making the system more flexible.
- **Flexibility:** You can easily switch out low-level modules (e.g., change from SMS to email notifications) without modifying the high-level modules.
- **Maintainability:** The system is easier to maintain and extend because changes in one part do not affect other parts.

**Analogy:**

Think of DIP like a restaurant where the chef (high-level module) depends on ingredients (abstractions) rather than specific suppliers (low-level modules). The chef can cook the same dish regardless of whether the ingredients come from Supplier A or Supplier B. Similarly, in software, high-level modules should rely on interfaces, not specific implementations, allowing for greater flexibility and easier changes.

By following the Dependency Inversion Principle in your Ola ride-sharing app, you ensure that your system is more modular, flexible, and easier to maintain, as high-level functionalities are decoupled from low-level implementation details.

--

--

savant aakash
savant aakash

Written by savant aakash

0 Followers

Introduction od oops in python

No responses yet