Границы и связанность

Что это такое

Граница — линия в системе, за которой меняется стоимость пересечения. Вызов функции превращается в сетевой вызов, одна транзакция — в две, а решение одной команды — в зависимость другой. Связанность показывает, сколько всего приходится менять вместе, когда меняется одна сторона. Проводить границы — значит выбирать, где пройдут дорогие пересечения; связанность же служит мерой того, удачным ли был выбор.

Почему это важно

Любая архитектурная проблема, с которой вы столкнётесь, сводится именно к этому. Изменение, требующее правки четырёх модулей, миграции и согласованного релиза, сильно связано, и никакой чистый код внутри каждого модуля этого не исправит. Границы ещё решают, кто может работать независимо: две команды по разные стороны слабой границы поставляют изменения, не разговаривая друг с другом, а две команды, делящие одну схему, — не могут. Решение принимают один раз, а живут с ним годы, поэтому оно заслуживает большего размышления, чем код, который в нём лежит.

Ключевые идеи

Граница — там, где меняется стоимость вызова

Самая дешёвая граница — вызов функции. Граница модуля позволяет менять внутренности, не трогая вызывающих, если интерфейс остаётся на месте. Граница процесса добавляет сериализацию и режим отказа. Граница сети добавляет задержку, частичный отказ и версионирование. Каждый шаг дороже и покупает больше изоляции, поэтому проводите границы там, где эта изоляция действительно нужна.

вызов -> граница модуля -> граница процесса -> граница сети
стоимость:  низкая            средняя              высокая
покупает:  приватность    изоляция падений   независимые развёртывания

Связанность — это число вещей, которые меняются вместе

Практическая проверка — усиление изменения: сколько файлов, модулей и команд придётся тронуть ради изменения, которое перед вами? Сосчитайте это число до и после предложенной границы — и у вас на руках измерение, а не мнение. Высокая связанность не обязательно неверна: иногда две вещи действительно одно понятие. Однако решение о ней должно быть осознанным, а не случайным.

изменение «добавить поле в заказ»
трогает: orders/ (1 файл)             -> слабая связанность
трогает: orders/, billing/, reports/  -> связанность через общую схему

Зависите в одном направлении

Импорты должны указывать от изменчивого к стабильному. Бизнес-правила не должны импортировать веб-фреймворк, ORM или брокер сообщений, потому что те меняются чаще и по причинам, не связанным с правилами. Практическая форма: слой домена объявляет нужные ему интерфейсы, а внешний слой поставляет реализации — та же инверсия, что в SOLID, только применённая к модулям.

http/handler.ts  ->  domain/order.ts  ->  domain/ports.ts
                     (фреймворк никогда не импортируется доменом)

Проводите границы вокруг изменений, а не вокруг существительных

Соблазнительное разделение — по типу вещи: пользователи здесь, заказы там, — потому что оно совпадает с моделью данных. Лучше делить по тому, что меняется вместе: если правило ценообразования меняется каждый квартал, а правила доставки — раз в год, они принадлежат разным сторонам границы, хотя обе живут на заказе. Существительные дают аккуратную диаграмму, а вот скорости изменения дают систему, которую дёшево менять.

Сплочённость — вторая половина

Модуль должен содержать вещи, которые принадлежат друг другу. Если у содержимого модуля нет причины меняться одновременно, модуль делает две работы, и каждое изменение его раздваивает. Высокая сплочённость и низкая связанность — одно и то же наблюдение с двух сторон: вместе то, что меняется вместе.

Практика

Посчитайте усиление одного изменения

Возьмите изменение, которое вы недавно делали: добавили поле, поменяли правило, переименовали понятие. Сосчитайте каждый файл и модуль, который пришлось править, а затем найдите общее понятие, из-за которого возникла каждая лишняя правка.

Готово, когда: вы можете назвать связанность, вызвавшую самую широкую правку, или утверждать, что изменение было действительно локальным.

Проверьте направление зависимостей

Выберите модуль, которым владеете, и перечислите его импорты. Для каждого импорта решите, указывает он на что-то более стабильное или менее стабильное, чем сам модуль.

Готово, когда: каждый импорт либо указывает на что-то более стабильное, либо является швом реализации, который можно заменить в тесте.

Частые ошибки

  • Разделение по существительному, потому что диаграмма аккуратная — части, меняющиеся вместе, оказываются врозь, и каждое изменение пересекает их; исправить: группируйте по скорости и причине изменения.
  • Попадание фреймворка в домен — обновление фреймворка становится переписыванием бизнес-правил; исправить: держите домен свободным от типов фреймворка и адаптируйте на периферии.
  • Сетевая граница без операционной причины — задержка, частичный отказ и версионирование приходят даром; исправить: делайте сервис сервисом только тогда, когда нужно независимое развёртывание или изоляция.
  • Общая модель между двумя ограниченными контекстами — переименование поля у одной стороны ломает поведение другой; исправить: каждый контекст владеет своей моделью и транслирует на границе.
  • Измерение архитектуры числом слоёв — число слоёв ничего не говорит о связанности; исправить: измеряйте, сколько мест трогает типичное изменение.

Что читать дальше