Dependency Injection (DI) is the most common design pattern to implement Inversion of Control (IoC), where instead of a class creating its own dependencies, they’re injected from the outside. This decouples the creation of objects from business logic, improving modularity and testing.
NOTE
It’s like ordering coffee at a cafe instead of making it yourself. You specify what you need, and the barista prepares it for you.
Approach
On this post, I will explain how to use Manual Dependency Injection, where you explicitly manage service creation and wiring without a framework. This approach gives you full control over how dependencies are created and injected.
NOTE
There are other approaches like using a Dependency Injection Container (e.g., Spring, dependency-injector) or a Service Locator, which I will not cover but are worth exploring.
Implementation
To implement manual DI, I use two key mechanisms: a Service Factory for managing service creation and Constructor Injection for passing dependencies.
Service Factory
A Service Factory is a class with static methods that return instances of services. It provides a centralized place to create and manage service lifecycles, making it easy to implement singletons.
from user_service import UserService
from database_service import DatabaseService
class ServiceFactory:
_database_service = None
@staticmethod
def get_database_service():
# Ensure singleton instance
if ServiceFactory._database_service is None:
ServiceFactory._database_service = DatabaseService()
return ServiceFactory._database_service
@staticmethod
def get_user_service():
database_service = ServiceFactory.get_database_service()
return UserService(database_service)
TIP
You can use instance methods instead of static methods if you need multiple configurations (e.g., separate test and production setups), but static methods are simpler for most use cases.
Constructor Injection
For actually injecting dependencies into services, I use Constructor Injection, where dependencies are provided through a class constructor. This technique is explicit and ensures that an object is always in a valid state after construction.
from database_service import DatabaseService
class UserService:
def __init__(self, database_service: DatabaseService):
self.database_service = database_service
def get_user(self, user_id):
return self.database_service.fetch_user(user_id)
NOTE
Other injection techniques include Setter Injection (dependencies provided through setter methods) and Interface Injection (dependencies provided through an interface). Constructor injection is the most common and recommended approach.
Example Usage
Putting it all together, here’s how you would use them in your application:
from service_factory import ServiceFactory
def main():
"""Main entry point of the application."""
user_service = ServiceFactory.get_user_service()
user = user_service.get_user(1)
print(user)
if __name__ == "__main__":
main()
In this example, the main function retrieves the UserService from the ServiceFactory, which in turn gets the DatabaseService injected into it automatically. By pushing the dependency management to lower levels, the code remains clean and focused on business logic.
Conclusion
Manual Dependency Injection promotes decoupling and testability without external frameworks. Using a Service Factory and Constructor Injection gives you full control over dependencies, making your code more maintainable and flexible.