Циклические зависимости - circular-dependencies в коде и архитектуре

05 января 2026
Автор

Олег Марков

Введение

Циклические зависимости (circular dependencies) появляются, когда два или больше модулей, пакетов, классов или сервисов зависят друг от друга по кругу. На диаграмме это выглядит как стрелка, которая в итоге возвращается к исходной точке.

Смотрите, формально это выглядит так:

  • Модуль A зависит от модуля B
  • Модуль B зависит от модуля C
  • Модуль C зависит от модуля A

В итоге ни один из них «логически» не может существовать без остальных.

На практике такие зависимости:

  • усложняют сборку и запуск приложения
  • осложняют тестирование и повторное использование кода
  • повышают связанность (coupling) и усложняют изменения
  • иногда приводят к ошибкам времени компиляции или рантайма (например, при статическом импорте или ленивой инициализации)

В этой статье вы увидите, что циклические зависимости могут возникать:

  • между файлами и модулями (import / include кругом)
  • между классами (взаимные ссылки, дружба, композиция)
  • между слоями и сервисами в архитектуре
  • в системах сборки и конфигурации (например, зависимости в DI-контейнере)

Давайте разберем, откуда они берутся, чем именно опасны и какие практические способы есть, чтобы их убрать без хаотичного рефакторинга.

Что такое циклическая зависимость более формально

Граф зависимостей и цикл

Зависимости удобно представлять в виде ориентированного графа:

  • вершины — модули, классы, сервисы
  • ребра — «A зависит от B» (стрелка из A в B)

Циклическая зависимость — это наличие цикла в таком графе. Если вы от модуля A, идя по стрелкам depend-on, сможете вернуться в A, значит есть цикл.

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

  • user-service импортирует email-service
  • email-service импортирует user-service

Здесь цикл длиной 2. Аналогично можно получить циклы длиной 3, 4 и больше.

Виды циклов

Полезно различать несколько видов:

  1. Прямой цикл
    A зависит от B, B зависит от A.

  2. Непрямой (опосредованный) цикл
    A зависит от B, B от C, C от A.

  3. Цикл времени компиляции
    Когда статический импорт/инклюд приводит к невозможности собрать проект или к странному порядку инициализации.

  4. Цикл времени исполнения
    Когда объекты или сервисы создаются через DI-контейнер, и контейнер не может построить граф зависимостей или попадает в рекурсию.

Теперь давайте посмотрим, как это проявляется в конкретном коде.

Примеры циклических зависимостей в разных языках

Циклические зависимости между модулями в JavaScript / TypeScript

Смотрите, простой пример, который многие встречают при работе с модулями.

a.ts:

// a.ts
import { bFunc } from "./b";

export function aFunc() {
  // Здесь вызываем функцию из модуля B
  console.log("aFunc вызывается");
  bFunc();
}

b.ts:

// b.ts
import { aFunc } from "./a";

export function bFunc() {
  // Здесь вызываем функцию из модуля A
  console.log("bFunc вызывается");
  aFunc();
}

Что здесь происходит:

  • Модуль A импортирует модуль B
  • Модуль B импортирует модуль A
  • В момент загрузки модулей интерпретатору нужно решить, в каком порядке инициализировать экспортируемые сущности
  • В зависимости от системы модулей (CommonJS, ESM) можно получить:
    • ошибку времени выполнения
    • частично инициализированный модуль (undefined в экспортах)
    • бесконечную рекурсию при вызове aFunc()bFunc()

Обратите внимание, как взаимный вызов функций усиливает проблему: даже если импорты технически допустимы, вы легко уйдете в рекурсивный вызов без условия выхода.

Циклические зависимости между пакетами в Go

В Go компилятор жестко запрещает циклические импорты.

a/a.go:

// package a
package a

import "myproject/b"

// FuncA зависит от пакета b
func FuncA() {
    // Здесь мы обращаемся к функции из пакета b
    b.FuncB()
}

b/b.go:

// package b
package b

import "myproject/a"

// FuncB зависит от пакета a
func FuncB() {
    // Здесь мы обращаемся к функции из пакета a
    a.FuncA()
}

При сборке вы увидите ошибку вида:

import cycle not allowed
package myproject/a
    imports myproject/b
    imports myproject/a

Go прямо говорит, что такой цикл недопустим. Это удобный пример, чтобы увидеть, что проблема обнаруживается на уровне компиляции, а не в рантайме.

Циклические ссылки между классами в объектно-ориентированных системах

Представим две сущности: Order и User.

В псевдокоде:

// User.java
public class User {
    private List<Order> orders; // Пользователь хранит список заказов

    // Здесь методы работы с заказами пользователей
}

// Order.java
public class Order {
    private User user; // Заказ хранит ссылку на пользователя

    // Здесь методы работы с заказом
}

Здесь формально есть двунаправленная связь (bidirectional association). Это еще не обязательно «плохой» цикл, но:

  • сериализация этих объектов (например, в JSON) может вести к рекурсивному обходу и StackOverflow
  • ORM (Hibernate, JPA, Entity Framework и т.п.) требуют аккуратной настройки, чтобы не загружать данные по кругу
  • при тестировании вам приходится создавать User, затем Order, затем обратно связывать, что усложняет изоляцию

Часто такие связи перерастают в более крупные циклы на уровне модулей.

Циклы в архитектуре слоев

Еще один частый сценарий: слои архитектуры «протекают» и начинают зависеть друг от друга в обе стороны.

Например:

  • ControllerServiceRepository — так обычно и должно быть
  • Но кто-то добавляет использование Controller внутри Service (например, для доступа к HTTP контексту или валидации)
  • Или Repository начинает вызывать Service, чтобы дернуть какой-то бизнес-метод

В итоге:

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

Здесь проблема уже не на уровне синтаксиса, а на уровне архитектурных правил.

Почему циклические зависимости опасны

Усложняют понимание и сопровождение кода

Когда модуль A зависит от B и vice versa, вы уже не можете понять один из них в изоляции. Вам приходится:

  • держать в голове оба (или весь цикл)
  • бояться изменений, так как неясно, как они отразятся на другом конце цикла
  • сталкиваться с «слепыми» зависимостями (например, A тянет половину дерева зависимостей B, о которых вы сразу не знаете)

Это приводит к высокой связанности (tight coupling) и снижает модульность.

Могут ломать сборку и запуск

Некоторые языки и окружения:

  • запрещают циклические импорты полностью (Go, Rust, частично Java с модульной системой)
  • допускают их, но вводят особые правила инициализации (ES Modules, Python), что может привести к:

    • неинициализированным переменным экспорта
    • неожиданному порядку выполнения кода
    • странным багам при первой загрузке модулей

На практике разработчики часто сталкиваются с ситуацией, когда «простая» перестановка кода в модуле неожиданно ломает работу из-за скрытого цикла.

Усложняют тестирование и внедрение зависимостей

Цикл может серьезно усложнить unit-тестирование:

  • вы не можете поднять один сервис или класс без второго
  • моки приходится прокидывать глубже
  • тесты становятся интеграционными, а не модульными

Особенно это заметно в проектах с DI-контейнерами: контейнер либо не может построить граф зависимостей, либо вам приходится использовать хаки — ленивые зависимости, сервис-локаторы и т.п.

Уводят архитектуру от чистой структуры

Другая важная проблема — циклы часто означают, что уровни абстракции перепутаны:

  • доменная модель начинает знать о инфраструктуре
  • слой данных знает о веб-слое
  • клиенты зависят от серверов и наоборот

Цикл — почти всегда сигнал: где-то нарушен принцип единственной ответственности и/или принцип направленности зависимостей (Dependency Inversion).

Как находить циклические зависимости

Инструменты анализа зависимости для разных языков

Вместо ручного поиска удобно использовать статический анализ или плагины:

  • Java / JVM
    • Maven/Gradle плагины (maven-dependency-plugin, gradle-dependency-analysis)
    • архитектурные тесты на ArchUnit
  • JavaScript / TypeScript
    • ESLint правило import/no-cycle
    • madge (CLI-утилита, которая строит граф зависимостей)
  • Python
    • pipdeptree (для внешних пакетов)
    • инструменты статического анализа (pylint, pydeps)
  • Go
    • встроенный компилятор сразу ругается на import cycle not allowed
    • утилиты вроде go list, go mod graph, сторонние вьюеры графов
  • .NET
    • NDepend, ReSharper, интеграции в Rider/Visual Studio

Смотрите, как использовать madge для JS/TS, чтобы вы увидели цикл на наглядной схеме.

