$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогКарта зависимостей: как находить скрытые циклы и утечки ответственности в проекте

Карта зависимостей: как находить скрытые циклы и утечки ответственности в проекте

$ sudo teach IT
·24 июля 2026 г.·10 мин·51
Карта зависимостей: как находить скрытые циклы и утечки ответственности в проекте

Разберём методы визуализации связей модулей/сервисов и поиска нежелательных зависимостей. Подскажем, как рефакторить границы без переписывания всего.

Содержание
Что именно считать зависимостью: уровни, направление, тип связиИмпортные зависимости (compile-time / build-time)Зависимости на уровне архитектуры (layering)Зависимости на уровне исполнения (runtime)Зависимости по даннымКарта зависимостей как инженерный инструмент, а не картинкаКак визуализировать зависимости модулей: практические подходыПодход 1: импортные графы (статический анализ)Подход 2: граф архитектурных слоёв (layer graph)Подход 3: runtime-связи и вызовы (observability)Поиск скрытых циклов: как отличить “плохой цикл” от “нормального сообщества”Практика: поиск SCC в импортном графеПочему циклы часто “скрыты”Утечки ответственности: когда граф зависимостей “неявно ломает архитектуру”Как это увидеть на картеТипичная форма утечки: “утилитарный модуль”Рефакторинг границ без переписывания всего: стратегии разрыва зависимостейСтратегия 1: разрывайте циклы через контракты (порт ↔ адаптер)Стратегия 2: “поднять” типы и исключения в общий доменный контрактСтратегия 3: ограничение зависимостей в коде (lint/CI guardrails)Стратегия 4: “вынести” инфраструктурные штуки в края и заменить прямые вызовы на события/очереди (аккуратно)Стратегия 5: итеративное снижение “межслойных” реберМини-кейс: от “лапши импортов” к управляемым границамКак сделать карту полезной: фильтры, матрицы и “порог критичности”1) Смотрите на матрицу зависимостей, а не только на граф2) Используйте топ-списки3) Фиксируйте регресс по изменению картыПодводные камни: что ломает интерпретацию карты

В зрелом проекте “зависимости” — это не просто связи между модулями в коде. Это способ организации ответственности, гарантий и допущений: кто что знает, кто кому доверяет и где именно со временем накапливается технический долг. Когда связи становятся неуправляемыми, возникают два типичных симптома:

  1. Скрытые циклы (неочевидные обратные связи по направлению вызовов или данных).
  2. Утечки ответственности (модуль “впитывает” чужие полномочия: начинает обращаться к инфраструктуре, знать доменную логику, смешивает уровни абстракций).

Оба явления часто проявляются не сразу. Сначала это ощущается как “сложно менять”, потом — как “опасно трогать”, а затем — как “всё держится на удаче”. Ключевой инструмент против этой деградации — карта зависимостей: визуализированная модель того, как элементы системы связаны, и какие зависимости возникают при сборке, импортах, вызовах и передаче данных.

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


Что именно считать зависимостью: уровни, направление, тип связи

Прежде чем строить “карту”, важно договориться о модели. Обычно зависимости бывают нескольких видов:

Импортные зависимости (compile-time / build-time)

Например, в TypeScript/Java/Python: кто от кого импортирует. Это фиксируется статически и удобно для визуализации.

Зависимости на уровне архитектуры (layering)

Например: “домен не должен знать про БД”, “инфраструктура не должна напрямую вызывать адаптеры”, “UI не должен вызывать репозитории напрямую”.

Это уже не столько “техническая” зависимость, сколько архитектурное ограничение, которое можно проверить по фактам: где лежат классы, как они ссылаются, какие слои имеют право зависеть от каких.

Зависимости на уровне исполнения (runtime)

Когда модуль/сервис вызывает другой модуль, или когда события/сообщения “ведут” в обратные направления. Такие циклы могут отсутствовать в импортах, но появляться в логике: через событийную шину, коллбеки, клиентские вызовы, очереди, RPC и т.д.

Зависимости по данным

Даже если нет “вызовов”, есть обмен схемами, DTO, событиями. Если доменная модель начинает “подстраиваться” под инфраструктуру, это тоже разновидность утечки ответственности.

Вывод: карту зависимостей можно и нужно строить с разных уровней, но начните с того, что даёт максимальную ясность быстро — обычно импортные зависимости и отношения “слой ↔ слой”.


Карта зависимостей как инженерный инструмент, а не картинка

Цель карты — не красивая схема для презентации, а средство расследования:

  • Где образовался цикл, который затрудняет изменения?
  • Какие направления зависимостей нарушают архитектурные правила?
  • Какие модули “разрастаются” и начинают тащить чужие зависимости?
  • Где интерфейсы “размываются”: вместо контрактов начинают использоваться детали реализации?

Чтобы карта работала, она должна быть:

  • обновляемой (хотя бы в CI),
  • сопоставимой между версиями (чтобы видеть дрейф),
  • достаточно точной (чтобы ошибки были воспроизводимыми),
  • с фильтрами (иначе диаграмма превращается в “лапшу” без смысла).

Как визуализировать зависимости модулей: практические подходы

Подход 1: импортные графы (статический анализ)

Это самый быстрый путь. Идея: собрать граф “A импортирует B” и визуализировать.

JavaScript/TypeScript: популярные инструменты строят dependency graph по import/require. В Python — по import в AST. В Java — по bytecode/модулям.

На практике важны два решения:

  1. Что считать узлом графа?
    • папка,
    • пакет,
    • библиотека,
    • модуль (файл),
    • класс.
  2. Как считать ребро?
    • только прямые импорты,
    • или транзитивные зависимости,
    • учитывать ли динамические импорты.

Чем “мелче” узлы — тем точнее диагностика, но тем больше шума. Начните с уровня “пакет/директория”, а “разобраться глубже” можно уже по проблемным узлам.

Подход 2: граф архитектурных слоёв (layer graph)

Если в коде есть условные слои (domain, application, infrastructure, ui), полезно строить не только граф модулей, но и граф между слоями. Он покажет:

  • насколько слой “чистый”,
  • кто кого нарушает,
  • где архитектура “плывёт” со временем.

Технически это делается маппингом: каждому узлу назначается слой по пути или по метаданным, а дальше считается матрица зависимостей “слой → слой”.

Подход 3: runtime-связи и вызовы (observability)

Для циклов по исполнению (например, события, запросы, RPC) статический граф может быть неполным. Тут помогают:

  • трассировка (distributed tracing),
  • анализ логов корреляцией,
  • графы на основе зависимостей сервисов (service-to-service).

Карта становится “динамической”: узлы — сервисы, ребра — реальные запросы/сообщения, а также их тип.


Поиск скрытых циклов: как отличить “плохой цикл” от “нормального сообщества”

Циклы бывают разными:

  • Короткие технические циклы между модулями (часто из-за удобного “импорта по пути”).
  • Длинные поведенческие циклы через события (“инициатор публикует → подписчик делает и публикует обратно”).
  • Нормальные зависимости на уровне контрактов (например, общий интерфейс, который цитируют и домен, и инфраструктура).

Для диагностики удобно использовать понятия из теории графов:

  • SCC (Strongly Connected Components) — сильно связные компоненты. Это множества узлов, внутри которых достижимость взаимная.
  • Цикл по направлению — когда граф “слой → слой” содержит обратные направления, противоречащие архитектурным правилам.

Практика: поиск SCC в импортном графе

Допустим, вы получили граф импорта. Тогда:

  1. Строите направленный граф: ребро A -> B означает “A зависит от B”.
  2. Находите SCC.
  3. Все SCC размером > 1 почти всегда требуют внимания: это кандидаты на утечки границ или циклические контрактные связи.

Обычно “нормальные” случаи — когда цикл возникает из-за общего типа (например, shared models). Но “технические” циклы чаще связаны с тем, что один слой начинает импортировать детали другого.

Почему циклы часто “скрыты”

Цикл может отсутствовать в очевидной форме, но появляться через:

  • события и реакторы,
  • обратные вызовы (callbacks),
  • фабрики, которые создают “всё подряд”,
  • сериализацию доменных объектов “как есть” (тем самым слои становятся зависимыми друг от друга).

Поэтому SCC — лишь первый фильтр. Далее нужно подтверждение: что именно связывает цикл и можно ли разорвать его контрактом.


Утечки ответственности: когда граф зависимостей “неявно ломает архитектуру”

Утечки ответственности проявляются не только в циклах. Иногда зависимости “вроде бы односторонние”, но уровень абстракции смешан. Признаки:

  • Доменный модуль импортирует инфраструктурный (например, знает про ORM-репозитории, HTTP-клиенты, файловые системы).
  • UI слой начинает использовать детали транзакций, маппинг БД или логику идемпотентности.
  • Приложение знает о конкретной очереди/топике и формате ретраев.
  • Инфраструктура начинает содержать доменную логику: правила, которые “должны жить в domain”.

Как это увидеть на карте

Есть простой способ: ввести правила архитектуры и проверить их:

  • Какие импорты запрещены между слоями?
  • Какие зависимости разрешены (например, только application -> domain, infrastructure -> application через интерфейсы)?
  • Какие типы “утекают” вниз: DTO, сущности, исключения, конфигурация?

Даже без сложных формальных проверок можно:

  • агрегировать импортные зависимости по категориям,
  • смотреть “топ узлов” по входящим/исходящим ребрам,
  • выявлять “черные дыры”: модуль, к которому стекаются зависимости из многих мест.

Типичная форма утечки: “утилитарный модуль”

Часто появляется общий модуль utils, который постепенно превращается в “всё для всех”:

  • в него складывают бизнес-правила,
  • он начинает импортировать инфраструктуру,
  • через него проходят ссылки “в обе стороны”.

На карте он выглядит как узел с огромным числом ребер и как точка пересечения SCC (или “мостик” для почти всех циклов). Лечится разделением на несколько слоёв и введением интерфейсов вместо прямых импортов.


Рефакторинг границ без переписывания всего: стратегии разрыва зависимостей

Полезное правило: не пытайтесь “разорвать все циклы сразу”. Делайте постепенные, проверяемые шаги, иначе вы упрётесь в каскадные изменения.

Ниже — несколько стратегий, которые работают именно в существующих кодовых базах.

Стратегия 1: разрывайте циклы через контракты (порт ↔ адаптер)

Если цикл существует потому, что одна сторона “знает детали” другой, сделайте контракт:

  • домен/приложение объявляет интерфейс (порт),
  • инфраструктура реализует его (адаптер),
  • связь проходит через инверсию зависимостей.

Пример (условно на TypeScript): доменный слой не должен знать про конкретный storage.

code
// domain/ports/UserRepository.ts
export interface UserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
}
code
// infrastructure/db/UserRepositoryPg.ts
import { UserRepository } from "../../domain/ports/UserRepository";
import { User } from "../../domain/entities/User";

export class UserRepositoryPg implements UserRepository {
  async findById(id: string): Promise<User | null> {
    // обращение к БД ...
    return null;
  }

  async save(user: User): Promise<void> {
    // insert/update ...
  }
}
code
// application/services/UserService.ts
import { UserRepository } from "../../domain/ports/UserRepository";

export class UserService {
  constructor(private readonly repo: UserRepository) {}

  async register(id: string) {
    const user = /* ... доменная логика ... */;
    await this.repo.save(user);
  }
}

Что важно: контракт живёт в “верхнем” слое, реализация — в “нижнем”. Так вы обычно ломаете обратные зависимости и уменьшаете SCC.

Стратегия 2: “поднять” типы и исключения в общий доменный контракт

Утечки часто происходят из-за того, что:

  • домен выбрасывает исключения инфраструктуры,
  • приложение опирается на DTO из API,
  • модель события копирует схему транспорта.

Решение — определить канонические типы:

  • доменные события/команды (или хотя бы доменные интерфейсы),
  • доменные исключения (или коды ошибок, инвариантные к инфраструктуре),
  • контрактные модели, не привязанные к транспорту.

Вместо того чтобы “доменные сущности сериализовать как есть”, введите слой преобразований. Это может быть отдельный модуль “mappers”, но следите, чтобы он не стал свалкой.

Стратегия 3: ограничение зависимостей в коде (lint/CI guardrails)

Чтобы рефактор не “развалился” через две недели, зафиксируйте правила:

  • запрещён импорт из domain в infrastructure,
  • запрещено импортировать “хардовые” библиотеки из неправильного слоя,
  • запрещено использовать конкретные HTTP/DB клиенты в домене.

Технически это делается через:

  • линтеры правил импорта,
  • скрипты проверки графа зависимостей в CI,
  • архитектурные тесты (например, в виде “dependency matrix” и порогов).

Даже минимальный guardrail резко снижает регрессы.

Стратегия 4: “вынести” инфраструктурные штуки в края и заменить прямые вызовы на события/очереди (аккуратно)

Если цикл поведенческий и держится на синхронных вызовах, иногда лучше сделать границу асинхронной:

  • приложение публикует событие домена,
  • адаптер доставки отправляет в очередь/шину,
  • обработчик на другой стороне вызывает нужные сервисы.

Но здесь подводный камень: можно получить “зависимость по данным” (когда формат события начинает тянуть инфраструктуру). Тогда всё равно придётся формализовать контракт события на уровне домена или приложения.

Стратегия 5: итеративное снижение “межслойных” ребер

Практический подход, если проект большой:

  1. Выберите 3–5 наиболее проблемных узлов по карте (высокие входящие/исходящие ребра, SCC).
  2. Для каждого узла определите, какая ответственность лишняя.
  3. Разделите модуль на два-три меньших по роли (интерфейсы/контракт, логика, адаптация).
  4. Обновите зависимости постепенно: сначала интерфейсы, затем реализации, затем “внутренние” типы.

Так вы избегаете “переписывания всего” и сохраняете историю изменений.


Мини-кейс: от “лапши импортов” к управляемым границам

Представим типичную ситуацию в средних проектах:

  • domain импортирует infrastructure/config — потому что нужно значение фичей.
  • infrastructure импортирует application/useCases — потому что там “удобная” функция для валидации.
  • application импортирует domain — “всё логично”.
  • В итоге получается SCC между domain, application и infrastructure.

Что делают команды обычно (ошибка):

  • “переносим конфиг в domain” (а значит, домен начинает знать про источник данных конфигурации),
  • “переносим валидацию в infrastructure” (и домен теряет инварианты),
  • в итоге SCC просто смещается.

Что работает:

  1. В application или domain оставляем интерфейс FeatureFlags:
    code
    // domain/ports/FeatureFlags.ts
    export interface FeatureFlags {
      isEnabled(flag: string): boolean;
    }
    
  2. infrastructure реализует этот интерфейс через конкретный конфиг-стор:
    code
    // infrastructure/config/FeatureFlagsLocal.ts
    import { FeatureFlags } from "../../domain/ports/FeatureFlags";
    export class FeatureFlagsLocal implements FeatureFlags {
      constructor(private readonly flags: Record<string, boolean>) {}
      isEnabled(flag: string) { return !!this.flags[flag]; }
    }
    
  3. Использование фичей проходит через инъекцию зависимостей — домен не импортирует инфраструктуру.
  4. Функции “для валидации” выносятся в domain как часть доменной логики или в application как оркестрация, но так, чтобы не приходилось импортировать обратно.

На карте зависимостей SCC распадается, а направления становятся контролируемыми. И самое важное — появляется механизм, который удерживает границы: контракты и guardrails.


Как сделать карту полезной: фильтры, матрицы и “порог критичности”

Чтобы карта не стала “схемой на стену”, введите практики:

1) Смотрите на матрицу зависимостей, а не только на граф

Матрица “слой → слой” часто компактнее. Если в проекте слои А, B, C, вы получите количество ребер или факт “есть/нет зависимости” по паре слоёв. Это быстро показывает нарушения.

2) Используйте топ-списки

  • Узлы с максимальным числом входящих ребер (куда “течёт” зависимость).
  • Узлы с максимальным числом исходящих ребер (кто “тянет” за собой всё).

Это приближает к местам, где ответственности размазаны.

3) Фиксируйте регресс по изменению карты

Если карта строится автоматически, вы можете сравнивать версии:

  • увеличилось число SCC,
  • появилась новая нежелательная связь “domain → infrastructure”,
  • выросли “входящие ребра” конкретного модуля.

Это превращает архитектуру в наблюдаемую сущность.


Подводные камни: что ломает интерпретацию карты

  1. Смешение уровней в одном узле.
    Если узлом является “папка”, где лежат разные роли, то зависимости будут выглядеть “непонятно”. Разделяйте узлы по смыслу (контракты/реализация/адаптация).

  2. Динамические импорты и рефлексия.
    Статический анализ может пропустить runtime-зависимости. Карта по импортам — не гарантия отсутствия циклов исполнения.

  3. Общие типы и “shared”.
    Общий модуль может выглядеть как зло, хотя он нужен для контрактов. Проблема начинается,

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

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

Перейти к курсу

Другие статьи

Простой и надежный вход в CI/CD: как настроить кэш зависимостей, чтобы сборки не “умирали”
простой

Простой и надежный вход в CI/CD: как настроить кэш зависимостей, чтобы сборки не “умирали”

Разберем практические паттерны ускорения пайплайнов: кэширование pip/poetry/node_modules, ключи кэша по lockfile, правила “всегда воспроизводимо” и типичные ошибки, когда кэш портит сборку. Доведем до минимально поддерживаемой конфигурации.

30 июля 2026 г.
540
Карта профессии: как выбрать роль в IT и не потерять фокус на старте
карта

Карта профессии: как выбрать роль в IT и не потерять фокус на старте

Разберём, как соотнести интересы и сильные стороны с ролями (backend, frontend, QA, data, devops) и собрать реалистичный план обучения на 8–12 недель. В статье — чек-лист критериев выбора и критерии прогресса, чтобы не распыляться.

13 сентября 2026 г.
500
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Что именно считать зависимостью: уровни, направление, тип связиИмпортные зависимости (compile-time / build-time)Зависимости на уровне архитектуры (layering)Зависимости на уровне исполнения (runtime)Зависимости по даннымКарта зависимостей как инженерный инструмент, а не картинкаКак визуализировать зависимости модулей: практические подходыПодход 1: импортные графы (статический анализ)Подход 2: граф архитектурных слоёв (layer graph)Подход 3: runtime-связи и вызовы (observability)Поиск скрытых циклов: как отличить “плохой цикл” от “нормального сообщества”Практика: поиск SCC в импортном графеПочему циклы часто “скрыты”Утечки ответственности: когда граф зависимостей “неявно ломает архитектуру”Как это увидеть на картеТипичная форма утечки: “утилитарный модуль”Рефакторинг границ без переписывания всего: стратегии разрыва зависимостейСтратегия 1: разрывайте циклы через контракты (порт ↔ адаптер)Стратегия 2: “поднять” типы и исключения в общий доменный контрактСтратегия 3: ограничение зависимостей в коде (lint/CI guardrails)Стратегия 4: “вынести” инфраструктурные штуки в края и заменить прямые вызовы на события/очереди (аккуратно)Стратегия 5: итеративное снижение “межслойных” реберМини-кейс: от “лапши импортов” к управляемым границамКак сделать карту полезной: фильтры, матрицы и “порог критичности”1) Смотрите на матрицу зависимостей, а не только на граф2) Используйте топ-списки3) Фиксируйте регресс по изменению картыПодводные камни: что ломает интерпретацию карты