Méthodes d'usine vs framework d'injection en Python - qu'est-ce qui est plus propre?


9

Ce que je fais habituellement dans mes applications, c'est que je crée tous mes services / dao / repo / clients en utilisant des méthodes d'usine

class Service:
    def init(self, db):
        self._db = db

    @classmethod
    def from_env(cls):
        return cls(db=PostgresDatabase.from_env())

Et quand je crée une application, je le fais

service = Service.from_env()

ce qui crée toutes les dépendances

et dans les tests quand je ne veux pas utiliser de vrai db je fais juste DI

service = Service(db=InMemoryDatabse())

Je suppose que c'est assez loin de l'architecture propre / hexadécimale car le service sait comment créer une base de données et sait quel type de base de données il crée (pourrait également être InMemoryDatabse ou MongoDatabase)

Je suppose que dans une architecture propre / hexadécimale j'aurais

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

Et je mettrais en place un cadre d'injecteur pour faire

# in app
inject.clear_and_configure(lambda binder: binder
                           .bind(DatabaseInterface, PostgresDatabase()))

# in test
inject.clear_and_configure(lambda binder: binder
                           .bind(DatabaseInterface, InMemoryDatabse()))

Et mes questions sont:

  • Mon chemin est-il vraiment mauvais? N'est-ce plus une architecture propre?
  • Quels sont les avantages de l'injection?
  • Vaut-il la peine de déranger et d'utiliser le framework inject?
  • Existe-t-il d'autres meilleurs moyens de séparer le domaine de l'extérieur?

Réponses:


1

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 requestssans 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) -> Ordercela 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 injectcadre 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 Postcardmodèle de domaine mondial , par exemple, sait cette chose?

Je recommanderais d'utiliser punqpour des cas simples et dependenciescomplexes.

injectn'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 punqfonctionne:

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 punqles 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 punqconteneur 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_letterssera 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, RequiresContexta une .mapmé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:


0

L'exemple initial est assez proche d'un "bon" nettoyage / hex. Ce qui manque, c'est l'idée d'une racine de composition, et vous pouvez faire un nettoyage / hexadécimal sans infrastructure d'injecteur. Sans cela, vous feriez quelque chose comme:

class Service:
    def __init__(self, db):
        self._db = db

# In your app entry point:
service = Service(PostGresDb(config.host, config.port, config.dbname))

qui passe par DI pur / vanille / pauvre, en fonction de qui vous parlez. Une interface abstraite n'est pas absolument nécessaire, car vous pouvez compter sur le typage canard ou le typage structurel.

Que vous souhaitiez ou non utiliser un framework DI est une question d'opinion et de goût, mais il existe d'autres alternatives plus simples à injecter comme punq que vous pourriez envisager, si vous choisissez de suivre cette voie.

https://www.cosmicpython.com/ est une bonne ressource qui examine ces problèmes en profondeur.


0

vous voudrez peut-être utiliser une base de données différente et vous voulez avoir la flexibilité de le faire de manière simple, pour cette raison, je considère l'injection de dépendances comme une meilleure façon de configurer votre service

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.