← Назад к вопросам

Как решаешь циклические зависимости?

2.0 Middle🔥 141 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как решаются циклические зависимости

Циклические зависимости — это проблема, когда модули A и B зависят друг от друга (прямо или через цепь других модулей). Это может привести к undefined значениям, ошибкам инициализации и сложностям при тестировании. Я расскажу о проверенных методах решения.

1. Проблема: Как появляются циклические зависимости

// userService.js
const { getPost } = require('./postService');

function createUser(data) {
  const posts = getPost(data.id);
  return { ...data, posts };
}

module.exports = { createUser };

// postService.js
const { createUser } = require('./userService');  // циклическая ссылка!

function getPost(userId) {
  const user = createUser({ id: userId });  // проблема
  return { userId, author: user };
}

module.exports = { getPost };

При запуске:

Node.js пытается загрузить userService
  → userService требует postService
  → postService требует userService (уже загружается!)
  → postService получает неполный userService
  → ошибка: createUser не определена

2. Решение 1: Перестроить архитектуру (лучший вариант)

Исключить циклический импорт через separation of concerns. Создаём промежуточный слой:

// domain/user.js
class User {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}

module.exports = { User };

// domain/post.js
class Post {
  constructor(id, title, userId) {
    this.id = id;
    this.title = title;
    this.userId = userId;
  }
}

module.exports = { Post };

// services/userService.js
const { User } = require('../domain/user');
const postRepository = require('../repositories/postRepository');

function createUser(data) {
  const user = new User(data.id, data.name);
  // не вызываем напрямую postService
  return user;
}

function getUserWithPosts(userId) {
  const user = createUser({ id: userId, name: 'John' });
  const posts = postRepository.getByUserId(userId);  // отделили логику
  return { user, posts };
}

module.exports = { createUser, getUserWithPosts };

// services/postService.js
const { Post } = require('../domain/post');
const userRepository = require('../repositories/userRepository');

function getPost(id) {
  return new Post(id, 'Some title', 1);
}

module.exports = { getPost };

3. Решение 2: Отложенная инициализация (Lazy Loading)

Загружаем модуль только когда он действительно нужен, внутри функции:

// userService.js
function createUserWithPosts(userId) {
  const user = { id: userId, name: 'John' };
  
  // Загружаем только когда нужно
  const { getPost } = require('./postService');
  const posts = getPost(userId);
  
  return { user, posts };
}

module.exports = { createUserWithPosts };

// postService.js
function getPost(userId) {
  // Загружаем только когда нужно
  const { createUserWithPosts } = require('./userService');
  
  return {
    id: 1,
    title: 'Post 1',
    userId: userId
  };
}

module.exports = { getPost };

Так модули загружаются не при импорте, а при вызове функции, когда оба уже инициализированы.

4. Решение 3: Инъекция зависимостей (DI Container)

Используем DI контейнер (например, inversify или собственный):

// container.js
const Container = require('dependency-injection');
const container = new Container();

// Регистрируем сервисы
container.register('userService', require('./services/userService'));
container.register('postService', require('./services/postService'));

module.exports = container;

// services/userService.js
class UserService {
  constructor(postService) {  // инъекция как параметр
    this.postService = postService;
  }
  
  createUserWithPosts(userId) {
    const user = { id: userId, name: 'John' };
    const posts = this.postService.getByUserId(userId);
    return { user, posts };
  }
}

module.exports = UserService;

// services/postService.js
class PostService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }
  
  getByUserId(userId) {
    return [{ id: 1, title: 'Post 1', userId }];
  }
}

module.exports = PostService;

// Инициализация в главном файле
const container = require('./container');
const userService = container.get('userService');
const postService = container.get('postService');

const user = userService.createUserWithPosts(1);

5. Решение 4: Абстрактные интерфейсы

Обе стороны зависят не друг от друга, а от интерфейса:

// interfaces/IPostProvider.js
class IPostProvider {
  getByUserId(userId) {
    throw new Error('Not implemented');
  }
}

module.exports = { IPostProvider };

// services/userService.js
const { IPostProvider } = require('../interfaces/IPostProvider');

class UserService {
  constructor(postProvider) {
    if (!(postProvider instanceof IPostProvider)) {
      throw new Error('postProvider must implement IPostProvider');
    }
    this.postProvider = postProvider;
  }
  
  getUserWithPosts(userId) {
    const user = { id: userId, name: 'John' };
    const posts = this.postProvider.getByUserId(userId);
    return { user, posts };
  }
}

module.exports = { UserService };

// services/postService.js
const { IPostProvider } = require('../interfaces/IPostProvider');

class PostService extends IPostProvider {
  getByUserId(userId) {
    return [{ id: 1, title: 'Post 1', userId }];
  }
}

module.exports = { PostService };

// app.js
const { UserService } = require('./services/userService');
const { PostService } = require('./services/postService');

const postService = new PostService();
const userService = new UserService(postService);

const user = userService.getUserWithPosts(1);

6. Решение 5: Разделение на слои (Layered Architecture)

Используем DDD/Clean Architecture с чёткими слоями:

domain/              # бизнес-логика, не зависит ни от кого
  ├── User.js
  └── Post.js

application/         # use cases, зависит только от domain
  ├── CreateUserUseCase.js
  └── GetUserPostsUseCase.js

infrastructure/      # БД, API, сторонние сервисы
  ├── UserRepository.js
  └── PostRepository.js

presentation/        # API, контроллеры
  ├── UserController.js
  └── PostController.js
// domain/User.js
class User {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}
module.exports = User;

// application/GetUserPostsUseCase.js
class GetUserPostsUseCase {
  constructor(userRepository, postRepository) {
    this.userRepository = userRepository;
    this.postRepository = postRepository;
  }
  
  execute(userId) {
    const user = this.userRepository.findById(userId);
    const posts = this.postRepository.findByUserId(userId);
    return { user, posts };
  }
}
module.exports = GetUserPostsUseCase;

// presentation/UserController.js
class UserController {
  constructor(getUserPostsUseCase) {
    this.useCase = getUserPostsUseCase;
  }
  
  async getUserWithPosts(req, res) {
    const { userId } = req.params;
    const result = this.useCase.execute(userId);
    res.json(result);
  }
}
module.exports = UserController;

Зависимости идут только в одну сторону: presentation → application → domain

7. Обнаружение циклических зависимостей

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

# Использование eslint
npm install --save-dev eslint-plugin-import

# В .eslintrc.json:
{
  "plugins": ["import"],
  "rules": {
    "import/no-cycle": ["error", { "maxDepth": 1 }]
  }
}

# Запуск
npm run lint
# Использование madge (анализ зависимостей)
npm install --save-dev madge
npx madge --circular ./src

Пример вывода madge

Found 1 circular dependencies:
- userService.js → postService.js → userService.js

8. Практический пример: Node.js приложение

// BAD: циклические зависимости
// models/User.js
const Post = require('./Post');

class User {
  getPosts() {
    return Post.findByUserId(this.id);
  }
}

// models/Post.js
const User = require('./User');

class Post {
  getAuthor() {
    return User.findById(this.userId);
  }
}

// GOOD: разделённые модели и сервисы
// models/User.js
class User {
  constructor(id, name) {
    this.id = id;
    this.name = name;
  }
}
module.exports = User;

// models/Post.js
class Post {
  constructor(id, title, userId) {
    this.id = id;
    this.title = title;
    this.userId = userId;
  }
}
module.exports = Post;

// services/UserService.js
const User = require('../models/User');
const postRepository = require('../repositories/postRepository');

function getUserWithPosts(userId) {
  const user = new User(userId, 'John');
  const posts = postRepository.findByUserId(userId);
  return { user, posts };
}

module.exports = { getUserWithPosts };

Сравнение решений

РешениеСложностьГибкостьТестируемостьКогда использовать
Перестройка архитектурыСредняяВысокаяОтличнаяНа этапе проектирования
Lazy LoadingНизкаяСредняяСредняяВременное решение
Dependency InjectionСредняяВысокаяОтличнаяБолее сложные приложения
ИнтерфейсыСредняяВысокаяОтличнаяКогда есть TypeScript
Layered ArchitectureВысокаяВысокаяОтличнаяКрупные проекты

Лучшие практики

  1. Избегайте циклических зависимостей с самого начала — проектируйте архитектуру правильно
  2. Используйте инструменты проверки — eslint, madge при каждом commit
  3. Следуйте DDD/Clean Architecture — четкая иерархия слоёв
  4. Тестируйте в изоляции — используйте dependency injection
  5. Документируйте зависимости — пишите схемы зависимостей

Заключение

Циклические зависимости — это признак проблем в архитектуре. Лучший способ их решить — спроектировать правильную архитектуру с самого начала, разделив ответственность на отдельные модули с чёткой иерархией зависимостей. Если циклическая зависимость уже появилась, используйте инъекцию зависимостей или layered architecture для их рефакторинга.

Как решаешь циклические зависимости? | PrepBro