Связность и сцепление модулей
Связность и сцепление — зачем это новичку
Два модуля «работают» — но через полгода любая правка тянет десять файлов, тесты падают неожиданно, а новый человек в команде боится трогать 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. Сложность предметной области
- Много правил, исключений, интеграций — DDD и bounded contexts (доменная модель, типы классов в DDD).
Метрики не заменяют ревью. Цикломатическая «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, тем ближе связанные элементы должны жить в коде (в одном модуле). Слабые формы (имя, тип) допустимы на границах через контракты; сильные (алгоритм, позиция) — повод для рефакторинга.
Практические приёмы на стадии конструирования
- Один модуль — одна причина для изменения (SRP, функциональная связность).
- Интерфейсы на границах — DTO на вход API, не «entity наружу».
- Запрет циклических import'ов — архитектурные тесты (ArchUnit, NetArchTest, dependency-cruiser для JS).
- Code review с вопросами: «Этот класс точно про одно?», «Зачем модуль B знает структуру C?»
- Рефакторинг
miscиhelpers— первый кандидат на разбор по смыслу. - Метрики как сигнал: рост afferent/efferent coupling по пакету — повод нарисовать граф зависимостей до следующего инкремента.
Мини-сценарий рефакторинга
До: модуль Reports импортирует UserEntity из Auth и напрямую читает таблицу orders.
Шаги:
- Ввести
OrderSummaryDto— сцепление по данным вместо по содержимому. - Вынести запросы в
IOrderReadModel— Auth не знает про отчёты. - Разделить «генерацию PDF» и «агрегацию цифр» — две функциональные связности вместо случайной.
После: отчёт меняется без правок в JWT-middleware; тест отчёта — на фake-репозитории.
Частые вопросы на экзамене
Чем связность отличается от сцепления?
Связность — внутри модуля; сцепление — между модулями.
Что лучше: высокая или низкая связность?
Высокая (элементы модуля про одно). Путаница: «низкая связанность модулей» в быту иногда имеют в виду low coupling — это хорошо между модулями.
Может ли модуль иметь логическую связность и быть нормальным?
Да, для утилитных библиотека. Для доменной логики стремитесь к функциональной.
Куда дальше
- Конструирование: понятие и ЖЦ
- Модели жизненного цикла
- GRASP: High Cohesion, Low Coupling
- Цикломатическая сложность
- Чек-лист раздела
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). Что такое конструирование программного обеспечения, как оно связано с другими стадиями SDLC, какие артефакты и стандарты применяются на этапе реализации. Классический, инкрементный, RAD, спиральный и компонентно-ориентированный подходы — как они влияют на стадию конструирования ПО. Планирование производства компонентов: диаграмма Ганта, критический путь, PERT, Planning Poker и связь с тестированием. Языки программирования, проектирования, спецификации и конфигурации на стадии конструирования ПО — роли, примеры, выбор. Краткие итоги раздела «Конструирование ПО». Вопросы для закрепления раздела «Конструирование ПО» с отсылками к статьям энциклопедии.Конструирование ПО — понятие, жизненный цикл, стандарты
Модели жизненного цикла для конструирования
Планирование конструирования — PERT, CPM, оценки
Языки конструирования программных систем
Итоги — конструирование ПО
Чек-лист самопроверки — конструирование ПО