Правило зависимостей
Зависимости в исходном коде должны указывать в сторону стабильных абстракций. Конкретное зависит от абстрактного — не наоборот. Правило разделяет flow-of-control и flow-of-source-dependency и разрывает цепочки принудительных изменений.
На B2B-платформе заказов был модуль billing и модуль order. Billing зависел от order, order зависел от слоя доступа к данным, тот — от драйвера PostgreSQL. Всё логично: каждый слой зависит от нижележащего. Затем бизнес попросил новый платёжный воркфлоу: billing должен был напрямую вызывать платёжный шлюз, а шлюз — знать о статусе заказа, чтобы предотвратить двойное списание. Добавили модуль payment gateway, который тоже стал зависеть от order. Через два месяца команда захотела вынести billing в отдельный сервис. Не получилось: billing импортировал order, order импортировал слой БД, payment gateway тоже импортировал order, а billing успел импортировать хелперы из payment gateway напрямую. Зависимости образовали паутину. Каждый модуль знал о каждом другом конкретном модуле. Вынести billing означало тащить с собой всю платформу. Команда так и не задала вопрос, который предотвратил бы это: в каком направлении должны указывать зависимости в исходном коде?
Два разных потока: поток управления и зависимость в исходном коде
Путаница, порождающая большинство проблем с зависимостями, — это смешение двух различных вещей, происходящих в коде одновременно: flow of control и flow of source-code dependency.
Flow of control — это то, что выполняется в runtime. Когда модуль billing вызывает orderService.getOrder(id), управление переходит от billing в order service. Это путь выполнения — кто вызывает кого.
Flow of source-code dependency — это то, что видит компилятор. Когда billing содержит import { OrderService } from '../order/orderService', billing имеет зависимость в исходном коде на модуль order. На этапе компиляции, если orderService.ts изменит свой экспортируемый интерфейс, billing потребует перекомпиляции. Это путь зависимости — кто знает о ком.
Эти два потока независимы. Они могут указывать в одну сторону, а могут — в противоположные. Правило зависимостей касается второго потока, а не первого. Управление всегда будет течь туда, куда диктуют runtime-вызовы. Но ты выбираешь, в каком направлении движется знание — импорт, requires, ссылка на тип — между модулями.
Критический вопрос: если модуль изменится, какие другие модули вынуждены знать об этом?
Правило зависимостей: указывай в сторону стабильности
Правило зависимостей Роберта Мартина, впервые сформулированное в контексте Clean Architecture, — это принцип, формализующий направление зависимостей в исходном коде:
Зависимости в исходном коде должны указывать в направлении стабильности.
Стабильность здесь имеет конкретное значение: модуль стабилен, если он маловероятно изменится. Две силы делают модуль стабильным:
- Высокий afferent coupling (Ca): на него зависит много модулей, поэтому он испытывает сильное давление оставаться на месте — его изменение ломает слишком много вызывающих.
- Абстрактность, а не конкретность: он определяет интерфейс или контракт, а не реализацию. Интерфейсы не меняются из-за деталей реализации; они меняются только тогда, когда сам контракт должен эволюционировать.
Правило зависимостей следует из этого: если модуль A зависит от модуля B, то при каждом изменении B модуль A может быть вынужден измениться тоже. Чтобы минимизировать принудительные изменения, A должен зависеть от вещей, которые меняются реже всего — от стабильных, абстрактных — а не от вещей, которые меняются чаще всего — от volatile, конкретных.
На B2B-платформе модуль billing должен зависеть от концепции «что-то, предоставляющее данные о заказе» — интерфейса — а не от конкретного PostgresOrderRepository, который реализует это сегодня. Если реализация переключится на другую БД или команда order проведёт рефакторинг внутренностей репозитория, billing останется нетронутым. Зависимость billing — на контракт, который стабилен, а не на реализацию, которая volatile.
▸Почему это работает
Почему указание в сторону стабильности снижает жёсткость? Потому что жёсткость — невозможность изменить один модуль без каскадных изменений — вызвана зависимостями, идущими от стабильных модулей к volatile. Если billing (относительно стабилен, много вещей его вызывает) зависит напрямую от PostgreSQL-репозитория (volatile, детали реализации меняются), то каждое внутреннее изменение репозитория вынуждает перекомпилировать и проверять billing и каждый модуль, который billing экспортирует. Разворот этого направления — billing зависит только от интерфейса, репозиторий реализует его — означает, что репозиторий может свободно меняться, пока контракт держится. Стабильность становится свойством того, куда указывает стрелка зависимости, а не только того, как часто меняется код.
Конкретное зависит от абстрактного, но не наоборот
Практическая форма правила зависимостей: конкретные вещи зависят от абстрактных, никогда наоборот.
Абстрактный модуль (интерфейс, протокол, порт в терминологии hexagonal) — это контракт. Он определяет, что что-то делает, не уточняя как. Он меняется только когда контракт эволюционирует — что происходит редко и намеренно.
Конкретный модуль (класс, сервис, реализация репозитория) — это единица выполнения. Он реализует контракт с использованием конкретной технологии, алгоритма или внешней зависимости. Он меняется каждый раз, когда нужно изменить реализацию — что происходит часто.
Если конкретный модуль A зависит от другого конкретного модуля B, A должен меняться при каждом изменении реализации B. Это создаёт цепочки принудительных изменений, распространяющихся по кодовой базе. Разворот направления — конкретное зависит от абстрактного — разрывает цепочку.
Зависимость billing от IOrderRepository вместо PostgresOrderRepository — пример: PostgresOrderRepository реализует IOrderRepository. Цепочка зависимостей:
BillingService → IOrderRepository ← PostgresOrderRepositoryОба — billing и реализация Postgres — зависят от интерфейса. Ни один не зависит от другого. Реализацию можно подменить тест-дублером, другой БД или REST API клиентом — и billing не изменится.
▸lesson.inset.note
Слова «стабильный» и «абстрактный» коррелируют, но не тождественны. Очень широко используемый, очень конкретный класс (например, утилитарная библиотека) может быть стабильным на практике, потому что никто его не меняет. Но стабильность по соглашению хрупка — она зависит от социального договора, а не от структурного принуждения. Привязка зависимости к интерфейсу делает стабильность структурной: интерфейс меняется только когда контракт меняется, а не когда меняется реализация. Структура надёжнее соглашения.
Модуль billing содержит строку `import { PostgresOrderRepository } from '../order/infra/postgres-order-repository'`. Что именно неправильно в этом импорте и какое структурное исправление верно?
В runtime BillingService вызывает orderRepository.getOrder(id), который выполняет логику БД и возвращает заказ. На этапе компиляции BillingService импортирует IOrderRepository (интерфейс), а PostgresOrderRepository реализует IOrderRepository. Какое утверждение верно описывает отношение между flow of control и зависимостью в исходном коде?
Команда хочет вынести модуль billing в отдельный сервис. После применения правила зависимостей — сделав все кросс-модульные зависимости указывающими на интерфейсы — вынесение заняло два дня. До применения правила та же операция была заброшена через неделю из-за каскадных цепочек импортов. Какое структурное свойство установило правило зависимостей, сделав вынесение реальным?
- 01В чём разница между flow of control и flow of source-code dependency? Почему правило зависимостей управляет только вторым?
- 02Что означает 'стабильный' в контексте правила зависимостей и почему указание в сторону стабильности снижает жёсткость?
- 03На B2B-платформе, до применения правила зависимостей, почему вынесение модуля billing требовало тащить с собой всю платформу?
Правило зависимостей — это дисциплина о том, в каком направлении указывают импорты в исходном коде, а не о том, в каком направлении идут вызовы в runtime. Flow of control (кто вызывает кого) и flow of source-code dependency (кто импортирует кого) независимы. Правило гласит: зависимости в исходном коде должны указывать в сторону стабильных абстракций.
Стабильность означает маловероятность изменений. Абстрактный модуль — интерфейс, порт, контракт — структурно стабилен, потому что меняется только когда контракт эволюционирует. Конкретный модуль — реализация репозитория, адаптер шлюза, класс фреймворка — volatile, потому что меняется при каждом изменении реализации. Правило зависимостей гласит: volatile конкретные модули зависят от стабильных абстрактных, но не наоборот.
Структурное следствие: конкретное зависит от абстрактного, абстрактное никогда не зависит от конкретного. На B2B-платформе BillingService импортирует IOrderRepository (абстрактный, стабильный). PostgresOrderRepository реализует IOrderRepository. Ни один не зависит от другого напрямую. Управление по-прежнему течёт от billing к репозиторию в runtime; зависимость в исходном коде от обоих идёт к интерфейсу.
Нарушение этого правила — конкретное импортирует конкретное — создаёт цепочки принудительных изменений. Исправление требует введения абстракции на каждой границе, где volatile-модуль импортируется модулем, который должен быть от него изолирован. Следующий урок рассматривает механизм, выполняющий это инвертирование: принцип инверсии зависимостей.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.