Инверсия зависимостей и внедрение зависимостей
Зачем это на интервью
DIP и DI часто смешивают. На интервью E4 должен разделить принцип проектирования и технику сборки объектов: DIP отвечает, от какого контракта зависит бизнес-код; DI отвечает, как конкретная зависимость оказывается в объекте. Это позволяет обсуждать тестируемость, замену HTTP-клиента, транзакции и lifecycle без обещаний «всё замокать».
Минимум для E4
- Сформулировать разницу между DIP, DI и service locator.
- Объявить узкий интерфейс возле прикладного потребителя, а реализацию оставить в адаптере.
- Передать обязательные зависимости через конструктор и проверить их на старте.
- Учитывать
context.Context, ошибки, таймауты и владение ресурсом в контракте.
При прямой зависимости use case от *sql.DB бизнес-слой знает деталь хранения. Инверсия создаёт порт, выражающий нужную операцию, например загрузку пользователя. Адаптер SQL реализует этот порт; composition root соединяет их в main.
type UserFinder interface {
FindByID(ctx context.Context, id string) (User, error)
}
type GetProfile struct {
users UserFinder
}
func NewGetProfile(users UserFinder) (*GetProfile, error) {
if users == nil {
return nil, errors.New("users is required")
}
return &GetProfile{users: users}, nil
}Инъекция через конструктор делает зависимость явной и проверяет nil-интерфейс до создания объекта. Она не распознаёт интерфейс с typed-nil pointer: composition root должен не присваивать такой указатель интерфейсу, а конструкторы конкретных адаптеров должны проверять свой pointer до возврата. Полевая инъекция в Go обычно хуже: она допускает nil и скрывает обязательность. Параметрическая инъекция подходит для зависимости, нужной только одному методу, например Clock или генератора ID.
Углубление для E5/Senior
E5 проектирует composition root и жизненный цикл. Долгоживущие клиенты, пулы соединений и конфигурация создаются один раз на границе процесса; request-scoped данные передают через context.Context или явные параметры, но не кладут в singleton. Конструктор не должен делать сетевой I/O: иначе сборка графа становится неуправляемой и плохо тестируется.
Граница порта должна отражать доменную потребность, а не копировать API поставщика. PaymentGateway.Charge может возвращать доменный результат и классифицированные ошибки, тогда смена провайдера не протекает в use case. Но не прячьте каждую библиотеку за интерфейсом заранее: внешний вызов, который не надо подменять и который имеет один сценарий, можно использовать напрямую до появления причины.
Ключевые понятия
| Термин | Смысл | Признак здорового применения |
|---|---|---|
| DIP | Политика зависит от абстракции | Контракт выражает нужду клиента |
| DI | Передача готовой зависимости извне | Конструктор показывает обязательные зависимости |
| Composition root | Место сборки графа объектов | Обычно main или bootstrap-пакет |
| Port/adapter | Контракт и его инфраструктурная реализация | Адаптер не диктует доменную модель |
| Service locator | Глобальный поиск зависимости | Скрывает зависимости и затрудняет тесты |
Контракт обязан описывать ошибки. Для отсутствующей записи различайте ожидаемое состояние (ErrNotFound) и инфраструктурную ошибку; вызывающий код должен понимать, что ретраить, а что превратить в ответ 404.
Типовые вопросы
- DIP и DI — одно и то же?
- Нет. DIP — направление зависимостей через абстракцию; DI — способ передать реализацию.
- Где объявлять интерфейс в Go?
- Обычно в пакете потребителя и только с методами, которые ему нужны.
- Почему service locator часто вреден?
- Зависимости не видны в API типа, ошибка проявляется поздно, а тесты делят глобальное состояние.
- Нужен ли интерфейс вокруг каждой базы и SDK?
- Нет. Он нужен при сменяемом поведении, тестовой границе или защите доменного кода от нестабильного API.
- Как передать транзакцию без протекания
*sql.Txв домен?- Описать нужный репозиторный или unit-of-work контракт в прикладном слое и реализовать его в инфраструктуре.
- Почему
context.Contextне используют как контейнер зависимостей?- Его значения не видны в сигнатуре по ключам, их типовая безопасность слабее, а lifecycle скрыт.
Практика
- Переделайте use case, который вызывает
http.Clientи парсит ответ поставщика внутри бизнес-метода.- Критерии готовности: у use case есть узкий порт; адаптер переводит transport-ошибки; конструктор отклоняет
nil; unit-тест не использует сеть.
- Критерии готовности: у use case есть узкий порт; адаптер переводит transport-ошибки; конструктор отклоняет
- Соберите приложение в
mainс конфигурацией, клиентом, адаптером и use case.- Критерии готовности: только bootstrap знает конкретные типы; закрытие ресурса происходит в одном месте; request-данные не являются глобальными.
Частые ошибки и ловушки
- Путать передачу зависимостей с автоматическим соблюдением DIP.
- Экспортировать интерфейсы «на всякий случай» из пакета реализации.
- Превращать DI-контейнер в service locator внутри прикладного кода.
- Хранить пользователя, request ID или транзакцию в singleton.
- Скрывать все ошибки адаптера и лишать вызывающий код решения о retry.
Связанные темы
Источники
- Robert C. Martin. Clean Architecture, 2017.
- Go Blog: Package names.
- Go Code Review Comments: Interfaces.