Istnieje kilka głównych celów techniki wstrzykiwania zależności, w tym (ale nie tylko):
- Opuszczanie sprzęgła między częściami twojego systemu. W ten sposób możesz zmienić każdą część przy mniejszym wysiłku. Patrz „Wysoka kohezja, niskie sprzężenie”
- Egzekwowanie surowszych zasad dotyczących obowiązków. Jedna istota musi zrobić tylko jedną rzecz na poziomie abstrakcji. Inne podmioty muszą być zdefiniowane jako zależności od tego. Zobacz „IoC”
- Lepsze doświadczenie w testowaniu. Jawne zależności pozwalają na wycieranie różnych części systemu za pomocą niektórych prymitywnych zachowań testowych, które mają ten sam publiczny interfejs API niż kod produkcyjny. Zobacz „Mocks arent 'stubs”
Inną rzeczą, o której należy pamiętać, jest to, że zwykle będziemy polegać na abstrakcjach, a nie implementacjach. Widzę wielu ludzi, którzy używają DI do wstrzykiwania tylko określonej implementacji. Jest duża różnica.
Ponieważ kiedy wstrzykujesz i polegasz na implementacji, nie ma różnicy w jakiej metodzie używamy do tworzenia obiektów. To po prostu nie ma znaczenia. Na przykład, jeśli wstrzykujesz requests
bez odpowiednich abstrakcji, nadal potrzebujesz czegoś podobnego z tymi samymi metodami, podpisami i typami zwrotów. W ogóle nie będziesz w stanie zastąpić tej implementacji. Ale wstrzyknięcie fetch_order(order: OrderID) -> Order
oznacza, że wszystko może być w środku. requests
, baza danych, cokolwiek.
Podsumowując:
Jakie są zalety stosowania zastrzyku?
Główną zaletą jest to, że nie trzeba ręcznie konfigurować zależności. Jednak wiąże się to z ogromnymi kosztami: używasz złożonych, nawet magicznych narzędzi do rozwiązywania problemów. Pewnego dnia złożoność cię odeprze.
Czy warto zawracać sobie głowę i korzystać z ramek wstrzykiwania?
W szczególności jeszcze jedna rzecz o inject
frameworku. Nie lubię, kiedy przedmioty, do których coś wstrzykuję, wiedzą o tym. To szczegół implementacji!
Jak Postcard
na przykład w modelu domeny światowej wie o tym?
Polecam używać punq
do prostych i dependencies
złożonych przypadków .
inject
nie wymusza również czystego oddzielenia „zależności” i właściwości obiektu. Jak powiedziano, jednym z głównych celów DI jest egzekwowanie surowszych obowiązków.
Natomiast pokażę, jak punq
działa:
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)
Widzieć? Nie mamy nawet konstruktora. Deklaracyjnie definiujemy nasze zależności i punq
automatycznie je wprowadzamy. I nie definiujemy żadnych konkretnych implementacji. Tylko protokoły do naśladowania. Ten styl nazywa się „obiektami funkcjonalnymi” lub klasami stylizowanymi SRP .
Następnie definiujemy punq
sam kontener:
# 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)
I użyj tego:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Widzieć? Teraz nasze klasy nie mają pojęcia, kto i jak je tworzy. Bez dekoratorów, bez specjalnych wartości.
Przeczytaj więcej o klasach w stylu SRP tutaj:
Czy istnieją inne lepsze sposoby oddzielenia domeny od zewnątrz?
Możesz użyć koncepcji programowania funkcjonalnego zamiast koniecznych. Główną ideą wstrzykiwania zależności funkcji jest to, że nie wywołujesz rzeczy zależnych od kontekstu, którego nie masz. Połączenia te zaplanujesz na później, gdy kontekst będzie obecny. Oto jak zilustrować wstrzykiwanie zależności za pomocą prostych funkcji:
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
Jedynym problemem związanym z tym wzorem jest trudność _award_points_for_letters
do skomponowania.
Dlatego stworzyliśmy specjalne opakowanie, które pomaga w kompozycji (jest to część 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
Na przykład RequiresContext
ma specjalną .map
metodę komponowania się z czystą funkcją. I to wszystko. W rezultacie masz tylko proste funkcje i pomocniki przy składaniu z prostym API. Bez magii, bez dodatkowej złożoności. A jako bonus wszystko jest poprawnie wpisane i zgodne mypy
.
Przeczytaj więcej o tym podejściu tutaj: