Dependency Injection на созвоне часто звучит как Spring и «контейнер сам всё проставит». На деле это скучная развилка: сервис заказа сам делает new Postgres или получает уже готовое хранилище снаружи. Вторая схема дешевле меняется и тестируется. Контейнер — опция. Буква D в SOLID — тот же шов, только в алфавите: подробности принципов в чистой архитектуре и SOLID, здесь не повторяю пять букв.
Ниже — зачем шов, чем конструктор отличается от «достать из глобального локатора», когда ручная склейка честнее фреймворка, и как не написать DI в шапке резюме без теста без сети. Блоков кода нет специально: на экране просят ваш сервис, не учебный сниппет.
Коротко:
- Зависимость приходит снаружи. Класс не знает, Postgres это или фейк в тесте.
- Конструктор — обычный шов. Сеттер и «поле, которое кто-то потом заполнит» хуже читаются.
- Контейнер (Spring, Nest, FastAPI Depends) автоматизирует склейку. Он не заменяет шов: без шва контейнер только прячет new в XML/декораторах.
- Маленький скрипт и пет из трёх эндпоинтов часто дешевле собирать руками. Фреймворк ради «как во взрослых» — долг.
- В CV нужен тест без сети и замена шлюза, не слово «DI». Пули: опыт.
Боль: жёсткая склейка, не отсутствие фреймворка
Сервис оплаты внутри конструктора создаёт клиент банка, логгер и почту. Тест либо ходит в банк, либо не пишется. Смена провайдера — правка того же класса. Ревьюер не видит, что тестировать. Это связанность, не «мало паттернов».
DI не про красоту. Про стоимость замены и про тест за доли секунды. Если за год шлюз не меняли и теста нет — сначала тест с фейком, потом разговор про контейнер. Иначе вы перенесёте new в конфиг и назовёте это архитектурой. Тот же критерий, что на ревью SOLID: называйте изменение, которое уже болело, не букву паттерна.
Связь с чистой архитектурой: ядро не импортирует драйвер. DI — способ это соблюсти на уровне объекта. Папки domain/infra без шва в конструкторе — декорации. Рекрутер откроет репозиторий и увидит слои без рабочего сценария — это слабее одного теста без сети. Для входа без стажа сценарий важнее графа зависимостей: резюме без опыта.
Конструктор, а не магия полей
Самый скучный рабочий вариант: в конструктор передали хранилище, очередь, часы. В проде — боевые. В тесте — фейки, которые запоминают вызовы. Часы тоже зависимость, если пишете «сейчас»: иначе тесты плавают по понедельникам. Генератор id и «случайность» — тот же приём: не прячьте их внутри, если от них зависит ветка сценария.
Сеттер-инъекция: объект существует «полусобранным», пока кто-то не вызвал setDb. Легко забыть. На созвоне это видно по пустой ссылке в середине сценария. Свойства, которые контейнер заполняет рефлексией, удобны фреймворку и непрозрачны человеку. Предпочитайте явный конструктор, пока команда не договорилась иначе. Если фреймворк настаивает на сеттерах — соберите объект полностью до первого вызова бизнес-метода, не «поле заполнится потом».
Интерфейс нужен, когда есть две реализации или тест. Интерфейс «на будущее» с одной реализацией — шум, это уже в SOLID. Для DI достаточно роли: «то, что умеет сохранить заказ». Имя роли важнее слова Interface в файле. Не плодите IOrderService и OrderServiceImpl, если impl один и имя ничего не говорит.
Пример 1. Заказы и почта
Было: сервис считает сумму, сам ходит в Postgres, сам шлёт SMTP. Тест поднимает Docker. CI красный без VPN.
Стало. Сервис принимает «хранилище» и «уведомитель». Считает сумму сам. В тесте хранилище — список в памяти, уведомитель считает письма и никуда не стучится. Боевая склейка — один файл рядом с точкой входа, не внутри домена. Сменили SMTP на очередь — правка склейки, тесты суммы зелёные. Это DI без Spring. Если потом появится контейнер, он соберёт те же роли.
Не выносите расчёт суммы в «сервис ради сервиса». Шов там, где уже жгло: почта, база, чужой HTTP. Расчёт, который меняется вместе с правилами заказа, может жить в том же классе, пока правил мало.
Пример 2. Созвон «у нас Nest, значит умею DI»
Игорь в CV: «NestJS, Dependency Injection, SOLID». На экране просят: как протестировать сервис без Mongo. Он открывает модуль с декораторами и не может подставить фейк без подъёма всего приложения.
Что сказать. «Сервис получает репозиторий конструктором. В юните — фейк с двумя заказами. В интеграционном — контейнер с тестовой базой, один сценарий». Если юнита нет и всё только e2e — честно так и скажите: шов бумажный, контейнер есть. Тогда не пишите DI в шапке. Напишите Nest и поставку. Шаблон рассказа: как в SOLID-созвоне, только про одну зависимость, не про пять букв.
Фреймворк, который «сам проставляет», не учит шву. Учит, когда вы один раз собрали вручную и поняли, кого подменяете.
Контейнер, service locator, ручная склейка
Контейнер полезен, когда объектов десятки и склейка руками превращается в простыню. Цена: невидимые зависимости, циклические графы, старт приложения ради юнита. Договорённость: доменные сервисы не просят контейнер внутрь себя. Внутрь — роли. Контейнер живёт у края, рядом с HTTP. Если юнит не стартует без «поднять контекст» — вы тестируете фреймворк, не сценарий.
Service locator: объект сам ходит в глобальный «дай мне базу». Это прячет зависимость ещё сильнее, чем new. Тест должен настраивать глобальное состояние. На созвоне это слабый ответ. Локатор допустим в легаси на время, не как цель. Миграция: сначала конструктор на одном сервисе, потом вычищайте getInstance из середины.
Ручная склейка в composition root — норма для сервиса команды из пяти человек. Не стыдитесь. Стыдно, когда склейка размазана по двадцати конструкторам с new внутри. Один файл «как собираем прод» читается за минуту и экономит онбординг лучше, чем «у нас Spring, разберёшься».
Где DI вреден
- Скрипт на вечер, миграция один раз, прототип к пятнице.
- Слой абстракций на каждый чих в учебном CRUD без второй реализации.
- Протаскивание десяти ролей в конструктор «на чистоту», хотя класс делает одну вещь с одной базой.
- Поднимать Spring ради проверки, что 2+2 в калькуляторе заказа.
- DI в резюме junior без репозитория, где шов видно.
Принцип окупается в коде, который живут и меняют. Для одноразового — мешает. На ревью аргумент: «тест перестанет ходить в сеть». Не: «так надо по Фаулеру». Статья Фаулера про injection полезна как история термина, не как чеклист вашего PR.