La technique d'injection de dépendances a plusieurs objectifs principaux, notamment (mais sans s'y limiter):
- Abaissement du couplage entre les parties de votre système. De cette façon, vous pouvez changer chaque pièce avec moins d'effort. Voir "Haute cohésion, faible couplage"
- Pour appliquer des règles plus strictes sur les responsabilités. Une entité ne doit faire qu'une seule chose à son niveau d'abstraction. D'autres entités doivent être définies comme des dépendances de celle-ci. Voir "IoC"
- Meilleure expérience de test. Les dépendances explicites vous permettent de bloquer différentes parties de votre système avec un comportement de test primitif qui a la même API publique que votre code de production. Voir "Mocks arent 'stubs"
L'autre chose à garder à l'esprit est que nous devons généralement compter sur des abstractions, pas sur des implémentations. Je vois beaucoup de gens qui utilisent DI pour injecter uniquement une implémentation particulière. Il y a une grande différence.
Parce que lorsque vous injectez et utilisez une implémentation, il n'y a aucune différence dans la méthode que nous utilisons pour créer des objets. Cela n'a pas d'importance. Par exemple, si vous injectez requests
sans abstractions appropriées, vous aurez toujours besoin de quelque chose de similaire avec les mêmes méthodes, signatures et types de retour. Vous ne pourriez pas du tout remplacer cette implémentation. Mais, quand vous vous injectez, fetch_order(order: OrderID) -> Order
cela signifie que tout peut être à l'intérieur. requests
, base de données, peu importe.
Pour résumer:
Quels sont les avantages de l'injection?
Le principal avantage est que vous n'avez pas à assembler vos dépendances manuellement. Cependant, cela a un coût énorme: vous utilisez des outils complexes, voire magiques, pour résoudre les problèmes. Un jour ou l'autre, la complexité vous repoussera.
Vaut-il la peine de déranger et d'utiliser le framework inject?
Une dernière chose à propos du inject
cadre en particulier. Je n'aime pas quand les objets où j'injecte quelque chose le savent. C'est un détail d'implémentation!
Comment dans un Postcard
modèle de domaine mondial , par exemple, sait cette chose?
Je recommanderais d'utiliser punq
pour des cas simples et dependencies
complexes.
inject
n'applique pas non plus une séparation nette des "dépendances" et des propriétés des objets. Comme il a été dit, l'un des principaux objectifs de l'ID est d'imposer des responsabilités plus strictes.
En revanche, permettez-moi de montrer comment punq
fonctionne:
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)
Voir? Nous n'avons même pas de constructeur. Nous définissons de manière déclarative nos dépendances et punq
les injectons automatiquement. Et nous ne définissons aucune implémentation spécifique. Seuls les protocoles à suivre. Ce style est appelé "objets fonctionnels" ou classes de style SRP .
Ensuite, nous définissons le punq
conteneur lui-même:
# 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)
Et utilisez-le:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Voir? Maintenant, nos classes n'ont aucune idée de qui et comment les crée. Pas de décorateurs, pas de valeurs spéciales.
En savoir plus sur les classes de style SRP ici:
Existe-t-il d'autres meilleurs moyens de séparer le domaine de l'extérieur?
Vous pouvez utiliser des concepts de programmation fonctionnelle au lieu de concepts impératifs. L'idée principale de l'injection de dépendance de fonction est que vous n'appelez pas des choses qui dépendent du contexte que vous n'avez pas. Vous planifiez ces appels pour plus tard, lorsque le contexte est présent. Voici comment illustrer l'injection de dépendances avec des fonctions simples:
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
Le seul problème avec ce modèle est qu'il _award_points_for_letters
sera difficile à composer.
C'est pourquoi nous avons fait un emballage spécial pour aider à la composition (il fait partie du 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
Par exemple, RequiresContext
a une .map
méthode spéciale pour se composer avec une fonction pure. Et c'est tout. En conséquence, vous disposez de fonctions simples et d'aides à la composition avec une API simple. Pas de magie, pas de complexité supplémentaire. Et en prime, tout est correctement tapé et compatible avec mypy
.
En savoir plus sur cette approche ici: