Esistono diversi obiettivi principali nella tecnica di Iniezione delle dipendenze, tra cui (ma non limitato a):
- Abbassamento dell'accoppiamento tra le parti del sistema. In questo modo puoi cambiare ogni parte con meno sforzo. Vedi "Alta coesione, basso accoppiamento"
- Applicare regole più severe sulle responsabilità. Un'entità deve fare solo una cosa sul suo livello di astrazione. Altre entità devono essere definite come dipendenze da questa. Vedi "IoC"
- Migliore esperienza di test. Le dipendenze esplicite consentono di stub diverse parti del sistema con un comportamento di test primitivo che ha la stessa API pubblica del codice di produzione. Vedi "Mock arent 'stub"
L'altra cosa da tenere a mente è che di solito faremo affidamento su astrazioni, non su implementazioni. Vedo molte persone che usano DI per iniettare solo un'implementazione particolare. C'è una grande differenza.
Perché quando si inietta e si fa affidamento su un'implementazione, non vi è alcuna differenza nel metodo utilizzato per creare oggetti. Non importa. Ad esempio, se si inietta requests
senza astrazioni adeguate, si richiederebbe comunque qualcosa di simile con gli stessi metodi, firme e tipi di ritorno. Non saresti in grado di sostituire affatto questa implementazione. Ma quando si inietta fetch_order(order: OrderID) -> Order
significa che tutto può essere dentro. requests
, database, qualunque cosa.
Per riassumere:
Quali sono i vantaggi dell'utilizzo dell'iniezione?
Il vantaggio principale è che non è necessario assemblare manualmente le dipendenze. Tuttavia, questo ha un costo enorme: stai usando strumenti complessi, persino magici, per risolvere i problemi. Un giorno o un'altra complessità ti combatteranno.
Vale la pena disturbare e utilizzare l'inject framework?
Un'altra cosa sul inject
framework in particolare. Non mi piace quando gli oggetti in cui inietto qualcosa lo sanno. È un dettaglio di implementazione!
Come in un Postcard
modello di dominio mondiale , ad esempio, conosce questa cosa?
Consiglierei di usare punq
per casi semplici e dependencies
complessi.
inject
inoltre non impone una netta separazione tra "dipendenze" e proprietà degli oggetti. Come è stato detto, uno degli obiettivi principali di DI è quello di imporre responsabilità più rigorose.
Al contrario, lascia che ti mostri come punq
funziona:
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)
Vedere? Non abbiamo nemmeno un costruttore. Definiamo in modo dichiarativo le nostre dipendenze e punq
le inietteremo automaticamente. E non definiamo implementazioni specifiche. Seguiranno solo i protocolli. Questo stile è chiamato "oggetti funzionali" o classi con stile SRP .
Quindi definiamo il punq
contenitore stesso:
# 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)
E usalo:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Vedere? Ora le nostre lezioni non hanno idea di chi e come le crei. Nessun decoratore, nessun valore speciale.
Maggiori informazioni sulle classi in stile SRP qui:
Esistono altri modi migliori per separare il dominio dall'esterno?
È possibile utilizzare concetti di programmazione funzionale anziché imperativi. L'idea principale dell'iniezione di dipendenza dalle funzioni è che non si chiamano cose che si basano su un contesto che non si ha. Si programmano queste chiamate per dopo, quando il contesto è presente. Ecco come è possibile illustrare l'iniezione di dipendenza con semplici funzioni:
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
L'unico problema con questo modello è che _award_points_for_letters
sarà difficile da comporre.
Ecco perché abbiamo realizzato un involucro speciale per aiutare la composizione (fa parte di 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
Ad esempio, RequiresContext
ha un .map
metodo speciale per comporsi con una funzione pura. E questo è tutto. Di conseguenza hai solo semplici funzioni e aiutanti di composizione con una semplice API. Nessuna magia, nessuna complessità aggiuntiva. E come bonus tutto è correttamente digitato e compatibile con mypy
.
Maggiori informazioni su questo approccio qui: