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('user@gmail.com', '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("customer@example.com", "Your order shipped!")

print("\n=== Using SendGrid ===")
sendgrid_notifier = NotificationService(SendGridProvider("api-key-123"))
sendgrid_notifier.notify_user("customer@example.com", "Your order shipped!")

print("\n=== Testing ===")
mock = MockEmailProvider()
test_notifier = NotificationService(mock)
test_notifier.notify_user("test@test.com", "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:

S - Single Responsibility

One class, one job

O - Open/Closed

Add features without changing code

L - Liskov Substitution

Children replace parents

I - Interface Segregation

Many small interfaces

D - Dependency Inversion

Depend on abstractions

🏃 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!