Границы и связанность
Что это такое
Граница — линия в системе, за которой меняется стоимость пересечения. Вызов функции превращается в сетевой вызов, одна транзакция — в две, а решение одной команды — в зависимость другой. Связанность показывает, сколько всего приходится менять вместе, когда меняется одна сторона. Проводить границы — значит выбирать, где пройдут дорогие пересечения; связанность же служит мерой того, удачным ли был выбор.
Почему это важно
Любая архитектурная проблема, с которой вы столкнётесь, сводится именно к этому. Изменение, требующее правки четырёх модулей, миграции и согласованного релиза, сильно связано, и никакой чистый код внутри каждого модуля этого не исправит. Границы ещё решают, кто может работать независимо: две команды по разные стороны слабой границы поставляют изменения, не разговаривая друг с другом, а две команды, делящие одну схему, — не могут. Решение принимают один раз, а живут с ним годы, поэтому оно заслуживает большего размышления, чем код, который в нём лежит.
Ключевые идеи
Граница — там, где меняется стоимость вызова
Самая дешёвая граница — вызов функции. Граница модуля позволяет менять внутренности, не трогая вызывающих, если интерфейс остаётся на месте. Граница процесса добавляет сериализацию и режим отказа. Граница сети добавляет задержку, частичный отказ и версионирование. Каждый шаг дороже и покупает больше изоляции, поэтому проводите границы там, где эта изоляция действительно нужна.
вызов -> граница модуля -> граница процесса -> граница сети
стоимость: низкая средняя высокая
покупает: приватность изоляция падений независимые развёртывания
Связанность — это число вещей, которые меняются вместе
Практическая проверка — усиление изменения: сколько файлов, модулей и команд придётся тронуть ради изменения, которое перед вами? Сосчитайте это число до и после предложенной границы — и у вас на руках измерение, а не мнение. Высокая связанность не обязательно неверна: иногда две вещи действительно одно понятие. Однако решение о ней должно быть осознанным, а не случайным.
изменение «добавить поле в заказ»
трогает: orders/ (1 файл) -> слабая связанность
трогает: orders/, billing/, reports/ -> связанность через общую схему
Зависите в одном направлении
Импорты должны указывать от изменчивого к стабильному. Бизнес-правила не должны импортировать веб-фреймворк, ORM или брокер сообщений, потому что те меняются чаще и по причинам, не связанным с правилами. Практическая форма: слой домена объявляет нужные ему интерфейсы, а внешний слой поставляет реализации — та же инверсия, что в SOLID, только применённая к модулям.
http/handler.ts -> domain/order.ts -> domain/ports.ts
(фреймворк никогда не импортируется доменом)
Проводите границы вокруг изменений, а не вокруг существительных
Соблазнительное разделение — по типу вещи: пользователи здесь, заказы там, — потому что оно совпадает с моделью данных. Лучше делить по тому, что меняется вместе: если правило ценообразования меняется каждый квартал, а правила доставки — раз в год, они принадлежат разным сторонам границы, хотя обе живут на заказе. Существительные дают аккуратную диаграмму, а вот скорости изменения дают систему, которую дёшево менять.
Сплочённость — вторая половина
Модуль должен содержать вещи, которые принадлежат друг другу. Если у содержимого модуля нет причины меняться одновременно, модуль делает две работы, и каждое изменение его раздваивает. Высокая сплочённость и низкая связанность — одно и то же наблюдение с двух сторон: вместе то, что меняется вместе.
Практика
Посчитайте усиление одного изменения
Возьмите изменение, которое вы недавно делали: добавили поле, поменяли правило, переименовали понятие. Сосчитайте каждый файл и модуль, который пришлось править, а затем найдите общее понятие, из-за которого возникла каждая лишняя правка.
Готово, когда: вы можете назвать связанность, вызвавшую самую широкую правку, или утверждать, что изменение было действительно локальным.
Проверьте направление зависимостей
Выберите модуль, которым владеете, и перечислите его импорты. Для каждого импорта решите, указывает он на что-то более стабильное или менее стабильное, чем сам модуль.
Готово, когда: каждый импорт либо указывает на что-то более стабильное, либо является швом реализации, который можно заменить в тесте.
Частые ошибки
- Разделение по существительному, потому что диаграмма аккуратная — части, меняющиеся вместе, оказываются врозь, и каждое изменение пересекает их; исправить: группируйте по скорости и причине изменения.
- Попадание фреймворка в домен — обновление фреймворка становится переписыванием бизнес-правил; исправить: держите домен свободным от типов фреймворка и адаптируйте на периферии.
- Сетевая граница без операционной причины — задержка, частичный отказ и версионирование приходят даром; исправить: делайте сервис сервисом только тогда, когда нужно независимое развёртывание или изоляция.
- Общая модель между двумя ограниченными контекстами — переименование поля у одной стороны ломает поведение другой; исправить: каждый контекст владеет своей моделью и транслирует на границе.
- Измерение архитектуры числом слоёв — число слоёв ничего не говорит о связанности; исправить: измеряйте, сколько мест трогает типичное изменение.
Что читать дальше
- Паттерны проектирования (01-cs-fundamentals/18-design-patterns) — как оформить шов, когда граница уже выбрана, поэтому читайте первым.
- Чистый код (01-cs-fundamentals/08-clean-code) — имена и структура, делающие границу читаемой для тех, кто её пересекает.
- Владение данными и состоянием (01-cs-fundamentals/25-software-architecture/02-data-and-state-ownership) — граница, которую труднее всего сдвинуть потом, потому что данные уже по обе стороны.