Существует несколько основных целей в технике внедрения зависимостей, включая (но не ограничиваясь ими):
- Снижение связи между частями вашей системы. Таким образом, вы можете изменить каждую часть с меньшими усилиями. См. «Высокая когезия, низкая связь»
- Для обеспечения соблюдения более строгих правил об ответственности. Одна сущность должна делать только одну вещь на своем уровне абстракции. Другие сущности должны быть определены как зависимости от этого. См. "Io C"
- Лучший опыт тестирования. Явные зависимости позволяют вам заглушить различные части вашей системы с помощью некоторого примитивного поведения теста, которое имеет тот же API publi c, что и ваш производственный код. См. «Mocks is not stubs»
Другое, что нужно иметь в виду, это то, что мы обычно полагаемся на абстракции, а не на реализации. Я вижу много людей, которые используют DI для внедрения только конкретной реализации. Есть большая разница.
Потому что, когда вы внедряете и полагаетесь на реализацию, нет никакой разницы в том, какой метод мы используем для создания объектов. Это просто не имеет значения. Например, если вы вводите requests
без надлежащих абстракций, вам все равно потребуется что-то похожее с теми же методами, сигнатурами и типами возвращаемых данных. Вы не сможете заменить эту реализацию вообще. Но когда вы вводите fetch_order(order: OrderID) -> Order
, это означает, что внутри может быть что угодно. requests
, база данных, что угодно.
Подводя итог:
Каковы преимущества использования инъекций?
Основное преимущество заключается в том, что Вам не нужно собирать свои зависимости вручную. Однако это требует огромных затрат: вы используете сложные, даже магические инструменты для решения проблем. Однажды или другая сложность будет отбиваться от вас.
Стоит ли беспокоиться и использовать инфраструктуру для инъекций?
Еще одна вещь, касающаяся inject
инфраструктуры в частности. Мне не нравится, когда объекты, в которые я ввожу что-то, знают об этом. Это деталь реализации!
Как в мире, например, модель домена 1039 * знает эту вещь?
Я бы порекомендовал использовать punq
для простые случаи и dependencies
для сложных.
inject
также не обеспечивает четкое разделение "зависимостей" и свойств объекта. Как уже было сказано, одной из основных целей DI является обеспечение более строгих обязанностей.
Напротив, позвольте мне показать, как работает punq
:
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)
Видите? У нас даже нет конструктора. Мы декларативно определяем наши зависимости и punq
автоматически внедряет их. И мы не определяем никаких конкретных c реализаций. Только протоколы для подражания. Этот стиль называется "функциональными объектами" или классами в стиле SRP .
Затем мы определяем сам контейнер punq
:
# 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)
И используем его:
from project.implemented import container
send_postcards = container.resolve(SendTodaysPostcardsUsecase)
send_postcards(datetime.now())
Видите? Теперь наши классы понятия не имеют, кто и как их создает. Без декораторов, без специальных значений.
Подробнее о классах в стиле SRP можно прочитать здесь:
Существуют ли другие более эффективные способы отделения домена от внешней среды?
Вы можете использовать концепции функционального программирования вместо императивных. Основная идея внедрения зависимости в функции заключается в том, что вы не вызываете вещи, которые зависят от контекста, которого у вас нет. Вы планируете эти вызовы на потом, когда контекст присутствует. Вот как вы можете проиллюстрировать внедрение зависимостей с помощью простых функций:
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
Единственная проблема с этим шаблоном в том, что _award_points_for_letters
будет сложно составлять.
Вот почему мы сделали специальный Обертка, чтобы помочь композиции (это часть 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
Например, RequiresContext
имеет специальный метод .map
для составления себя с чистой функцией. И это все. Как В результате у вас есть просто простые функции и помощники по составлению с простым API, без магии c, без дополнительной сложности. И в качестве бонуса все правильно напечатано и совместимо с mypy
.
Подробнее об этом подходе читайте здесь: