Перейти к основному содержимому

Связность и сцепление модулей

Разработчику Архитектору

Связность и сцепление — зачем это новичку

Два модуля «работают» — но через полгода любая правка тянет десять файлов, тесты падают неожиданно, а новый человек в команде боится трогать utils. Часто причина в границах модулей: слишком слабая связность внутри и слишком сильное сцепление снаружи.

Представьте шкаф с инструментами:

  • Высокая связность — в ящике «электрика» только отвёртки и изолента, всё по одной теме.
  • Низкое сцепление — ящик «электрика» можно вынуть и отнести на объект, не перетаскивая весь гараж.
  • Низкая связность — в одном ящике лежат гвозди, суп и USB-кабель — непонятно, зачем они вместе.
  • Высокое сцепление — чтобы поменить лампочку, нужно разобрать полку с краской, потому что провода проложены «как получилось».

В коде цель та же: внутри модуля — одна задача; между модулями — тонкий, понятный контракт.

На курсах по конструированию ПО эти термины идут парой:

  • Связность (cohesion) — насколько элементы внутри модуля относятся к одной задаче.
  • Сцепление (coupling) — насколько модули зависят друг от друга.

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

Терминология

В русскоязычной литературе встречаются «связность» и «сцепление»; в GRASP — High Cohesion и Low Coupling (паттерны GRASP). Смысл один; не путайте связность (cohesion, внутри) со связанностью (coupling, между).

Подробнее про SOLID и принципы уровня класса — в Принципах проектирования. Здесь — классическая типология модулей, как в учебниках Myers–Constantine и на экзаменах.


Модульность

Модуль — логически завершённая часть системы с явным интерфейсом: пакет, namespace, библиотека, микросервис, файл с классом.

Модульность — свойство системы, при котором:

  • функциональность разделена на части;
  • части взаимодействуют через ограниченные интерфейсы;
  • часть можно понять, протестировать и заменить с минимальным риском для остальных.

Модульность не равна «много мелких файлов». Можно иметь сотню файлов и один «комок грязи», если все импортируют всех. И наоборот — монолит с чёткими модульными границами может быть вполне здоровым.


Связность (cohesion)

Связность модуля — степень, в которой элементы модуля (классы, функции, данные) собраны вокруг одной ответственности и работают для одной цели.

Шкала типов связности (от лучшего к худшему)

ТипСутьПример
ФункциональнаяВсе элементы выполняют одну чётко определённую задачуМодуль PasswordHasher: только хеширование и проверка паролей
ПоследовательнаяВыход одной части — вход для следующей в одной цепочкеПарсер → валидатор → нормализатор одного формата сообщения
КоммуникационнаяЭлементы работают с одними и теми же данными, но разные операцииМодуль CustomerProfile: чтение, обновление, маскирование PII одного агрегата
ПроцедурнаяЭлементы вызываются в фиксированном порядке шагов одного сценарияImportPipeline: load → transform → save (один бизнес-процесс)
ВременнаяЭлементы объединены потому, что выполняются в одно время (инициализация, shutdown)ApplicationBootstrap: старт БД, кэша, логгера при запуске
ЛогическаяВ модуле «однотипные» функции без общей задачиStringUtils, MathUtils — удобно, но слабая семантическая связь
Случайная (coincidental)Элементы попали вместе без причиныmisc.py с форматированием дат, HTTP-клиентом и константами цветов

На экзамене часто просят расставить типы по «силе» или привести пример. Запомните: функциональная — идеал для бизнес-модуля; логическая — допустима для утилит; случайная — признак рефакторинга.

Разбор типов связности простыми словами

ТипОбъяснение «на пальцах»
ФункциональнаяМодуль делает одну понятную работу: «считает цену», «шлёт SMS»
ПоследовательнаяКонвейер: выход шага 1 = вход шага 2, все шаги про один документ
КоммуникационнаяРазные операции над одними данными (профиль клиента: читать, маскировать, обновлять)
ПроцедурнаяОдин сценарий «импорт файла»: шаги идут по порядку, но это разные операции
ВременнаяВсё, что нужно при старте приложения, собрано в Bootstrap
Логическая«Всё про строки» в StringUtils — удобно, но нет одной бизнес-задачи
СлучайнаяПапка misc — признак, что границы не продумали

Cohesion (связность) всегда про внутри одного модуля. Не путайте с coupling (сцеплением) — это про соседей.

Пример — низкая и высокая связность

Низкая связность — один класс «обо всём»:

class OrderService:
def calculate_total(self, order): ...
def send_invoice_email(self, order): ...
def write_audit_log(self, order): ...
def render_pdf(self, order): ...

Четыре разные причины для изменения (цены, шаблон письма, аудит, вёрстка PDF). Это нарушение SRP и случайная/логическая связность «всё про заказ».

Высокая связность — разделение по ответственности:

class OrderCalculator:
def total(self, order): ...

class InvoiceNotifier:
def send(self, order): ...

class AuditLogger:
def record_order_event(self, event): ...

Каждый модуль можно тестировать и менять отдельно.


Сцепление (coupling)

Сцепление — мера зависимости между модулями: насколько изменение в одном модуле заставляет менять другой.

Шкала типов сцепления (от лучшего к худшему)

ТипСутьПример
По даннымМодули обмениваются простыми структурами через параметры; внутренняя реализация скрытаcreate_order(dto: OrderCreateDto) -> OrderId
По метке (stamp)Передаётся структура целиком, модуль использует только часть полейПередача всего User в функцию, которой нужен только email
По управлениюОдин модуль диктует другому, что делать (флаги, ветвления)`process(order, mode="FAST"
Общая область (common)Модули делят глобальные данныеГлобальный singleton AppConfig, мutable shared state
По содержимому (content)Один модуль лезет во внутренности другогоПрямой доступ к приватным полям, SQL другого сервиса
ВнешнееЗависимость от внешней системы, протокола, форматаЖёсткая привязка к конкретному SDK без адаптера

Цель: сцепление по данным через стабильные контракты (DTO, интерфейсы, события). Избегать общих изменяемых глобальных переменных и «проброса» внутренних типов домена наружу.

Разбор типов сцепления для новичка

ТипЧто происходитАналогия
По даннымПередали OrderDto с полями — внутренности модуля скрытыЗаказали блюдо по меню, не заходя на кухню
По метке (stamp)Передали весь объект User, нужен только emailПринесли весь шкаф, чтобы взять одну отвёртку
По управлениюОдин модуль говорит другому как работать флагами mode=FASTМикроменеджмент
Общая областьВсе читают/пишут один глобальный configОбщая тетрадь на весь офис
По содержимомуЛезем в приватные поля и SQL чужого модуляВскрыли двигатель чужой машины
ВнешнееЖёсткая привязка к SDK вендора без адаптераТолько одна марка картриджа

Пример — высокое и низкое сцепление

Высокое сцепление:

// Модуль A знает внутренний класс модуля B
var conn = DatabaseConnectionPool.InternalConnection;
conn.ExecuteRaw(sqlBuiltInModuleA);

Низкое сцепление:

public interface IOrderRepository {
Task SaveAsync(Order order);
}
// Модуль A зависит от интерфейса; B меняет PostgreSQL на Mongo — контракт тот же

Паттерны Dependency Inversion, Adapter, Facade — инструменты снижения сцепления (1112.md).


Связность и сцепление вместе

Два измерения независимы: модуль может быть сильно связан внутри, но при этом жёстко привязан к десяти соседям. Оценивайте оба.

Связность ↓ / Сцепление →Низкое (хорошо)Высокое (плохо)
Высокая (хорошо)Идеал: понятные модули, легко тестировать и менятьМодули «правильные внутри», но больно трогать из-за зависимостей
Низкая (плохо)Много мелочи, дублирование, неясные границыBig Ball of Mud — худший случай

Разбор на примере — «сервис заказов»

Плохо (низкая связность + высокое сцепление): один класс OrderHelper считает скидки, шлёт SMS, пишет в Kafka и знает SQL-схему склада. Любая смена тарифа или провайдера SMS ломает «заказы».

Лучше: OrderPricing (функциональная связность), NotificationPort (интерфейс), OrderRepository (сцепление по данным через DTO). SMS и Kafka — адаптеры за интерфейсом; склад не видит деталей биллинга.

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

На уровне компонентов (NuGet, npm) те же идеи в REP/CCP/CRP — компонентная архитектура.


Сложность программной системы

Сложность — не только «много строк кода». На конструировании смотрят на несколько слоёв:

1. Сложность отдельного модуля (локальная)

  • Цикломатическая сложность (McCabe) — число независимых путей в функции; связь с тестированием (статья с практикой).
  • Глубина вложенности, длина методов, когнитивная нагрузка при чтении.

2. Сложность структуры (глобальная)

  • Число зависимостей между пакетами; циклы в графе модулей.
  • Распространение изменений — сколько модулей перекомпилируется/перетестируется при правке одного.
  • Метрики нестабильности (stable dependencies principle) — в 103.md.

3. Сложность предметной области

Метрики не заменяют ревью. Цикломатическая «10» в чужом коде может быть оправдана; «3» в запутанном switch — нет. Используйте метрики как сигнал, не как KPI ради KPI.


Connascence — «скрытое» сцепление

В современной литературе (Meilir Page-Jones, What Every Programmer Should Know About Object-Orientation) connascence описывает степень согласованности между частями кода: если меняете одно — что ещё обязаны поменять?

ВидСутьПример
Connascence of NameИмена должны совпадатьПоле userId в JSON и в классе
Connascence of TypeТипы должны совпадатьint vs long в протоколе
Connascence of AlgorithmОдин алгоритм в двух местахДублирование формулы скидки в API и в отчёте
Connascence of PositionПорядок аргументов важен(amount, currency) vs (currency, amount)

Правило: чем сильнее connascence, тем ближе связанные элементы должны жить в коде (в одном модуле). Слабые формы (имя, тип) допустимы на границах через контракты; сильные (алгоритм, позиция) — повод для рефакторинга.


Практические приёмы на стадии конструирования

  1. Один модуль — одна причина для изменения (SRP, функциональная связность).
  2. Интерфейсы на границах — DTO на вход API, не «entity наружу».
  3. Запрет циклических import'ов — архитектурные тесты (ArchUnit, NetArchTest, dependency-cruiser для JS).
  4. Code review с вопросами: «Этот класс точно про одно?», «Зачем модуль B знает структуру C?»
  5. Рефакторинг misc и helpers — первый кандидат на разбор по смыслу.
  6. Метрики как сигнал: рост afferent/efferent coupling по пакету — повод нарисовать граф зависимостей до следующего инкремента.

Мини-сценарий рефакторинга

До: модуль Reports импортирует UserEntity из Auth и напрямую читает таблицу orders.

Шаги:

  1. Ввести OrderSummaryDto — сцепление по данным вместо по содержимому.
  2. Вынести запросы в IOrderReadModel — Auth не знает про отчёты.
  3. Разделить «генерацию PDF» и «агрегацию цифр» — две функциональные связности вместо случайной.

После: отчёт меняется без правок в JWT-middleware; тест отчёта — на фake-репозитории.


Частые вопросы на экзамене

Чем связность отличается от сцепления?
Связность — внутри модуля; сцепление — между модулями.

Что лучше: высокая или низкая связность?
Высокая (элементы модуля про одно). Путаница: «низкая связанность модулей» в быту иногда имеют в виду low coupling — это хорошо между модулями.

Может ли модуль иметь логическую связность и быть нормальным?
Да, для утилитных библиотека. Для доменной логики стремитесь к функциональной.


Куда дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).