Existen varios objetivos principales en la técnica de inyección de dependencia, que incluyen (entre otros):
- Bajar el acoplamiento entre partes de su sistema. De esta manera puede cambiar cada parte con menos esfuerzo. Consulte "Alta cohesión, bajo acoplamiento"
- Hacer cumplir normas más estrictas sobre las responsabilidades. Una entidad debe hacer solo una cosa en su nivel de abstracción. Otras entidades deben definirse como dependencias de esta. Ver "IoC"
- Mejor experiencia de prueba. Las dependencias explícitas le permiten tropezar diferentes partes de su sistema con un comportamiento de prueba primitivo que tiene la misma API pública que su código de producción. Ver "Los simulacros no son"
La otra cosa a tener en cuenta es que usualmente confiaremos en abstracciones, no en implementaciones. Veo a muchas personas que usan DI para inyectar solo una implementación particular. Hay una gran diferencia
Porque cuando inyectas y confías en una implementación, no hay diferencia en el método que usamos para crear objetos. Simplemente no importa. Por ejemplo, si inyecta requests
sin las abstracciones adecuadas, aún necesitaría algo similar con los mismos métodos, firmas y tipos de retorno. No podrá reemplazar esta implementación en absoluto. Pero, cuando se inyecta fetch_order(order: OrderID) -> Order
, significa que cualquier cosa puede estar adentro. requests
, base de datos, lo que sea.
Para resumir las cosas:
¿Cuáles son los beneficios de usar inyectar?
El principal beneficio es que no tiene que ensamblar sus dependencias manualmente. Sin embargo, esto tiene un costo enorme: está utilizando herramientas complejas, incluso mágicas, para resolver problemas. Un día u otra complejidad te contraatacará.
¿Vale la pena molestarse y usar el marco de inyección?
Una cosa más sobre el inject
marco en particular. No me gusta cuando los objetos donde inyecto algo lo saben. ¡Es un detalle de implementación!
¿Cómo, en un Postcard
modelo de dominio mundial , por ejemplo, sabe esto?
Recomendaría usar punq
para casos simples y dependencies
complejos.
inject
tampoco impone una separación limpia de "dependencias" y propiedades de objeto. Como se dijo, uno de los principales objetivos de DI es imponer responsabilidades más estrictas.
En contraste, déjame mostrarte cómo punq
funciona:
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)
¿Ver? Ni siquiera tenemos un constructor. Definitivamente definimos nuestras dependencias y las punq
inyectaremos automáticamente. Y no definimos implementaciones específicas. Solo protocolos a seguir. Este estilo se llama "objetos funcionales" o clases con estilo SRP .
Luego definimos el punq
contenedor en sí:
# 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)
Y úsalo:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
¿Ver? Ahora nuestras clases no tienen idea de quién y cómo los crea. Sin decoradores, sin valores especiales.
Lea más sobre las clases de estilo SRP aquí:
¿Hay otras formas mejores de separar el dominio del exterior?
Puede usar conceptos de programación funcional en lugar de los imperativos. La idea principal de la inyección de dependencia de funciones es que no se llaman cosas que dependen del contexto que no se tiene. Usted programa estas llamadas para más tarde, cuando el contexto está presente. Así es como puede ilustrar la inyección de dependencia con funciones 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
El único problema con este patrón es que _award_points_for_letters
será difícil de componer.
Es por eso que hicimos un envoltorio especial para ayudar a la composición (es parte de 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
Por ejemplo, RequiresContext
tiene un .map
método especial para componerse con una función pura. Y eso es. Como resultado, tiene funciones simples y ayudantes de composición con API simple. Sin magia, sin complejidad extra. Y como beneficio adicional, todo está correctamente escrito y es compatible mypy
.
Lea más sobre este enfoque aquí: