В технике внедрения зависимостей есть несколько основных целей, включая (но не ограничиваясь ими):
- Понижение связи между частями вашей системы. Таким образом, вы можете изменить каждую часть с меньшими усилиями. Смотрите «Высокая когезия, низкая связь»
- Для обеспечения более строгих правил об ответственности. Одна сущность должна делать только одну вещь на своем уровне абстракции. Другие сущности должны быть определены как зависимости от этого. Смотрите "IoC"
- Лучший опыт тестирования. Явные зависимости позволяют вам заглушить разные части вашей системы с помощью некоторого примитивного поведения тестирования, которое имеет тот же открытый API, что и ваш производственный код. Смотрите "Мок не заглушки"
Еще одна вещь, которую нужно иметь в виду, это то, что мы обычно полагаемся на абстракции, а не на реализации. Я вижу много людей, которые используют DI для внедрения только конкретной реализации. Там большая разница.
Потому что, когда вы внедряете и полагаетесь на реализацию, нет разницы в том, какой метод мы используем для создания объектов. Это просто не имеет значения. Например, если вы вводите requests
без надлежащих абстракций, вам все равно потребуется что-то похожее с теми же методами, сигнатурами и типами возвращаемых данных. Вы не сможете заменить эту реализацию вообще. Но когда вы делаете инъекцию, fetch_order(order: OrderID) -> Order
это означает, что внутри может быть что угодно. requests
, база данных, что угодно.
Подводя итог:
Каковы преимущества использования инъекций?
Основным преимуществом является то, что вам не нужно собирать свои зависимости вручную. Однако это требует огромных затрат: вы используете сложные, даже магические инструменты для решения проблем. В тот или иной день вам придется дать отпор.
Стоит ли беспокоить и использовать фреймворк?
Еще одна вещь о inject
структуре в частности. Мне не нравится, когда объекты, в которые я ввожу что-то, знают об этом. Это деталь реализации!
Как Postcard
, например, в мировой модели предметной области это известно?
Я бы порекомендовал использовать 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
автоматически внедряем их. И мы не определяем какие-либо конкретные реализации. Только протоколы для подражания. Этот стиль называется «функциональные объекты» или 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. Никакой магии, никакой дополнительной сложности. И в качестве бонуса все правильно напечатано и совместимо с mypy
.
Подробнее об этом подходе читайте здесь: