Формы coupling
Afferent и efferent coupling измеряют направление зависимостей. Лестница coupling идёт от content до data. Более слабый coupling не всегда лучше — правильный уровень зависит от реальных границ изменений домена.
Команда billing в B2B SaaS-компании решила изменить логику расчёта налогов. Простое изменение, думали они — один файл, пара часов. Вместо этого ушло три дня. Функция расчёта налогов напрямую читала внутренние строки БД модуля заказов, интерпретировала сырую раскладку колонок и ветвилась по внутреннему целочисленному статусу, который имел разный смысл в разных подсистемах. Когда нашли все места, зависящие от старого поведения, тронули одиннадцать файлов и внесли две регрессии. Никто не проектировал этот coupling намеренно. Он вырос по одной маленькой зависимости за раз, и никто его не измерял.
Afferent и efferent coupling
У каждого модуля системы есть два показателя coupling, которые важны по отдельности:
Afferent coupling (Ca) — количество модулей, которые зависят от данного: сколько других частей системы импортирует или вызывает его. Модуль с высоким afferent coupling широко используется. На него давит стабильность: если он изменится, сломается многое. Billing-движок в B2B-платформе имеет высокий Ca — от него зависят сервис заказов, финансовая панель, инструменты администрирования и ERP-интеграция.
Efferent coupling (Ce) — количество модулей, от которых зависит данный: сколько других частей системы он импортирует или вызывает. Модуль с высоким efferent coupling хрупок: он чувствителен к изменениям во многих других модулях. Модуль, импортирующий репозиторий заказов, tax-сервис, discount-движок и платёжный шлюз — высоко efferent. Любое изменение в любом из этих четырёх каскадирует в него.
Отношение Ce / (Ca + Ce) даёт грубую оценку нестабильности (метрика Роберта Мартина). Модуль с Ce = 8 и Ca = 2 имеет нестабильность ≈ 0.8 — он зависит от многих вещей, но зависит от него немного. Модуль с Ce = 0 и Ca = 20 максимально стабилен — ничто из его зависимостей не сломает его, но всё, что его использует, подвержено его изменениям.
Лестница coupling
Не весь coupling одинаков. Page-Jones (1988) и Yourdon & Constantine определили классическую таксономию, ранжирующую формы coupling от самой жёсткой до самой мягкой. Чем слабее coupling, тем независимее оба модуля могут эволюционировать.
Content coupling (самый жёсткий): модуль A напрямую обращается к внутренним структурам данных или деталям реализации модуля B. Функция расчёта налогов из открытой истории читала сырые строки БД модуля заказов — это content coupling. Любое изменение внутреннего устройства модуля заказов ломает функцию налогов. Coupling невидим на уровне интерфейса; он существует в коде, который полностью обходит интерфейс.
Common coupling: оба модуля разделяют глобальную изменяемую переменную или общее хранилище данных. Два сервиса, разделяющих таблицу БД и оба пишущих в неё напрямую — это common coupling. Любой из них может испортить общее состояние так, что это сломает другой, и ни один не контролирует контракт данных.
Control coupling: модуль A передаёт модулю B флаг или код, управляющий его поведением. processOrder(order, mode: 'dry-run' | 'live') — control coupling, если billing-движок ветвится по флагу mode. Вызывающий должен знать о внутренних путях выполнения вызываемого — coupling находится в потоке управления, а не только в данных.
Stamp coupling (или data-structure coupling): модуль A передаёт модулю B большую структуру данных, а модуль B использует только её часть. Передача полного объекта Order, когда billing нужны только order.items и order.customerId — stamp coupling. Модули связаны с формой общей структуры даже по полям, которые ни один из них не должен трогать.
Data coupling (самый мягкий): модули A и B общаются только через простые, чётко определённые параметры — ровно те данные, что нужны. Billing-движок принимает (items: LineItem[], customerId: string) и возвращает (invoiceId: string, amount: Money). Ни один модуль ничего не знает о внутренностях другого. Это золотой стандарт.
▸Почему это работает
Почему порядок лестницы важен? Потому что чем жёстче coupling, тем больше координации требуется при изменении любого из модулей. Content coupling означает, что любой внутренний рефакторинг в модуле B рискует сломать модуль A. Common coupling означает, что любая запись в общее состояние со стороны A может испортить чтение B невидимым способом, обнаруживаемым только в runtime. Control coupling означает, что A должен обновляться, когда B добавляет новые режимы выполнения. Stamp coupling означает, что оба модуля связаны с общей формой данных, которая может расти или меняться по причинам, которые ни один из них не контролирует. Каждый шаг вниз по лестнице сокращает радиус взрыва от изменения одного модуля на другой.
Почему слабее — не всегда лучше
Вот структурная ловушка: команды, понявшие лестницу coupling, иногда заключают «data coupling везде, всегда». Это неверно, и типичный сбой — фрагментация.
Рассмотрим сервис заказов в B2B-платформе. Агрегат Order содержит header, lineItems, pricing, taxDetails, approvalState и auditTrail. Если каждая внутренняя функция, обращающаяся к данным заказа, рефакторится так, чтобы принимать только минимальные нужные примитивы, получается:
updateApprovalState(orderId, state, actorId, timestamp)— data coupled, чисто.recalcPricing(orderId, lineItems, customerId, discountCode)— data coupled, чисто.computeTax(lineItems, shippingAddress, taxClassification)— data coupled, чисто.
Теперь координация изменения, затрагивающего approval state, pricing и tax одновременно, требует оркестратора, вызывающего все три функции, собирающего результаты и пересобирающего обновлённый заказ. Оркестратор теперь жёстко связан со всеми тремя — он знает полный жизненный цикл заказа. Coupling переехал вверх по стеку вызовов, а не исчез.
Вопрос не «слабо ли это связано?», а «находится ли coupling в правильном месте и отражает ли он реальные инварианты, которые должны изменяться вместе?» Высокая cohesion (рассматривается в следующем уроке) — это противодействующая сила.
▸lesson.inset.note
Практическая эвристика: смотри на то, что изменяется вместе в продакшене. Если каждый раз, когда billing-команда деплоит изменение в генерацию инвойсов, приходится трогать структуры данных модуля заказов — coupling неправильный, независимо от позиции на лестнице. Coupling — это сила, а не цель. Цель — coupling, соответствующий реальным границам изменений домена.
Billing-движок читает записи заказов напрямую из таблицы БД, которой владеет сервис заказов, интерпретируя сырые значения колонок. После того как команда заказов переименовала колонку и добавила nullable-поле, billing молча начал генерировать неправильные инвойсы. Какая форма coupling стала причиной?
BillingEngine имеет Ca = 12 (двенадцать модулей зависят от него) и Ce = 1 (он зависит только от интерфейса PaymentGateway). Что это говорит о его стабильности и что это означает для подхода к его интерфейсу?
Команда рефакторит billing-движок так, чтобы каждая функция принимала только минимальные нужные примитивы, достигая data coupling повсюду. Теперь billing-движок вызывает двенадцать отдельных функций для обработки заказа. Отладка расхождения в billing требует трассировки через все двенадцать. В чём реальная структурная проблема?
- 01Что такое afferent coupling и почему высокий afferent coupling делает модуль стабильным?
- 02Пройди по лестнице coupling от самого жёсткого до самого мягкого, одним предложением на каждую ступень.
- 03Почему достижение data coupling везде не устраняет coupling?
Coupling измеряется в двух измерениях. Направление: afferent coupling (Ca) считает, сколько модулей зависит от данного; efferent coupling (Ce) считает, сколько зависит от других этот. Модуль с высоким Ca и низким Ce стабилен — он поглощает мало изменений из окружения, но широко распространяет изменения, когда они происходят. Метрика нестабильности Ce/(Ca+Ce) это количественно выражает.
Вид: лестница coupling идёт от content coupling (прямой доступ к внутренностям другого модуля, самая жёсткая и хрупкая форма) через common coupling (общее изменяемое состояние), control coupling (передача управляющих флагов) и stamp coupling (избыточные структуры данных) до data coupling (минимальные, точные параметры — самая мягкая форма). Каждый шаг вниз по лестнице сокращает объём того, что изменяется вместе при эволюции одного модуля.
Ловушка — заключение, что data coupling везде всегда лучше. Слабый coupling на каждом попарном интерфейсе хорош; чрезмерная декомпозиция, распределяющая координационную логику по неявному оркестратору — нет. Правильный уровень coupling — тот, что соответствует реальным границам изменений домена: именно то, что помогает определить высокая cohesion, тема следующего урока.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.