Існує декілька основних цілей у техніці введення залежностей, включаючи (але не обмежуючись ними):
- Опускання муфти між частинами вашої системи. Таким чином ви можете змінити кожну частину з меншими зусиллями. Див. "Висока згуртованість, низька муфта"
- Дотримуватись більш жорстких правил щодо відповідальності. Одне об'єднання повинно робити лише одне на своєму рівні абстракції. Інші сутності повинні бути визначені як залежності до цього. Див. "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
.
Детальніше про цей підхід читайте тут: