Связность и зацепление
Высокая связность означает, что элементы модуля совместно обслуживают одну задачу и используют близкие данные. Низкое зацепление означает, что соседние модули знают друг о друге минимум, достаточный для контракта. Хороший дизайн стремится к обоим свойствам, но не измеряет их числом пакетов или интерфейсов.
Зачем это на интервью
Вопросы о «чистом коде» часто проверяют способность объяснить последствия изменения. Кандидат должен увидеть, что 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, которое меняется реже его реализаций и выражает нужду потребителя.
Типовые вопросы
- Может ли модуль быть высокосвязным и сильно зацепленным?
- Да. Хорошо собранный модуль может зависеть от конкретных деталей многих соседей; внутреннее качество не отменяет внешнюю стоимость изменений.
- Всегда ли нужно уменьшать зацепление?
- Нет. Нулевая связь невозможна в работающей системе. Нужны явные, минимальные зависимости там, где есть реальная кооперация.
- Почему общий пакет
utilsчасто проблемный?- В него попадают несвязанные функции, он становится зависимостью всех слоёв и создаёт случайную связанность и скрытую платформу.
- Что делать с циклическим импортом в Go?
- Найти смешанную ответственность; перенести общий тип в нейтральный пакет или, чаще, определить маленький интерфейс у потребителя.
- Как общая БД создаёт зацепление?
- Сервисы зависят от одной схемы, миграций, блокировок и прав. Любое изменение таблицы может стать координированным релизом.
- Как обнаружить скрытый контракт?
- Ищите неявный формат данных, порядок вызовов, 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.