awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Инверсия управления: как зависимости добираются до класса

Суть Класс, конструирующий своих коллабораторов сам, приварен к ним. Инверсия управления передаёт классу нужное снаружи — а выбор между внедрением через конструктор, service locator и `new` внутри класса решает, насколько ваш код тестируем и честен.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Команда хочет протестировать свой OrderService. Тест создаёт один — и открывается реальное соединение с Postgres, срабатывает реальный вызов Stripe, и реальное письмо уходит на реальный адрес. Они не могут протестировать логику без всего продакшен-стека, потому что конструктор делает this.db = new PgClient(), this.payments = new StripeClient(), this.mail = new Mailer(). Логика никогда не была проблемой. Проблемой было связывание.

Связанность, создаваемая new

Когда класс пишет new EmailService() внутри себя, он принял необратимое решение за каждого вызывающего: этот код есть реальный email-сервис, всегда. Нельзя подставить фейк в тесте, подменить другую реализацию на окружение или переиспользовать класс с другим коллаборатором. Зависимость скрыта (невидима в типе) и жёсткая (приварена при конструировании).

Инверсия управления

Инверсия управления (IoC) переворачивает, кто решает. Вместо того чтобы класс выбирал конкретного коллаборатора, класс объявляет абстрактную потребность, а что-то другое поставляет конкретного. Внедрение зависимостей — самая частая форма IoC: зависимости внедряются снаружи, а не конструируются внутри.

Чистейшее внедрение — через конструктор:

class OrderService {
  constructor(
    private db: Database,
    private payments: PaymentGateway,
    private mail: Mailer,
  ) {}
}

Теперь сигнатура конструктора — честный, проверяемый на компиляции список всего, что нужно классу. Тест передаёт фейки; продакшен передаёт реальные. Зависимость стала видимой и мягкой.

Service locator: соблазнительный кузен внедрения

Service locator тоже избегает new, но класс просит зависимости у глобального реестра: const db = Container.get('Database'). Это выглядит как DI, но возвращает первородный грех — зависимости снова скрыты (конструктор больше их не перечисляет), и класс связан с локатором. Это широко считается антипаттерном в прикладном коде именно потому, что скрывает, что нужно классу, и падает лишь в рантайме, не на компиляции и не на старте.

Почему это работает

Почему service locator — антипаттерн, если он тоже убирает new? Оба убирают захардкоженное конструирование, но различаются честностью. При внедрении через конструктор зависимости прямо в сигнатуре — их видят и компилятор, и читатель, а отсутствующая падает на старте. При service locator конструктор выглядит пустым, пока класс тайком тянется в глобальный контейнер в рантайме; отсутствующая или неверно настроенная зависимость всплывает как рантайм-падение глубоко в пути кода, а тесты вынуждены настраивать глобальный локатор вместо простой передачи аргументов. Локатор легитимен в одном месте — внутри самого DI-фреймворка или корня композиции — но не разбросанным по бизнес-логике.

Корень композиции

Если классы больше не конструируют свои зависимости, что-то должно. Это что-то — корень композиции: единственное место рядом с точкой входа приложения, где собирается весь граф объектов — единственное место, называющее конкретные реализации. Всё ниже зависит лишь от абстракций. Централизация конструирования означает, что есть ровно одна точка для смены связывания, подмены реализаций или считывания всего графа зависимостей. DI-контейнер автоматизирует эту сборку, но принцип держится и при ручном связывании: держи конструирование на краю, держи логику в середине свободной от зависимостей.

ПодходЗависимостиПадает когдаТестируемо
new внутри классаСкрыты + привареныНикогда не подменитьНет
Service locatorСкрыты, добываются в рантаймеВ рантайме, глубоко в путиНеудобно
Внедрение через конструкторВидны в сигнатуреНа старте / компиляцииДа
Викторина

Почему `new PgClient()` внутри конструктора сервиса — проблема тестируемости?

Викторина

У класса пустой конструктор, но он вызывает `Container.get('Mailer')` внутри всякий раз, когда нужно отправить письмо. В чём главное возражение?

Расставь шаги по порядку

Упорядочьте шаги рефакторинга приваренного класса к внедрению через конструктор:

  1. 1 Найти вызовы `new`, спрятанные внутри класса
  2. 2 Определить абстракции (интерфейсы/типы) для этих зависимостей
  3. 3 Добавить их параметрами конструктора, типизированными абстракциями
  4. 4 Перенести конкретное конструирование в корень композиции
  5. 5 В тестах передавать фейки через конструктор вместо реальных сервисов
Вспомните перед уходом
  1. 01
    Какие два свойства делают внутриклассовые вызовы `new` проблемой связанности и как внедрение через конструктор их чинит?
  2. 02
    Почему service locator считают антипаттерном, хотя он убирает захардкоженный `new`?
  3. 03
    Что такое корень композиции и зачем централизовать конструирование там?
Итог

Конструирование коллабораторов через new внутри класса приваривает его к конкретным реализациям и попутно их скрывает, и поэтому сервис, построенный так, тащит весь продакшен-стек в каждый тест. Инверсия управления переворачивает решение: класс объявляет абстрактные потребности и получает конкретные экземпляры снаружи. Внедрение через конструктор — честная форма: зависимости перечислены в сигнатуре, видны компилятору и читателю, а отсутствующая падает быстро на старте. Service locator убирает new, но снова скрывает зависимости за глобальным реестром и откладывает падение до рантайма, поэтому это антипаттерн вне внутренностей фреймворка. Всё это требует корня композиции: одного края, где собирается реальный граф объектов, оставляя логику в середине свободной от зависимостей. Следующий вопрос — время жизни этих связанных объектов: синглтоны, request-скоуп, transient — и тонкие баги, к которым приглашает каждый скоуп.

Связанные уроки
встречается в185
Продолжить восхождение ↑Скоупы и время жизни DI: singleton, request, transient
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.