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

Высокая связность означает, что элементы модуля совместно обслуживают одну задачу и используют близкие данные. Низкое зацепление означает, что соседние модули знают друг о друге минимум, достаточный для контракта. Хороший дизайн стремится к обоим свойствам, но не измеряет их числом пакетов или интерфейсов.

Зачем это на интервью

Вопросы о «чистом коде» часто проверяют способность объяснить последствия изменения. Кандидат должен увидеть, что UserService, который и отправляет письма, и строит SQL, и считает тариф, имеет низкую связность; а пакет, зависящий от конкретной БД, HTTP-клиента и глобального конфига, трудно менять и тестировать.

Минимум для E4

  • Объяснять связность как внутреннюю согласованность ответственности, а зацепление как зависимость между модулями.
  • Замечать широкие интерфейсы, общие изменяемые данные и протекание деталей реализации через API.
  • Выбирать маленький контракт потребителя вместо зависимости от конкретного клиента или «god interface».
  • Проверять изменение: сколько пакетов, тестов и команд оно затрагивает.

Пакет billing с расчётом суммы, правилами округления и состоянием счёта может быть высокосвязным: всё относится к одной модели. Если туда добавить отправку Slack, создание HTTP-ответа и миграции БД, причины изменения смешаются. Обратная ошибка — разнести одну операцию по десятку «универсальных» пакетов, где для понимания формулы приходится прыгать между слоями.

Зацепление бывает явным и скрытым. Явная зависимость — импорт пакета и вызов его метода. Скрытая — формат записи в Redis, переменная окружения, порядок запуска, глобальный singleton, договорённость о поле JSON. Скрытые контракты особенно опасны: компилятор не покажет их нарушение, поэтому нужны schema-тесты, versioning и наблюдаемость.

type Clock interface {
    Now() time.Time
}
 
type TokenIssuer struct {
    clock Clock
    ttl   time.Duration
}
 
func (i TokenIssuer) Expiry() time.Time {
    return i.clock.Now().Add(i.ttl)
}

Здесь TokenIssuer зацеплен с узким понятием времени, а не с глобальным time.Now или большим инфраструктурным контейнером. Такая зависимость оправдана, если время влияет на правило и его нужно детерминированно тестировать; иначе интерфейс может быть излишним.

Углубление для E5/Senior

Изменение зацепления — это изменение архитектурной стоимости. Синхронный RPC связывает доступность и latency сервисов; общая база связывает схему, миграции и права; общий пакет доменных моделей связывает release cycle команд. Иногда эта цена оправдана строгой консистентностью или простым продуктом, но её нужно признать и измерять.

Оценивайте не только количество зависимостей, но и их направленность, стабильность и силу. Зависеть от стабильного стандарта или малого бизнес-контракта обычно дешевле, чем от быстро меняющейся реализации. Циклические импорты в Go — явный сигнал: две части не имеют самостоятельной границы или общий контракт надо переместить к потребителю.

Высокая связность помогает организовать ownership: команда, тесты, документация и метрики сходятся вокруг одной возможности продукта. Но «один модуль на домен» не абсолютен: read-модель, запись и интеграция могут иметь разные оптимальные границы. Проводите их по данным, согласованности и независимой эволюции API.

Ключевые понятия

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

Типовые вопросы

  1. Может ли модуль быть высокосвязным и сильно зацепленным?
    • Да. Хорошо собранный модуль может зависеть от конкретных деталей многих соседей; внутреннее качество не отменяет внешнюю стоимость изменений.
  2. Всегда ли нужно уменьшать зацепление?
    • Нет. Нулевая связь невозможна в работающей системе. Нужны явные, минимальные зависимости там, где есть реальная кооперация.
  3. Почему общий пакет utils часто проблемный?
    • В него попадают несвязанные функции, он становится зависимостью всех слоёв и создаёт случайную связанность и скрытую платформу.
  4. Что делать с циклическим импортом в Go?
    • Найти смешанную ответственность; перенести общий тип в нейтральный пакет или, чаще, определить маленький интерфейс у потребителя.
  5. Как общая БД создаёт зацепление?
    • Сервисы зависят от одной схемы, миграций, блокировок и прав. Любое изменение таблицы может стать координированным релизом.
  6. Как обнаружить скрытый контракт?
    • Ищите неявный формат данных, порядок вызовов, environment variables и глобальное состояние; фиксируйте их схемой, тестом или явным API.

Практика

  • Постройте карту импортов для небольшого Go-модуля. Критерий: для каждой зависимости указаны причина, владелец и альтернатива при изменении реализации.
  • Удалите цикл между двумя пакетами. Критерий: общий контракт расположен у потребителя или в нейтральном пакете, а go test ./... проходит без изменения внешнего поведения.
  • Разберите интеграцию через общую таблицу. Критерий: перечислены схема, миграции, права и release-порядок как явные контракты либо предложен версионированный API/событие.

Частые ошибки и ловушки

  • Считать, что много маленьких пакетов автоматически означает низкое зацепление.
  • Создавать интерфейс до появления второго варианта или необходимости в изолированном тесте.
  • Прятать важный контракт в глобальной конфигурации, JSON-поле или общей таблице.
  • Лечить циклический импорт пакетом common, который затем импортируют все.
  • Игнорировать операционное зацепление: общий rate limit, очередь, база и deploy pipeline тоже связывают компоненты.

Связанные темы

Принципы проектирования · Разделение ответственности · Композиция вместо наследования · Проектирование Go-приложений · System Design · Наблюдаемость

Источники

  • Larry Constantine, The Structured Design Approach, о cohesion и coupling.
  • John Ousterhout, A Philosophy of Software Design, главы о deep modules и complexity.
  • Robert C. Martin, Clean Architecture, главы о направлении зависимостей.
  • Документация Go: Package names и правила импортов в Go Specification.