Dependency Injection (DI) and Inversion of Control (IoC) are key design patterns that can significantly improve the structure and maintainability of web applications. They help in managing dependencies and controlling the flow of applications, making the code more modular and testable. PyNest is an open-source project that brings these concepts into the Python web development space, offering a practical approach to applying DI and IoC.
Understanding DI and IoC
Dependency Injection is a technique where components receive their dependencies from an external source rather than creating them internally. This approach promotes a modular architecture, making the code easier to manage and test.
Inversion of Control, in addition to that, refers to transferring the control of objects or portions of a program to a container or framework. It’s about taking the control away from your objects to make them more decoupled and easier to manage.
Example —
Imagine you’re building a web application with a service layer that needs to access a database. Without DI, the service might directly instantiate a database connection, making it tightly coupled to a specific database implementation.
class UserService:
def __init__(self):
self.db = Database() # Direct instantiation
def get_user(self, user_id):
return self.db.get_user(user_id)
With DI, the database connection (or a repository using it) is passed into the service as a parameter, making the service agnostic to the database implementation and easier to test.
class UserService:
def __init__(self, db):
self.db = db # Dependency injected, voila!
def get_user(self, user_id):
return self.db.get_user(user_id)
In this example, UserService
doesn't need to know which database system it's using or how the database connection is established. This decouples the service from the database layer, allowing for greater flexibility and easier testing.
Another exited concept that makes the DI so powerful is the Singelton pattern.
Singleton Pattern in the Context of DI
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This pattern can be particularly useful in web applications for managing resources that are expensive to create or need to be shared, such as database connections or configuration managers.
When combining the Singleton pattern with Dependency Injection, the DI framework typically manages the lifecycle of the singleton instance. This ensures that whenever a component requests a dependency that is defined as a singleton, the DI framework supplies the same instance.
Introducing PyNest
PyNest is a Python package that provides a straightforward way to implement DI and IoC in your web applications. It’s designed to be simple to use, focusing on reducing boilerplate code and making the development process more efficient.
PyNest’s Approach to DI
PyNest uses decorators to enable DI, allowing developers to annotate their classes and functions. This makes it clear where dependencies are being injected, improving code readability and maintainability.
Practical Applications
Using PyNest can help refactor tightly coupled components in a web application into a more loosely coupled architecture. This not only facilitates easier unit testing but also improves the application’s adaptability to changing requirements.
Let’s imagen that we are building an application for managing library. We will focus on the Book resource.
The first component we will create is the Provider.
Injectable Providers
Providers are a broad category that includes services, repositories, factories, helpers, and more. They are typically used to encapsulate business logic, database access, or any functionality that might be shared across different parts of the application. In DI systems, providers are injected into consumers (like Controllers or other services) that need them.
In our case, the book service manages the database access —
from nest import Injectable
from providers.database import DatabaseRepository # DatabaseRepository is also an injectable that follows the repository pattern
@Injectable
class BookService:
def __init__(self, db: DatabaseRepository):
self.db = db # Inject Database into BookService
def get_book(self, book_id: str):
return self.db.get_book(book_id)
Here, BookService
is marked as @Injectable
, indicating that it can be a dependency that other parts of the application may require. The DI framework manages the lifecycle and dependencies of these providers.
The next part of our module will be the controller —
Controllers
In the context of web applications, Controllers are responsible for handling incoming requests and returning responses to the client. They play a crucial role in managing the interaction between the user and the server. For a Python framework implementing DI, similar to how NestJS operates, Controllers would be defined classes where specific routes can trigger associated methods.
from nest import Controller, Get
from .book_service import BookService
@Controller('/users')
class BookController:
def __init__(self, book_service: BookService):
self.book_service = book_service
@Get('/:book_id')
def get_book(self, book_id: str):
return self.book_service.get_book(book_id)
In this example, the BookController
handles requests related to books. The @Controller
decorator helps define the routes at the class level, making the methods within the class respond to HTTP requests that match the given routes. In this example, the controller injects the book service from the DI framework.
The last part is the Module object —
Modules
Modules are an organizational framework that groups related components together. Each module is a cohesive block that encapsulates a certain set of capabilities and configuration. This architecture helps in maintaining a clear structure and promotes separation of concerns.
from nest.core import Module
from .book_controller import BookController
from .book_service import BookService
from providers.db import DatabaseRepository
@Module(
controllers: [BookController],
providers: [BookService, DatabaseRepository],
export: [BookService]
)
class UserModule:
pass
In the BookModule
, both BookController
and BookService
are included. It is also included DatabaseRepository
by declaring it in the provider's list. This module can be imported by the main application module, thereby organizing the codebase into a modular and manageable structure.
Visualizing PyNest’s DI Architecture
To bring our discussion to life, let’s visualize how PyNest manages dependencies with the help of the following architecture diagram:
This diagram encapsulates the dependency flow within a PyNest application, illustrating the modular approach and the decoupled nature of components facilitated by dependency injection (DI).
IoC Container
At the core, we have the IoC Container that uses the injector
packacge for managing object creation and injecting dependencies where they are needed. for further reading — PynestContainer
Injector — The injector
is a Python dependency injection framework, inspired by Guice, that aims to provide a Pythonic API with an emphasis on simplicity and support for static type checking. It avoids global state and encourages compartmentalized, decoupled code through the use of modules and injectable classes.
DI Framework
The DI Framework layer shows how @Injectable
components like DBRepository
and BookService
are wired together. The Inject
arrows highlight the direction of dependency resolution, ensuring that BookService
does not have to know the specifics of the DBRepository
.
Resources
- Asynchronous Magic: PyNest and SQLAlchemy 2.0 Drive a 25% Improvement in Python Apps Performance
- Beyond FastAPI: The Evolution of Python Microservices in 2024 with PyNest
- PyNest on PyPI: https://pypi.org/project/pynest-api
- Official Documentation: https://pythonnest.github.io/PyNest/
- GitHub Repository: https://github.com/PythonNest/PyNest