# Установка madge
npm install -g madge

# Анализ директорий src
madge src --circular

Комментарий:

  • здесь --circular выводит только циклы
  • вы увидите список цепочек модулей, замыкающихся в круг

Вы также можете сгенерировать изображение:

madge src --image graph.png

Комментарий:

  • граф «подсветит», где именно начинаются и заканчиваются циклы
  • это удобно, когда проект уже крупный и связи неочевидны

Архитектурные тесты как защита от новых циклов

Хорошая практика — не только исправить текущие циклы, но и зафиксировать архитектурные правила в виде тестов.

Например, на Java с ArchUnit:

// Здесь мы определяем архитектурный тест, который запрещает циклы между пакетами
@ArchTest
static final ArchRule no_cycles_between_packages =
    slices().matching("com.myapp.(*)..").should().beFreeOfCycles();

Комментарий:

  • slices().matching("com.myapp.(*)..") разбивает проект на «срезы» по пакетам
  • beFreeOfCycles() проверяет, что между ними нет циклов
  • если кто-то добавит новый цикл, тесты упадут

Это особенно полезно в долгоживущих проектах с большой командой.

Типичные причины появления циклов

Смешивание уровней ответственности

Частая ситуация:

  • модуль, который должен быть «низкоуровневым» (например, доступ к БД), начинает обращаться к «верхнеуровневому» модулю (например, веб-контроллеры)
  • бизнес-логика начинает ходить в слой UI для валидации или отображения сообщений

В итоге уровни, которые должны быть независимы, оказываются связанными.

Удобство «здесь и сейчас» вместо выделения общего слоя

Разработчик видит:

  • «Мне нужна функция из того модуля, просто импортну его сюда»
  • На первых этапах это кажется безобидным
  • Через пару таких шагов появляется зависимость назад, и формируется цикл

Обычно это означает, что часть логики нужно вынести в отдельный модуль (utility, shared, core), но это откладывается «на потом».

Непродуманная двунаправленная навигация в доменной модели

Мы уже видели пример UserOrder. Похожие связи часто встречаются в доменных моделях:

  • CategoryProduct
  • DepartmentEmployee
  • ParentChild

Двунаправленные связи удобны при навигации, но:

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

Неправильная настройка DI-контейнера

В системах с DI:

  • сервис A внедряет сервис B
  • сервис B внедряет сервис C
  • сервис C внедряет A

Контейнер при построении графа зависимостей может:

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

Давайте теперь посмотрим, как такие ситуации исправлять.

Стратегии устранения циклических зависимостей

1. Разделение модулей и выделение общего слоя

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

Схема:

  • Было: A ↔ B
  • Становится: A → Common ← B

Теперь зависимости направлены в одну сторону: к общему модулю.

Пример на TypeScript

Допустим, у нас есть модули order.ts и user.ts, которые зависят друг от друга.

user.ts:

// user.ts
import { Order } from "./order";

export class User {
  // Здесь пользователь хранит список заказов
  orders: Order[] = [];
}

order.ts:

// order.ts
import { User } from "./user";

export class Order {
  // Здесь заказ хранит ссылку на пользователя
  constructor(public user: User) {}
}

Цикл: user.tsorder.ts.

Решение — вынести общие интерфейсы в models.ts:

models.ts:

// models.ts
// Здесь мы выделяем общие интерфейсы для сущностей

export interface IUser {
  // Интерфейс пользователя - минимальный набор полей
  id: string;
}

export interface IOrder {
  // Интерфейс заказа - минимальный набор полей
  id: string;
  userId: string; // Ссылка на пользователя через идентификатор
}

user.ts:

// user.ts
import { IOrder, IUser } from "./models";

export class User implements IUser {
  // Реализация пользователя на основе интерфейса
  constructor(
    public id: string,
    public orders: IOrder[] = [] // Используем интерфейс IOrder вместо конкретного класса
  ) {}
}

order.ts:

// order.ts
import { IOrder } from "./models";

export class Order implements IOrder {
  // Реализация заказа на основе интерфейса
  constructor(
    public id: string,
    public userId: string // Храним только идентификатор, а не объект пользователя
  ) {}
}

Теперь:

  • models.ts не зависит от user.ts и order.ts
  • user.ts и order.ts зависят только от models.ts
  • цикл исчезает

Ключевая идея: общий модуль содержит лишь абстракции (интерфейсы, DTO, базовые типы), а не конкретные реализации.

2. Применение принципа Dependency Inversion (DIP)

Принцип инверсии зависимостей звучит примерно так:

  • высокоуровневые модули не должны зависеть от низкоуровневых
  • и те, и другие должны зависеть от абстракций
  • абстракции не должны зависеть от деталей, детали должны зависеть от абстракций

Практически: вместо того чтобы модуль A напрямую зависел от конкретного класса/реализации модуля B, он зависит от интерфейса, который реализует B (или кто-то еще).

Пример с репозиторием и сервисом

Смотрите, стандартная ситуация:

// userService.ts
import { UserRepository } from "./userRepository";

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

  // Здесь бизнес-логика, использующая репозиторий
}
// userRepository.ts
import { UserService } from "./userService";

export class UserRepository {
  // Пример - репозиторий хочет использовать сервис для валидации
  constructor(private service: UserService) {}
}

Цикл: userService.tsuserRepository.ts.

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

userRepository.ts:

// userRepository.ts
// Здесь мы определяем абстракцию репозитория

export interface IUserRepository {
  // Абстракция операции поиска
  findById(id: string): Promise<User | null>;
}

// Конкретная реализация репозитория
export class UserRepository implements IUserRepository {
  // Здесь уже нет ссылки на UserService
  async findById(id: string): Promise<User | null> {
    // Логика доступа к базе данных
    return null;
  }
}

// Здесь определен тип User, чтобы пример был самодостаточным
export type User = {
  id: string;
};

userService.ts:

// userService.ts
import { IUserRepository } from "./userRepository";

export class UserService {
  // Сервис зависит только от абстракции репозитория
  constructor(private repo: IUserRepository) {}

  async getUser(id: string) {
    // Здесь бизнес-логика получения пользователя
    return this.repo.findById(id);
  }
}

Теперь:

  • UserService зависит от интерфейса IUserRepository, а не от конкретного класса, который в свою очередь может зависеть от чего угодно
  • если репозиторию по какой-то причине нужен сервис (что само по себе уже вопрос к архитектуре), его стоит выделить в отдельный компонент или использовать доменные сервисы без зависимости назад

3. Введение портов и адаптеров (Hexagonal / Clean Architecture)

В архитектурах типа Hexagonal или Clean Architecture:

  • доменный слой определяет «порты» — абстрактные интерфейсы для внешнего мира
  • инфраструктура поставляет адаптеры, реализующие эти порты

Зависимости направлены:

  • от домена к абстрактным портам
  • от адаптеров к домену и портам
  • но не наоборот

Это почти тот же DIP, только на уровне всей архитектуры.

Схематично:

  • Домен → Порты
  • Адаптеры → Домен, Порты
  • Веб, БД, очереди — все идут через порты

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

4. Использование событий и шины сообщений вместо прямых вызовов

Еще один мощный прием — заменить прямые вызовы «A вызывает B» на событийную модель:

  • A публикует событие «что-то произошло»
  • B подписывается на это событие и реагирует
  • A больше не знает о B, а B не знает об A напрямую (они зависят от контракта события)

Пример с сервисами оповещения

Предположим:

  • UserService после регистрации пользователя напрямую вызывает EmailService
  • EmailService по какой-то причине хочет запросить статус пользователя в UserService

Получаем цикл UserServiceEmailService.

Решение — ввести событие UserRegistered и EventBus.

// events.ts
// Здесь мы определяем интерфейс шины событий и тип события

export interface EventBus {
  // Публикация события
  publish(event: DomainEvent): void;
  // Подписка на событие
  subscribe(eventName: string, handler: (event: DomainEvent) => void): void;
}

export interface DomainEvent {
  // Базовые поля доменных событий
  name: string;
  payload: unknown;
}

export interface UserRegisteredPayload {
  // Поля события о регистрации пользователя
  userId: string;
  email: string;
}

userService.ts:

// userService.ts
import { EventBus, DomainEvent, UserRegisteredPayload } from "./events";

export class UserService {
  constructor(private eventBus: EventBus) {}

  async registerUser(email: string, password: string) {
    // Логика создания пользователя в системе

    const event: DomainEvent = {
      name: "UserRegistered",
      payload: {
        userId: "generated-id",
        email: email,
      } as UserRegisteredPayload,
    };
    // Публикуем событие вместо прямого вызова EmailService
    this.eventBus.publish(event);
  }
}

emailService.ts:

// emailService.ts
import { EventBus, DomainEvent, UserRegisteredPayload } from "./events";

export class EmailService {
  constructor(private eventBus: EventBus) {
    // Подписываемся на событие регистрации пользователя
    this.eventBus.subscribe("UserRegistered", this.handleUserRegistered);
  }

  private handleUserRegistered(event: DomainEvent) {
    const payload = event.payload as UserRegisteredPayload;
    // Отправляем письмо, используя данные из события
    console.log("Отправляем приветственное письмо пользователю", payload.email);
  }
}

Теперь:

  • UserService не знает о EmailService
  • EmailService не знает непосредственно о UserService
  • они оба зависят от абстракции EventBus и контракта события

Цикл разорван, а взаимодействие становится слабосвязанным.

5. Ленивая инициализация и фабрики (как временный костыль)

Иногда архитектурный рефакторинг трудоемок, а проблему нужно снять быстро. В таких случаях используют:

  • ленивые зависимости (Lazy<T>, Provider<T>, лямбда-фабрики)
  • сервис-локаторы
  • отложенное связывание зависимостей на уровне конфигурации

Пример на псевдо-коде с DI-контейнером:

// Здесь мы используем ленивую зависимость, чтобы разорвать жесткий цикл конструкторов

class ServiceA {
  // Вместо непосредственной зависимости от ServiceB - функция, которая вернет его по запросу
  constructor(private getB: () => ServiceB) {}

  doSomething() {
    // Получаем ServiceB только когда он реально нужен
    const b = this.getB();
    // Дальше используем b
  }
}

class ServiceB {
  constructor(private a: ServiceA) {}
}

Комментарий:

  • в конфигурации DI-контейнера вы можете зарегистрировать ServiceA и ServiceB, используя фабрики
  • такой подход снимает проблему «циклического конструктора», но по сути не решает архитектурный цикл
  • это разумно как временная мера, но лучше все равно рефакторить

Практический пошаговый подход к устранению цикла

Давайте разберемся на условном примере, как вы можете системно подойти к разрыву цикла без «ломания всего» сразу.

Шаг 1. Найти и зафиксировать цикл

Используйте:

  • статический анализ (madge, ArchUnit, IDE-плагины)
  • компилятор (в Go, Rust)
  • собственные скрипты обхода зависимостей

Зафиксируйте:

  • какие модули/пакеты/классы входят в цикл
  • какой тип цикла: прямой или опосредованный

Шаг 2. Определить целевую направленность зависимостей

Ответьте:

  • какие модули по смыслу должны быть «выше» (большая бизнес-ценность, более абстрактные)
  • какие ниже (техническая инфраструктура, утилиты)

Циклы почти всегда должны быть разорваны так, чтобы:

  • верхние уровни зависели от нижних либо от абстракций
  • низкие уровни не знали о верхних

Это позволит наметить, кто «неправильно» зависит от кого.

Шаг 3. Выделить абстракции или общий модуль

Варианты:

  • вынести общие модели/интерфейсы в отдельный пакет core или api
  • определить интерфейсы (порты), которые будут реализованы с другой стороны
  • применить событийную модель, если зависимость больше похожа на реакцию на событие, чем на «запрос-ответ»

Шаг 4. Изменить код и пересобрать

После изменения:

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

Шаг 5. Почистить временные решения

Если на время вы использовали:

  • сервис-локатор
  • ленивые зависимости, которые не нужны после рефакторинга
  • страшные «if (x == null) initX()» внутри кода

то постепенно уберите их за ненадобностью. Они не должны стать постоянным паттерном.

Типичные антипаттерны, ведущие к циклам

«God object» и «Manager для всего»

Когда в проекте появляется один «главный» класс/модуль:

  • AppManager, CoreService, MainController и так далее
  • в нем концентрируются вызовы ко всем компонентам
  • в какой-то момент другие компоненты начинают ссылаться на него «для удобства»

Зависимости превращаются в сеть, где центр знает всех, а все знают центр. Появляются циклы:

  • UserServiceCoreServiceUserService

Решение:

  • разделить «бога» на специализованные сервисы
  • использовать DI и события вместо глобального менеджера

Service Locator

Service Locator позволяет «вытянуть» любой сервис «из воздуха»:

// Псевдо-пример сервис-локатора
class ServiceLocator {
  private static services = new Map<string, unknown>();

  static register(name: string, instance: unknown) {
    // Регистрируем сервис
    this.services.set(name, instance);
  }

  static resolve<T>(name: string): T {
    // Получаем сервис по имени
    return this.services.get(name) as T;
  }
}

Комментарий:

  • он скрывает зависимости и делает архитектурные циклы менее очевидными
  • код «компилируется», но реальные зависимости размазываются по проекту

Лучше явно прокидывать зависимости через конструкторы или DI-контейнер, где possible.

Двунаправленные отношения по умолчанию

Когда автоматически для каждой связи в доменной модели создается обратная связь:

  • «раз есть заказ у пользователя, то у заказа обязательно должен быть полный объект пользователя»

Это приводит к:

  • сложным графам
  • сложным мапперам DTO
  • рискам бесконечной рекурсии при сериализации

Лучше:

  • по умолчанию делать однонаправленные связи
  • добавлять обратные ссылки только там, где это обосновано
  • чаще использовать идентификаторы вместо полных объектов

Заключение

Циклические зависимости — это не просто неприятная ошибка компиляции, а индикатор архитектурных проблем:

  • нарушенных уровней абстракции
  • чрезмерной связанности модулей
  • неясных или смешанных ответственностей

На уровне кода циклы часто проявляются через:

  • взаимные импорты модулей
  • двунаправленные ссылки между классами
  • циклы в графе DI-контейнера

На уровне архитектуры — через:

  • «протекание» слоев (UI → Service → Repository → UI)
  • зависимость доменной логики от инфраструктуры
  • «божественные» классы и глобальных менеджеров

Чтобы эффективно работать с circular-dependencies, полезно:

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

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

Частозадаваемые технические вопросы по теме и ответы

Вопрос 1. Как найти циклические зависимости только внутри одного конкретного модуля или директории

Используйте инструменты анализа, которые позволяют ограничить область. Например, в TypeScript с madge:

# Анализ только директории src/features/user
madge src/features/user --circular

Комментарий:

  • так вы не «тонете» в глобальном графе всего проекта
  • удобнее рефакторить область по частям

Вопрос 2. Как временно обойти циклическую зависимость в DI-контейнере Spring

Можно использовать аннотацию @Lazy:

@Service
public class ServiceA {
    private final ServiceB serviceB;

    public ServiceA(@Lazy ServiceB serviceB) {
        // Ленивое внедрение зависимости
        this.serviceB = serviceB;
    }
}

Комментарий:

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

Вопрос 3. Что делать, если ORM (например, Hibernate) создает циклы при сериализации в JSON

Используйте аннотации для управления сериализацией. В Java + Jackson:

public class User {
    @JsonManagedReference
    private List<Order> orders;
}

public class Order {
    @JsonBackReference
    private User user;
}

Комментарий:

  • @JsonManagedReference и @JsonBackReference разрывают цикл при сериализации
  • на уровне доменной модели связь остается двунаправленной

Вопрос 4. Как убедиться, что новый модуль не создает циклов в монорепозитории

Включите автоматический анализ в CI:

  • добавьте команду типа madge packages/**/src --circular или аналог для вашего стека
  • настройте, чтобы пайплайн падал при обнаружении циклов
  • добавьте правило в CONTRIBUTING, что новые модули не могут добавлять циклические зависимости

Комментарий:

  • это позволяет ловить проблему на этапе Pull Request

Вопрос 5. Можно ли использовать forward-declaration в C++ как «решение» циклических зависимостей

В C++ можно объявить класс заранее:

// Здесь мы объявляем класс B, не включая его заголовок
class B;

class A {
    B* b; // Указатель на B - достаточно forward declaration
};

Комментарий:

  • это технически убирает необходимость двухстороннего include
  • но если логика классов по-прежнему сильно переплетена, архитектурная проблема остается
  • старайтесь все равно пересматривать дизайн классов и модулей, а не только заголовки
Стрелочка влевоИнверсия зависимостей - понятное объяснение с примерами кодаПравило абсолютных импортов - absolute-importsСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий