π 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! β‘
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):Copy
HighLevel β creates β LowLevel
Copy
HighLevel β depends on β Abstraction β implements β LowLevel
π¨ The Problem: Tight Coupling
β BAD: Creating Dependencies Inside
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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)
Copy
class UserService:
def __init__(self, user_repo: UserRepository, email_service: EmailService):
self.users = user_repo
self.email = email_service
2οΈβ£ Setter Injection
Copy
class UserService:
def __init__(self):
self._user_repo = None
def set_repository(self, repo: UserRepository):
self._user_repo = repo
3οΈβ£ Method Injection
Copy
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
Copy
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
Copy
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
Challenge: Fix the Weather App
Challenge: Fix the Weather App
This weather app violates DIP. Fix it!
Copy
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 DIP | With DIP |
|---|---|
new keyword everywhere | Inject dependencies |
| Tied to specific implementations | Tied to abstractions |
| Hard to test | Easy to mock |
| Hard to change | Easy to swap |
| Tight coupling | Loose 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!