What I usually do in my applications is that I create all my services/dao/repo/clients using factory methods
class Service:
def init(self, db):
self._db = db
@classmethod
def from_env(cls):
return cls(db=PostgresDatabase.from_env())
And when I create app I do
service = Service.from_env()
what creates all dependencies
and in tests when I dont want to use real db I just do DI
service = Service(db=InMemoryDatabse())
I suppose that is quite far from clean/hex architecture since Service knows how to create a Database and knows which database type it creates (could be also InMemoryDatabse or MongoDatabase)
I guess that in clean/hex architecture I would have
class DatabaseInterface(ABC):
@abstractmethod
def get_user(self, user_id: int) -> User:
pass
import inject
class Service:
@inject.autoparams()
def __init__(self, db: DatabaseInterface):
self._db = db
And I would set up injector framework to do
# in app
inject.clear_and_configure(lambda binder: binder
.bind(DatabaseInterface, PostgresDatabase()))
# in test
inject.clear_and_configure(lambda binder: binder
.bind(DatabaseInterface, InMemoryDatabse()))
And my questions are:
So to answer the question the main difference between the Factory pattern and DI is how the object reference is obtained. With dependency injection as the name implies the reference is injected or given to your code. With Factory pattern your code must request the reference so your code fetches the object.
python-inject is a fast and simple to use python dependency injection framework. It uses decorators and descriptors to reference external dependencies. python-inject has been created to provide the pythonic way of dependency injection, utilizing specific Python functionality.
Dependency injection is a principle that helps to achieve an inversion of control. A dependency injection framework can significantly improve the flexibility of a language with static typing. Implementation of a dependency injection framework for a language with static typing is not something that one can do quickly.
There are several main goals in Dependency Injection technique, including (but not limited to):
The other thing to keep in mind is that we usually shall rely on abstractions, not implementations. I see a lot of people who use DI to inject only particular implementation. There's a big difference.
Because when you inject and rely on an implementation, there's no difference in what method we use to create objects. It just does not matter. For example, if you inject requests
without proper abstractions you would still require anything similar with the same methods, signatures, and return types. You would not be able to replace this implementation at all. But, when you inject fetch_order(order: OrderID) -> Order
it means that anything can be inside. requests
, database, whatever.
To sum things up:
What are the benefits of using inject?
The main benefit is that you don't have to assemble your dependencies manually. However, this comes with a huge cost: you are using complex, even magical, tools to solve problems. One day or another complexity will fight you back.
Is it worth to bother and use inject framework?
One more thing about inject
framework in particular. I don't like when objects where I inject something knows about it. It is an implementation detail!
How in a world Postcard
domain model, for example, knows this thing?
I would recommend to use punq
for simple cases and dependencies
for complex ones.
inject
also does not enforce a clean separation of "dependencies" and object properties. As it was said, one of the main goal of DI is to enforce stricter responsibilities.
In contrast, let me show how punq
works:
from typing_extensions import final
from attr import dataclass
# Note, we import protocols, not implementations:
from project.postcards.repository.protocols import PostcardsForToday
from project.postcards.services.protocols import (
SendPostcardsByEmail,
CountPostcardsInAnalytics,
)
@final
@dataclass(frozen=True, slots=True)
class SendTodaysPostcardsUsecase(object):
_repository: PostcardsForToday
_email: SendPostcardsByEmail
_analytics: CountPostcardInAnalytics
def __call__(self, today: datetime) -> None:
postcards = self._repository(today)
self._email(postcards)
self._analytics(postcards)
See? We even don't have a constructor. We declaratively define our dependencies and punq
will automatically inject them. And we do not define any specific implementations. Only protocols to follow. This style is called "functional objects" or SRP-styled classes.
Then we define the punq
container itself:
# project/implemented.py
import punq
container = punq.Container()
# Low level dependencies:
container.register(Postgres)
container.register(SendGrid)
container.register(GoogleAnalytics)
# Intermediate dependencies:
container.register(PostcardsForToday)
container.register(SendPostcardsByEmail)
container.register(CountPostcardInAnalytics)
# End dependencies:
container.register(SendTodaysPostcardsUsecase)
And use it:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
See? Now our classes have no idea who and how creates them. No decorators, no special values.
Read more about SRP-styled classes here:
Are there any other better ways of separating the domain from the outside?
You can use functional programming concepts instead of imperative ones. The main idea of function dependency injection is that you don't call things that relies on context you don't have. You schedule these calls for later, when the context is present. Here's how you can illustrate dependency injection with just simple functions:
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from words_app.logic import calculate_points
def view(request: HttpRequest) -> HttpResponse:
user_word: str = request.POST['word'] # just an example
points = calculate_points(user_words)(settings) # passing the dependencies and calling
... # later you show the result to user somehow
# Somewhere in your `word_app/logic.py`:
from typing import Callable
from typing_extensions import Protocol
class _Deps(Protocol): # we rely on abstractions, not direct values or types
WORD_THRESHOLD: int
def calculate_points(word: str) -> Callable[[_Deps], int]:
guessed_letters_count = len([letter for letter in word if letter != '.'])
return _award_points_for_letters(guessed_letters_count)
def _award_points_for_letters(guessed: int) -> Callable[[_Deps], int]:
def factory(deps: _Deps):
return 0 if guessed < deps.WORD_THRESHOLD else guessed
return factory
The only problem with this pattern is that _award_points_for_letters
will be hard to compose.
That's why we made a special wrapper to help the composition (it is a part of the returns
:
import random
from typing_extensions import Protocol
from returns.context import RequiresContext
class _Deps(Protocol): # we rely on abstractions, not direct values or types
WORD_THRESHOLD: int
def calculate_points(word: str) -> RequiresContext[_Deps, int]:
guessed_letters_count = len([letter for letter in word if letter != '.'])
awarded_points = _award_points_for_letters(guessed_letters_count)
return awarded_points.map(_maybe_add_extra_holiday_point) # it has special methods!
def _award_points_for_letters(guessed: int) -> RequiresContext[_Deps, int]:
def factory(deps: _Deps):
return 0 if guessed < deps.WORD_THRESHOLD else guessed
return RequiresContext(factory) # here, we added `RequiresContext` wrapper
def _maybe_add_extra_holiday_point(awarded_points: int) -> int:
return awarded_points + 1 if random.choice([True, False]) else awarded_points
For example, RequiresContext
has special .map
method to compose itself with a pure function. And that's it. As a result you have just simple functions and composition helpers with simple API. No magic, no extra complexity. And as a bonus everything is properly typed and compatible with mypy
.
Read more about this approach here:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With