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

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

Middleware и DI: два паттерна, формирующие любой backend

Суть Под каждым фреймворком лежат два паттерна: middleware перехватывает каждый запрос по порядку, а внедрение зависимостей связывает сервисы один раз на старте. Один управляет тем, что выполняется вокруг вашего кода; другой — тем, до чего код может дотянуться и как вы его тестируете.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 11 min

Два инженера описывают один и тот же backend. Один говорит: «каждый запрос проходит через auth, логирование и валидацию до обработчика». Другой говорит: «обработчик получает клиент базы, кеш и mailer, которые он сам не создавал». Они описывают один код с двух ракурсов: запрос, текущий сквозь слои, и зависимости, текущие внутрь объектов. Эти два ракурса — middleware и внедрение зависимостей, и почти каждое решение по фреймворку, которое вы примете, относится к одному из них.

Две ортогональные оси

Middleware и внедрение зависимостей отвечают на разные вопросы, и их путаница — частый источник мутной архитектуры:

  • Middleware — про ось запроса: что выполняется вокруг обработчика на каждом запросе и в каком порядке. Это динамично, на каждый запрос, упорядочено: auth, логирование, парсинг тела, rate limiting. Прошлый юнит показал, почему этот порядок — граница безопасности.
  • Внедрение зависимостей — про ось связывания: какие объекты куску кода дают, вместо того чтобы он конструировал их сам. Это в основном статично, решается на старте и про связанность и тестируемость: обработчик получает UserRepository, а не вызывает new UserRepository() внутри себя.

Одно — конвейер; другое — граф. Запрос путешествует по конвейеру; граф собирается один раз до того, как придёт хоть один запрос.

Зачем нужен middleware: сквозные заботы

Auth, логирование, трейсинг, сжатие и обработка ошибок нужны почти каждому эндпоинту, но не принадлежат ни одному из них. Без middleware каждый обработчик переизобретает их — несогласованно. Middleware выносит эти сквозные заботы в упорядоченные слои, оборачивающие обработчик, чтобы они были написаны один раз и применялись единообразно.

Зачем нужен DI: инверсия управления

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

MiddlewareВнедрение зависимостей
ОсьЗапрос (что выполняется вокруг кода)Связывание (что коду дают)
КогдаНа каждый запрос, динамическиОдин раз на старте, статически
ФормаУпорядоченный конвейерГраф объектов
РешаетСквозные заботыСвязанность, тестируемость
ЛомаетсяПорядок → баги безопасности/производительностиСкрытый new → нетестируемый код

Они встречаются во фреймворке

Реальные фреймворки сплавляют оба. Контроллеры NestJS разрешаются из DI-контейнера и сидят за конвейером middleware/guard/interceptor. Express держит DI неявным (вы связываете руками или цепляете к req), но модель middleware явная. Распознавать, на какой оси живёт проблема, — рефлекс сеньора: «этот эндпоинт медленный на каждом запросе» — обычно middleware; «этот класс невозможно протестировать» — обычно DI.

Викторина

Класс-сервис вызывает `new StripeClient()` прямо в конструкторе, и команда понимает, что его невозможно юнит-тестировать без обращения к реальному Stripe API. На какой оси проблема?

Викторина

Что лучше всего описывает разницу в том, *когда* действуют middleware и DI?

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

Упорядочьте события от старта процесса до обслуженного запроса:

  1. 1 Корень композиции связывает граф объектов (репозитории, клиенты, сервисы)
  2. 2 Фреймворк регистрирует конвейер middleware по порядку
  3. 3 Сервер начинает слушать соединения
  4. 4 Приходит запрос и течёт через конвейер middleware
  5. 5 Обработчик выполняется, используя внедрённые в него зависимости
  6. 6 Ответ формируется и возвращается
Вспомните перед уходом
  1. 01
    В чём разница между осью запроса и осью связывания, и зачем держать их раздельно?
  2. 02
    Зачем нужен middleware и какую проблему создают сквозные заботы без него?
  3. 03
    Что такое инверсия управления и какую роль играет корень композиции?
Итог

Под каждым backend-фреймворком лежат два ортогональных паттерна. Middleware — ось запроса: упорядоченные слои, оборачивающие обработчик и выносящие сквозные заботы — auth, логирование, парсинг тела, rate limiting — чтобы они были написаны один раз и применялись единообразно, а их порядок служил границей безопасности и производительности. Внедрение зависимостей — ось связывания: вместо того чтобы класс конструировал своих коллабораторов через new, он объявляет, что ему нужно, а корень композиции собирает граф объектов один раз на старте, и именно это делает код подменяемым и тестируемым. Одно — конвейер на каждый запрос; другое — граф времени старта. Фреймворки их сплавляют — контроллер NestJS разрешается через DI и сидит за конвейером guard/interceptor — но рефлекс сеньора в том, чтобы спросить, на какой оси живёт проблема. Следующие уроки уходят вглубь каждой: сперва анатомия написания корректного middleware, затем механика инверсии управления и DI-скоупов.

Связанные уроки
встречается в185
Продолжить восхождение ↑Пишем middleware: сигнатуры, next() и три модели фреймворков
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.