Skip to main content

πŸ”Œ The DIP Rule

β€œHigh-level modules should not depend on low-level modules. Both should depend on abstractions.”
Think about how you charge your phone:
  • You plug into a wall socket (abstraction) πŸ”Œ
  • You don’t wire directly to the power plant! ⚑
The socket is an interface - any charger that fits will work!
Simple Rule: Instead of creating dependencies directly, depend on interfaces/abstractions and get the real thing injected from outside!

🎯 What Does β€œInversion” Mean?

Traditional dependency (BAD):
HighLevel β†’ creates β†’ LowLevel
Inverted dependency (GOOD):
HighLevel β†’ depends on β†’ Abstraction ← implements ← LowLevel
The control is β€œinverted” - high-level code doesn’t create its dependencies, it receives them!

🚨 The Problem: Tight Coupling

❌ BAD: Creating Dependencies Inside

class MySQLDatabase:
    def connect(self):
        print("Connecting to MySQL...")
    
    def query(self, sql):
        print(f"MySQL executing: {sql}")
        return [{"id": 1, "name": "Alice"}]

class UserRepository:
    def __init__(self):
        # 🚨 Directly creates MySQL - tightly coupled!
        self.database = MySQLDatabase()
    
    def find_user(self, user_id):
        self.database.connect()
        return self.database.query(f"SELECT * FROM users WHERE id = {user_id}")

# Problems:
# 1. Can't switch to PostgreSQL without changing UserRepository
# 2. Can't test without a real MySQL database
# 3. Can't use a different database for different environments

# Want to switch to PostgreSQL? Must modify UserRepository!
# Want to test? Must have MySQL running!

βœ… GOOD: Depend on Abstraction

from abc import ABC, abstractmethod

# 1. Define the abstraction (interface)
class Database(ABC):
    @abstractmethod
    def connect(self):
        pass
    
    @abstractmethod
    def query(self, sql):
        pass

# 2. Implement concrete versions
class MySQLDatabase(Database):
    def connect(self):
        print("🐬 Connecting to MySQL...")
    
    def query(self, sql):
        print(f"🐬 MySQL: {sql}")
        return [{"id": 1, "name": "Alice"}]

class PostgreSQLDatabase(Database):
    def connect(self):
        print("🐘 Connecting to PostgreSQL...")
    
    def query(self, sql):
        print(f"🐘 PostgreSQL: {sql}")
        return [{"id": 1, "name": "Alice"}]

class MockDatabase(Database):
    """For testing!"""
    def connect(self):
        print("πŸ§ͺ Mock database connected")
    
    def query(self, sql):
        print(f"πŸ§ͺ Mock query: {sql}")
        return [{"id": 99, "name": "Test User"}]

# 3. Depend on abstraction, receive via injection
class UserRepository:
    def __init__(self, database: Database):  # 🎯 Accepts ANY Database!
        self.database = database
    
    def find_user(self, user_id):
        self.database.connect()
        return self.database.query(f"SELECT * FROM users WHERE id = {user_id}")

# 4. Usage - inject whatever you need!
print("=== Production with MySQL ===")
mysql_repo = UserRepository(MySQLDatabase())
mysql_repo.find_user(1)

print("\n=== Production with PostgreSQL ===")
postgres_repo = UserRepository(PostgreSQLDatabase())
postgres_repo.find_user(1)

print("\n=== Testing with Mock ===")
test_repo = UserRepository(MockDatabase())
test_repo.find_user(1)

# πŸŽ‰ Same UserRepository code, different databases!

πŸ“§ Real Example: Notification System

❌ BAD: Directly Creating Email Client

import smtplib

class NotificationService:
    def __init__(self):
        # 🚨 Directly creating SMTP - tight coupling!
        self.server = smtplib.SMTP('smtp.gmail.com', 587)
        self.server.login('[email protected]', 'password')
    
    def send(self, to, message):
        self.server.send_message(message, to_addrs=[to])

# Problems:
# - Can't switch to SendGrid, Mailgun, or AWS SES easily
# - Can't test without sending real emails!
# - Gmail credentials hardcoded

βœ… GOOD: Inject Email Provider

from abc import ABC, abstractmethod

class EmailProvider(ABC):
    @abstractmethod
    def send(self, to: str, subject: str, body: str):
        pass

class GmailProvider(EmailProvider):
    def __init__(self, username, password):
        self.username = username
        self.password = password
    
    def send(self, to, subject, body):
        print(f"πŸ“§ Gmail: Sending to {to}")
        print(f"   Subject: {subject}")
        # Actual SMTP logic here

class SendGridProvider(EmailProvider):
    def __init__(self, api_key):
        self.api_key = api_key
    
    def send(self, to, subject, body):
        print(f"πŸ“§ SendGrid: Sending to {to}")
        print(f"   Subject: {subject}")
        # SendGrid API logic here

class AWSEmailProvider(EmailProvider):
    def __init__(self, access_key, secret_key, region):
        self.region = region
    
    def send(self, to, subject, body):
        print(f"πŸ“§ AWS SES ({self.region}): Sending to {to}")
        print(f"   Subject: {subject}")
        # AWS SES logic here

class MockEmailProvider(EmailProvider):
    """For testing - doesn't send real emails!"""
    def __init__(self):
        self.sent_emails = []
    
    def send(self, to, subject, body):
        print(f"πŸ§ͺ Mock: Would send to {to}")
        self.sent_emails.append({"to": to, "subject": subject, "body": body})

class NotificationService:
    def __init__(self, email_provider: EmailProvider):  # 🎯 Inject provider
        self.email = email_provider
    
    def notify_user(self, user_email, message):
        self.email.send(user_email, "Notification", message)

# Usage - swap providers easily!
print("=== Using Gmail ===")
gmail_notifier = NotificationService(GmailProvider("user", "pass"))
gmail_notifier.notify_user("[email protected]", "Your order shipped!")

print("\n=== Using SendGrid ===")
sendgrid_notifier = NotificationService(SendGridProvider("api-key-123"))
sendgrid_notifier.notify_user("[email protected]", "Your order shipped!")

print("\n=== Testing ===")
mock = MockEmailProvider()
test_notifier = NotificationService(mock)
test_notifier.notify_user("[email protected]", "Test message")
print(f"Captured {len(mock.sent_emails)} emails!")  # Can verify emails!

πŸ’³ Real Example: Payment Processing

from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class PaymentResult:
    success: bool
    transaction_id: str
    message: str

class PaymentGateway(ABC):
    @abstractmethod
    def process(self, amount: float, card_token: str) -> PaymentResult:
        pass
    
    @abstractmethod
    def refund(self, transaction_id: str) -> PaymentResult:
        pass

class StripeGateway(PaymentGateway):
    def __init__(self, api_key):
        self.api_key = api_key
    
    def process(self, amount, card_token):
        print(f"πŸ’³ Stripe: Processing ${amount}")
        return PaymentResult(True, "stripe_tx_123", "Payment successful")
    
    def refund(self, transaction_id):
        print(f"πŸ’³ Stripe: Refunding {transaction_id}")
        return PaymentResult(True, transaction_id, "Refund successful")

class PayPalGateway(PaymentGateway):
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
    
    def process(self, amount, card_token):
        print(f"πŸ…ΏοΈ PayPal: Processing ${amount}")
        return PaymentResult(True, "paypal_tx_456", "Payment successful")
    
    def refund(self, transaction_id):
        print(f"πŸ…ΏοΈ PayPal: Refunding {transaction_id}")
        return PaymentResult(True, transaction_id, "Refund successful")

class SquareGateway(PaymentGateway):
    def __init__(self, access_token):
        self.access_token = access_token
    
    def process(self, amount, card_token):
        print(f"⬜ Square: Processing ${amount}")
        return PaymentResult(True, "square_tx_789", "Payment successful")
    
    def refund(self, transaction_id):
        print(f"⬜ Square: Refunding {transaction_id}")
        return PaymentResult(True, transaction_id, "Refund successful")

class MockPaymentGateway(PaymentGateway):
    """For testing!"""
    def __init__(self, should_succeed=True):
        self.should_succeed = should_succeed
        self.processed = []
    
    def process(self, amount, card_token):
        self.processed.append(amount)
        if self.should_succeed:
            return PaymentResult(True, "mock_tx_000", "Mock success")
        return PaymentResult(False, "", "Mock failure")
    
    def refund(self, transaction_id):
        return PaymentResult(True, transaction_id, "Mock refund")

# Order service depends on abstraction
class OrderService:
    def __init__(self, payment_gateway: PaymentGateway):  # 🎯 Injected!
        self.payment = payment_gateway
    
    def checkout(self, order_total, card_token):
        print(f"πŸ›’ Processing order for ${order_total}")
        result = self.payment.process(order_total, card_token)
        
        if result.success:
            print(f"βœ… Order confirmed! Transaction: {result.transaction_id}")
        else:
            print(f"❌ Order failed: {result.message}")
        
        return result

# πŸŽ‰ Easy to switch payment providers!

# Production - use Stripe
order_service = OrderService(StripeGateway("sk_live_xxx"))
order_service.checkout(99.99, "tok_visa")

# Different environment - use PayPal
print()
paypal_service = OrderService(PayPalGateway("client_id", "secret"))
paypal_service.checkout(49.99, "paypal_tok")

# Testing - use mock
print("\n=== Testing ===")
mock = MockPaymentGateway(should_succeed=True)
test_service = OrderService(mock)
test_service.checkout(100.00, "test_token")
print(f"Total processed in tests: ${sum(mock.processed)}")

πŸ—οΈ Dependency Injection Patterns

1️⃣ Constructor Injection (Most Common)

class UserService:
    def __init__(self, user_repo: UserRepository, email_service: EmailService):
        self.users = user_repo
        self.email = email_service

2️⃣ Setter Injection

class UserService:
    def __init__(self):
        self._user_repo = None
    
    def set_repository(self, repo: UserRepository):
        self._user_repo = repo

3️⃣ Method Injection

class UserService:
    def create_user(self, data, email_service: EmailService):
        # Email service passed only when needed
        email_service.send_welcome(data['email'])

🏭 Simple DI Container

class Container:
    """Simple dependency injection container"""
    
    def __init__(self):
        self._services = {}
    
    def register(self, interface, implementation):
        self._services[interface] = implementation
    
    def resolve(self, interface):
        if interface not in self._services:
            raise KeyError(f"Service {interface} not registered")
        return self._services[interface]

# Setup container
container = Container()

# Register implementations
if ENVIRONMENT == "production":
    container.register("database", MySQLDatabase())
    container.register("email", SendGridProvider("api_key"))
    container.register("payment", StripeGateway("stripe_key"))
else:
    container.register("database", MockDatabase())
    container.register("email", MockEmailProvider())
    container.register("payment", MockPaymentGateway())

# Resolve dependencies
db = container.resolve("database")
email = container.resolve("email")

# Create services with resolved dependencies
user_repo = UserRepository(db)
notification = NotificationService(email)

πŸ“Š DIP Benefits Visualization

WITHOUT DIP (Tight Coupling):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         OrderService            β”‚
β”‚                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚   StripeGateway         │◄───┼── Hardcoded inside!
β”‚  β”‚   (concrete class)      β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                 β”‚
β”‚  - Can't change gateway         β”‚
β”‚  - Can't test without Stripe    β”‚
β”‚  - Coupled to implementation    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

WITH DIP (Loose Coupling):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         OrderService            β”‚
β”‚                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚   PaymentGateway        │◄───┼── Interface (abstraction)
β”‚  β”‚   (abstract interface)  β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β–²
           β”‚ implements
    β”Œβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚      β”‚      β”‚             β”‚
β”Œβ”€β”€β”€β”΄β”€β”€β”€β” β”Œβ”΄β”€β”€β”€β”€β” β”Œβ”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Stripe β”‚ β”‚PayPalβ”‚ β”‚  Square β”‚ β”‚MockPay β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜

- Easy to swap implementations
- Easy to test with mocks
- Decoupled from details

πŸ§ͺ Practice Exercise

This weather app violates DIP. Fix it!
import requests

class WeatherApp:
    def __init__(self):
        # 🚨 Directly coupled to specific API!
        self.api_key = "hardcoded_key"
        self.api_url = "https://api.openweathermap.org/data/2.5/weather"
    
    def get_weather(self, city):
        # 🚨 Can't test without calling real API!
        response = requests.get(
            self.api_url,
            params={"q": city, "appid": self.api_key}
        )
        data = response.json()
        return {
            "city": city,
            "temp": data["main"]["temp"],
            "description": data["weather"][0]["description"]
        }
    
    def display_weather(self, city):
        weather = self.get_weather(city)
        print(f"Weather in {weather['city']}: {weather['temp']}Β°K")
        print(f"Description: {weather['description']}")

# Problems:
# - Can't switch to different weather API (WeatherStack, AccuWeather)
# - Can't test without internet/real API calls
# - API key hardcoded

πŸ“ Key Takeaways

Without DIPWith DIP
new keyword everywhereInject dependencies
Tied to specific implementationsTied to abstractions
Hard to testEasy to mock
Hard to changeEasy to swap
Tight couplingLoose coupling

πŸŽ‰ SOLID Complete!

Congratulations! You’ve learned all five SOLID principles:

πŸƒ Next: Design Patterns

Now that you understand SOLID, let’s learn the classic design patterns that solve common problems!

Continue to Design Patterns β†’

Learn the proven solutions to recurring design problems!