open atlas
↑ К треку
Архитектура бэкенда BE · 02 · 03

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

Класс, конструирующий своих коллабораторов сам, приварен к ним. Инверсия управления передаёт классу нужное снаружи — а выбор между внедрением через конструктор, service locator и `new` внутри класса решает, насколько ваш код тестируем и честен.

BE Middle ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

Команда хочет протестировать свой 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-контейнер автоматизирует эту сборку, но принцип держится и при ручном связывании: держи конструирование на краю, держи логику в середине свободной от зависимостей.

Компромисс здесь — магия против явности. Контейнер покупает вам слабую связанность и тестовый шов в одну строку, но платит за это стоимостью старта и отлаживаемостью. Контейнеры на рефлексии и декораторах вынуждены читать метаданные, чтобы понять, что внедрять: приложение NestJS с несколькими сотнями провайдеров обычно тратит десятки-сотни миллисекунд на сборку графа при загрузке, и эта рефлексия метаданных — заметная доля холодного старта, из-за чего она и жжёт в serverless-функции, оплачиваемой помиллисекундно и вызываемой вхолодную. Ручное связывание — противоположность: корень композиции из обычных вызовов new в одном файле разрешается за микросекунды, каждая зависимость видна для «перейти к определению», а стек-трейс идёт через ваш код, а не через внутренности разрешения фреймворка. Ход сеньора — отказаться от контейнера, когда граф мал. Для CLI, воркера или узкоспециализированной Lambda с пятью объектами одна фабричная функция — buildApp(), которая new-ает эти пять и возвращает корень, — стартует быстрее, легче отлаживается и строго честнее, чем декораторы и контейнер ради сборки пяти объектов.

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

Все три подхода различаются по одному измерению: когда ты обнаруживаешь неверное связывание. Скрытый 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 — и тонкие баги, к которым приглашает каждый скоуп. Теперь, когда увидишь класс, делающий new SomeService() внутри конструктора, ты знаешь точный путь рефакторинга: вынести зависимость в абстракцию, поднять её в параметр конструктора и перенести конкретный new в корень композиции.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки
встречается в188

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.