Factory-Methoden vs Inject-Framework in Python - was ist sauberer?

9

Was ich normalerweise in meinen Anwendungen mache, ist, dass ich alle meine Dienste / dao / repo / clients mit Factory-Methoden erstelle

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

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

Und wenn ich eine App erstelle, mache ich das

service = Service.from_env()

was schafft alle Abhängigkeiten

und in Tests, wenn ich keine echte Datenbank verwenden möchte, mache ich einfach DI

service = Service(db=InMemoryDatabse())

Ich nehme an, das ist ziemlich weit von einer Clean / Hex-Architektur entfernt, da Service weiß, wie eine Datenbank erstellt wird und welcher Datenbanktyp erstellt wird (könnte auch InMemoryDatabse oder MongoDatabase sein).

Ich denke, dass ich in Clean / Hex-Architektur hätte

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

Und ich würde ein Injektor-Framework einrichten, um dies zu tun

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

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

Und meine Fragen sind:

  • Ist mein Weg wirklich schlecht? Ist es nicht mehr eine saubere Architektur?
  • Was sind die Vorteile der Injektion?
  • Lohnt es sich, das Inject-Framework zu verwenden?
  • Gibt es andere bessere Möglichkeiten, die Domain von außen zu trennen?
Ala Głowacka
quelle

Antworten:

1

Es gibt mehrere Hauptziele in der Abhängigkeitsinjektionstechnik, einschließlich (aber nicht beschränkt auf):

  • Kopplung zwischen Teilen Ihres Systems absenken. Auf diese Weise können Sie jedes Teil mit weniger Aufwand ändern. Siehe "Hohe Kohäsion, niedrige Kopplung"
  • Strengere Regeln für Verantwortlichkeiten durchsetzen. Eine Entität darf auf ihrer Abstraktionsebene nur eines tun. Andere Entitäten müssen als Abhängigkeiten zu dieser definiert werden. Siehe "IoC"
  • Bessere Testerfahrung. Mit expliziten Abhängigkeiten können Sie verschiedene Teile Ihres Systems mit einem primitiven Testverhalten stubben, das dieselbe öffentliche API wie Ihr Produktionscode hat. Siehe "Mocks arent 'stubs"

Die andere Sache, die zu beachten ist, ist, dass wir uns normalerweise auf Abstraktionen verlassen, nicht auf Implementierungen. Ich sehe viele Leute, die DI verwenden, um nur eine bestimmte Implementierung zu injizieren. Es gibt einen großen Unterschied.

Denn wenn Sie eine Implementierung einfügen und sich darauf verlassen, gibt es keinen Unterschied, mit welcher Methode wir Objekte erstellen. Es spielt einfach keine Rolle. Wenn Sie beispielsweise requestsohne geeignete Abstraktionen injizieren, benötigen Sie immer noch etwas Ähnliches mit denselben Methoden, Signaturen und Rückgabetypen. Sie können diese Implementierung überhaupt nicht ersetzen. Wenn Sie jedoch injizieren fetch_order(order: OrderID) -> Order, bedeutet dies, dass sich alles im Inneren befinden kann. requests, Datenbank, was auch immer.

Um es zusammenzufassen:

Was sind die Vorteile der Injektion?

Der Hauptvorteil besteht darin, dass Sie Ihre Abhängigkeiten nicht manuell zusammenstellen müssen. Dies ist jedoch mit enormen Kosten verbunden: Sie verwenden komplexe, sogar magische Tools, um Probleme zu lösen. Die eine oder andere Komplexität wird dich zurückschlagen.

Lohnt es sich, das Inject-Framework zu verwenden?

Noch etwas zum Thema injectFramework. Ich mag es nicht, wenn Objekte, in die ich etwas injiziere, davon wissen. Es ist ein Implementierungsdetail!

Woher in einem PostcardWeltdomänenmodell zum Beispiel weiß das?

Ich würde empfehlen, punqfür einfache und dependenciesfür komplexe Fälle zu verwenden.

injectErzwingt auch keine saubere Trennung von "Abhängigkeiten" und Objekteigenschaften. Wie bereits erwähnt, besteht eines der Hauptziele von DI darin, strengere Verantwortlichkeiten durchzusetzen.

Lassen Sie mich im Gegensatz dazu zeigen, wie es punqfunktioniert:

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)

Sehen? Wir haben nicht einmal einen Konstruktor. Wir definieren unsere Abhängigkeiten deklarativ und punqfügen sie automatisch ein. Und wir definieren keine spezifischen Implementierungen. Es sind nur Protokolle zu befolgen. Dieser Stil wird als "Funktionsobjekte" oder Klassen im SRP- Stil bezeichnet.

Dann definieren wir den punqContainer selbst:

# 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)

Und benutze es:

from project.implemented import container

send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())

Sehen? Jetzt haben unsere Klassen keine Ahnung, wer und wie sie erstellt. Keine Dekorateure, keine besonderen Werte.

Lesen Sie hier mehr über Klassen im SRP-Stil:

Gibt es andere bessere Möglichkeiten, die Domain von außen zu trennen?

Sie können funktionale Programmierkonzepte anstelle von zwingenden verwenden. Die Hauptidee der Funktionsabhängigkeitsinjektion besteht darin, dass Sie keine Dinge aufrufen, die auf dem Kontext beruhen, den Sie nicht haben. Sie planen diese Aufrufe für später, wenn der Kontext vorhanden ist. So können Sie die Abhängigkeitsinjektion mit nur einfachen Funktionen veranschaulichen:

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

Das einzige Problem mit diesem Muster ist, dass _award_points_for_letterses schwer zu komponieren ist.

Aus diesem Grund haben wir eine spezielle Verpackung erstellt, um die Komposition zu unterstützen (sie ist Teil der 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

Hat zum Beispiel RequiresContexteine spezielle .mapMethode, um sich mit einer reinen Funktion zusammenzusetzen. Und das ist es. Als Ergebnis haben Sie nur einfache Funktionen und Kompositionshilfen mit einfacher API. Keine Magie, keine zusätzliche Komplexität. Und als Bonus ist alles richtig geschrieben und kompatibel mit mypy.

Lesen Sie hier mehr über diesen Ansatz:

sobolevn
quelle
0

Das erste Beispiel kommt einem "richtigen" Clean / Hex ziemlich nahe. Was fehlt, ist die Idee eines Composition Root, und Sie können Clean / Hex ohne Injektor-Framework ausführen. Ohne sie würden Sie so etwas tun:

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

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

Das geht von Pure / Vanilla / Poor Man's DI, je nachdem, mit wem Sie sprechen. Eine abstrakte Schnittstelle ist nicht unbedingt erforderlich, da Sie sich auf Ententypisierung oder strukturelle Typisierung verlassen können.

Ob Sie ein DI-Framework verwenden möchten oder nicht, ist eine Frage der Meinung und des Geschmacks. Es gibt jedoch auch andere einfachere Alternativen zum Injizieren wie punq, die Sie in Betracht ziehen könnten, wenn Sie diesen Weg beschreiten.

https://www.cosmicpython.com/ ist eine gute Ressource, die sich eingehend mit diesen Themen befasst.

ejung
quelle
0

Möglicherweise möchten Sie eine andere Datenbank verwenden und möchten die Flexibilität haben, dies auf einfache Weise zu tun. Aus diesem Grund halte ich die Abhängigkeitsinjektion für eine bessere Möglichkeit, Ihren Dienst zu konfigurieren

Kederrac
quelle