Constructor Injection¶
Constructor injection is the primary mechanism by which WD-DI manages and provides dependencies to your services. When a service is resolved, WD-DI inspects its constructor (__init__
method), identifies the required dependencies based on type hints, and automatically supplies instances of those dependencies.
This pattern promotes clear, testable, and maintainable code by making a class's dependencies explicit in its constructor signature.
How It Works¶
- Service Registration: You register your services (e.g.,
DatabaseService
,UserService
) with theServiceCollection
, specifying their lifetimes. - Type Hinting: Your service constructors must use type hints for their parameters (e.g.,
db_service: DatabaseService
). WD-DI uses these hints to know which registered service to inject. - Resolution: When you request a service (e.g.,
provider.get_service(UserService)
), WD-DI:- Finds the registered entry for
UserService
. - Inspects its
__init__
method. - Sees it needs a
DatabaseService
. - Resolves an instance of
DatabaseService
(according to its registered lifetime). - Instantiates
UserService
, passing theDatabaseService
instance into its constructor. - Returns the fully constructed
UserService
instance.
- Finds the registered entry for
Example¶
Let's consider a UserService
that depends on a DatabaseService
.
from wd.di import ServiceCollection
services = ServiceCollection()
# Assume DatabaseService is defined and registered, for example:
# (This could be an interface IUserService or a concrete class)
class IDatabaseService: # Using an interface for demonstration
def query(self, sql: str):
raise NotImplementedError
@services.singleton(IDatabaseService) # Registering concrete type for the interface
class ConcreteDatabaseService(IDatabaseService):
def __init__(self):
print("ConcreteDatabaseService created (singleton)")
def query(self, sql: str):
print(f"Executing query: {sql}")
return [{"id": 1, "name": "Test User"}] # Dummy data
# UserService depends on IDatabaseService
@services.singleton() # UserService itself can be a singleton or other lifetime
class UserService:
def __init__(self, db_service: IDatabaseService): # Dependency is type-hinted
print("UserService created, injecting IDatabaseService.")
self.db = db_service
def get_user_data(self, user_id: int):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# --- Usage ---
# Build the service provider
provider = services.build_service_provider()
# Resolve the UserService
# WD-DI will automatically create and inject ConcreteDatabaseService
user_service = provider.get_service(UserService)
# Use the service
user_data = user_service.get_user_data(user_id=1)
print(f"User data retrieved: {user_data}")
# If you resolve IDatabaseService directly, you get the same singleton instance
db_instance = provider.get_service(IDatabaseService)
print(f"Is user_service.db the same as db_instance? {user_service.db is db_instance}")
# Expected Output:
# ConcreteDatabaseService created (singleton)
# UserService created, injecting IDatabaseService.
# Executing query: SELECT * FROM users WHERE id = 1
# User data retrieved: [{'id': 1, 'name': 'Test User'}]
# Is user_service.db the same as db_instance? True
In this example:
* ConcreteDatabaseService
is registered as a singleton for the IDatabaseService
interface.
* UserService
declares its dependency on IDatabaseService
in its constructor.
* When UserService
is resolved, WD-DI provides the singleton instance of ConcreteDatabaseService
.
Benefits of Constructor Injection¶
- Explicit Dependencies: A class's dependencies are clearly listed in its constructor signature, making the class's requirements easy to understand.
- Improved Testability: When unit testing, you can easily pass mock or stub implementations of dependencies directly to the constructor, isolating the class under test. For example:
# --- Defining services (simplified for testability example) --- from abc import ABC, abstractmethod class IEmailService(ABC): @abstractmethod def send_email(self, recipient: str, subject: str, body: str): pass class UserService: def __init__(self, email_service: IEmailService): self.email_service = email_service def notify_user(self, user_id: str, message: str): # In a real app, fetch user's email, etc. email_address = f"{user_id}@example.com" self.email_service.send_email(email_address, "Notification", message) # --- Test with a mock --- class MockEmailService(IEmailService): def __init__(self): self.sent_emails = [] def send_email(self, recipient: str, subject: str, body: str): print(f"MOCK: Sending email to {recipient} - Subject: {subject}") self.sent_emails.append({"to": recipient, "subject": subject, "body": body}) def test_user_service_notification(): mock_mailer = MockEmailService() # Manually inject the mock when creating UserService for the test user_service_for_test = UserService(email_service=mock_mailer) user_service_for_test.notify_user("testuser", "Your item has shipped!") assert len(mock_mailer.sent_emails) == 1 assert mock_mailer.sent_emails[0]["to"] == "testuser@example.com" assert mock_mailer.sent_emails[0]["body"] == "Your item has shipped!" print("Test passed: UserService correctly used the mock email service.") # To run the test (typically done by a test runner like pytest): # test_user_service_notification()
- Loose Coupling: Classes don't create their dependencies; they receive them. This reduces coupling between components.
- Compile-Time Safety (with Type Hints): While Python is dynamically typed, type hints used for DI allow static analysis tools (like MyPy) to catch potential type mismatches early.
- Readability and Maintainability: Makes the flow of dependencies through your application easier to trace and manage.
When to use:
Always prefer constructor injection for mandatory dependencies. It's the cleanest and most straightforward way to implement Inversion of Control and Dependency Injection. WD-DI is designed primarily around this pattern.