What is Dependency Injection?¶
Dependency Injection (DI) is a design pattern that helps you write more maintainable, testable, and flexible code. This guide explains what DI is, why it's useful, and how InjectQ implements it.
🎯 What is Dependency Injection?¶
Dependency Injection is a technique where objects receive their dependencies from an external source rather than creating them internally.
Without Dependency Injection¶
class UserService:
def __init__(self):
# Service creates its own dependencies
self.db = Database() # ❌ Tight coupling
self.cache = Cache() # ❌ Hard to test
def get_user(self, user_id):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
Problems: - Hard to test (can't mock dependencies) - Hard to change implementations - Tight coupling between classes - Difficult to reuse in different contexts
With Dependency Injection¶
class UserService:
def __init__(self, db: Database, cache: Cache):
# Dependencies are injected
self.db = db
self.cache = cache
# Somewhere else (composition root)
db = Database()
cache = Cache()
user_service = UserService(db, cache) # ✅ Loose coupling
Benefits: - Easy to test (can inject mocks) - Easy to change implementations - Loose coupling between classes - Highly reusable components
🏗️ The Dependency Injection Container¶
A DI Container is a framework that automatically manages dependency resolution and injection.
Manual Dependency Resolution¶
# Without a container - manual wiring
def create_user_service():
config = DatabaseConfig("postgresql://...")
db = Database(config)
cache = RedisCache("redis://...")
logger = Logger("user_service")
return UserService(db, cache, logger)
# Usage
service = create_user_service()
Problems: - Repetitive boilerplate code - Error-prone manual wiring - Hard to maintain as dependencies grow
With a DI Container¶
from injectq import injectq, inject
# Container automatically wires dependencies
container = injectq
# Bind implementations
container.bind(DatabaseConfig, DatabaseConfig)
container.bind(Database, Database)
container.bind(Cache, RedisCache)
# Usage - automatic resolution
@inject
def process_user(service: UserService):
# All dependencies automatically injected
pass
process_user() # No manual wiring needed!
🎭 Types of Dependency Injection¶
1. Constructor Injection (Recommended)¶
Dependencies are passed through the constructor:
Pros: - Dependencies are explicit and clear - Immutable after construction - Easy to test - Fail fast if dependencies are missing
2. Property Injection¶
Dependencies are set via properties:
class UserService:
def __init__(self):
self._db = None
self._cache = None
@property
def db(self):
return self._db
@db.setter
def db(self, value):
self._db = value
Pros: - Can change dependencies at runtime - Optional dependencies possible
Cons: - Dependencies not guaranteed to be set - Harder to test - Less explicit
3. Method Injection¶
Dependencies are passed to specific methods:
class UserService:
def process_user(self, user_id: int, db: Database):
# db is injected only for this method
pass
Pros: - Fine-grained control - Dependencies only where needed
Cons: - Verbose - Easy to forget to pass dependencies
🔄 Inversion of Control (IoC)¶
Inversion of Control is the principle behind DI. Instead of your code controlling dependency creation, the container controls it.
Traditional Control Flow¶
Inverted Control Flow¶
🎯 Benefits of Dependency Injection¶
1. Testability¶
def test_user_service():
# Easy to inject mocks
mock_db = MockDatabase()
mock_cache = MockCache()
service = UserService(mock_db, mock_cache)
# Test the service in isolation
result = service.get_user(1)
assert result is not None
2. Flexibility¶
# Easy to swap implementations
if environment == "production":
container.bind(Database, PostgreSQLDatabase)
elif environment == "testing":
container.bind(Database, InMemoryDatabase)
else:
container.bind(Database, SQLiteDatabase)
3. Maintainability¶
# Adding a new dependency is easy
class UserService:
def __init__(self, db: Database, cache: Cache, logger: Logger):
self.db = db
self.cache = cache
self.logger = logger # New dependency
4. Separation of Concerns¶
Each class focuses on its single responsibility:
class UserService: # Business logic
class Database: # Data persistence
class Cache: # Caching
class Logger: # Logging
5. Reusability¶
Components can be reused in different contexts:
🚨 Common Anti-Patterns¶
1. Service Locator¶
Problems: - Dependencies not explicit - Harder to test - Tightly coupled to locator
2. Factory Overload¶
class UserServiceFactory:
def create(self):
db = DatabaseFactory.create()
cache = CacheFactory.create()
return UserService(db, cache) # ❌ Manual wiring everywhere
Problems: - Boilerplate code - Error-prone - Hard to maintain
3. Circular Dependencies¶
class A:
def __init__(self, b: B):
self.b = b
class B:
def __init__(self, a: A): # ❌ Circular dependency
self.a = a
Problems: - Impossible to resolve - Indicates poor design
🏆 Best Practices¶
1. Use Constructor Injection¶
# ✅ Good
class UserService:
def __init__(self, db: Database, cache: Cache):
self.db = db
self.cache = cache
2. Depend on Abstractions¶
# ✅ Good - depend on interface/protocol
class UserService:
def __init__(self, repository: IUserRepository):
self.repository = repository
3. Single Responsibility¶
# ✅ Good - one reason to change
class UserService:
def __init__(self, repository: IUserRepository):
self.repository = repository
def get_user(self, id: int):
return self.repository.get_by_id(id)
4. Explicit Dependencies¶
# ✅ Good - all dependencies visible
@inject
def process_order(
order_service: OrderService,
payment_service: PaymentService,
notification_service: NotificationService
):
pass
🎉 Summary¶
Dependency Injection is a powerful pattern that:
- Improves testability by allowing easy mocking
- Increases flexibility by enabling easy implementation swaps
- Enhances maintainability by reducing coupling
- Promotes reusability by creating focused components
- Enables better architecture through clear separation of concerns
InjectQ makes DI easy by providing:
- Multiple injection patterns (@inject
, dict-like, manual)
- Automatic dependency resolution
- Powerful scoping mechanisms
- Framework integrations
- Testing utilities
Ready to dive deeper? Check out the container pattern next!