Existem vários objetivos principais na técnica de injeção de dependência, incluindo (mas não limitado a):
- Abaixando o acoplamento entre partes do seu sistema. Dessa forma, você pode alterar cada parte com menos esforço. Consulte "Coesão alta, baixo acoplamento"
- Aplicar regras mais estritas sobre responsabilidades. Uma entidade deve fazer apenas uma coisa em seu nível de abstração. Outras entidades devem ser definidas como dependências para esta. Veja "IoC"
- Melhor experiência de teste. Dependências explícitas permitem stub diferentes partes do seu sistema com algum comportamento de teste primitivo que possui a mesma API pública que o seu código de produção. Consulte "Zomba de stubs"
A outra coisa a ter em mente é que geralmente devemos confiar em abstrações, não em implementações. Eu vejo muitas pessoas que usam o DI para injetar apenas uma implementação específica. Há uma grande diferença.
Porque quando você injeta e confia em uma implementação, não há diferença em qual método usamos para criar objetos. Isso simplesmente não importa. Por exemplo, se você injetar requests
sem abstrações adequadas, você ainda precisará de algo semelhante com os mesmos métodos, assinaturas e tipos de retorno. Você não seria capaz de substituir esta implementação. Mas, quando você injeta fetch_order(order: OrderID) -> Order
, significa que qualquer coisa pode estar dentro. requests
, banco de dados, qualquer que seja.
Para resumir:
Quais são os benefícios do uso de injeção?
O principal benefício é que você não precisa montar suas dependências manualmente. No entanto, isso tem um custo enorme: você está usando ferramentas complexas e até mágicas para resolver problemas. Um dia ou outro de complexidade, você reagirá.
Vale a pena se preocupar e usar o framework de injeção?
Mais uma coisa sobre o inject
framework em particular. Não gosto quando objetos em que injeto algo sabem disso. É um detalhe de implementação!
Como em um Postcard
modelo de domínio mundial , por exemplo, sabe disso?
Eu recomendaria usar punq
para casos simples e dependencies
para casos complexos.
inject
também não impõe uma separação limpa de "dependências" e propriedades de objetos. Como foi dito, um dos principais objetivos da DI é reforçar as responsabilidades.
Por outro lado, deixe-me mostrar como 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)
Vejo? Nós ainda não temos um construtor. Definimos declarativamente nossas dependências e punq
as injetamos automaticamente. E não definimos nenhuma implementação específica. Apenas protocolos a seguir. Esse estilo é chamado de "objetos funcionais" ou classes estilizadas com SRP .
Em seguida, definimos o punq
próprio contêiner:
# 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 use-o:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Vejo? Agora, nossas aulas não têm idéia de quem e como as cria. Sem decoradores, sem valores especiais.
Leia mais sobre as classes no estilo SRP aqui:
Existem outras maneiras melhores de separar o domínio do lado de fora?
Você pode usar conceitos de programação funcional em vez de conceitos imperativos. A idéia principal da injeção de dependência de função é que você não chama coisas que dependem do contexto que não possui. Você agenda essas chamadas para mais tarde, quando o contexto estiver presente. Veja como você pode ilustrar a injeção de dependência com apenas funções 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
O único problema com esse padrão é que _award_points_for_letters
será difícil compor.
Por isso, criamos um invólucro especial para ajudar a composição (faz parte do 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 exemplo, RequiresContext
possui um .map
método especial para se recompor com uma função pura. E é isso. Como resultado, você tem apenas funções simples e auxiliares de composição com API simples. Sem mágica, sem complexidade extra. E como um bônus, tudo é digitado e compatível corretamente mypy
.
Leia mais sobre essa abordagem aqui: