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 requests
ohne 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 inject
Framework. Ich mag es nicht, wenn Objekte, in die ich etwas injiziere, davon wissen. Es ist ein Implementierungsdetail!
Woher in einem Postcard
Weltdomänenmodell zum Beispiel weiß das?
Ich würde empfehlen, punq
für einfache und dependencies
für komplexe Fälle zu verwenden.
inject
Erzwingt 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 punq
funktioniert:
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 punq
fü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 punq
Container 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_letters
es 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 RequiresContext
eine spezielle .map
Methode, 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: