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

Что такое циклические зависимости модулей в Node.js и как с ними работать?

2.3 Middle🔥 111 комментариев
#Node.js и JavaScript#Архитектура и паттерны

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

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

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

Циклические зависимости (circular dependencies) — это когда модули ссылаются друг на друга прямо или косвенно, создавая цикл. Это может привести к проблемам: undefined переменные, незаполненные экспорты, и ошибки выполнения.

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

// file-a.js
const b = require('./file-b');

function funcA() {
  b.funcB();
}

module.exports = {funcA};

// file-b.js
const a = require('./file-a');

function funcB() {
  a.funcA();
}

module.exports = {funcB};

// index.js
const a = require('./file-a');
a.funcA(); // Проблема: b.funcB undefined!

Как Node.js обрабатывает циклические зависимости

Node.js имеет встроенный механизм для предотвращения бесконечных циклов:

// file-a.js
console.log('Loading A');
const b = require('./file-b');
console.log('b:', b);

module.exports = {name: 'A'};
console.log('A loaded');

// file-b.js
console.log('Loading B');
const a = require('./file-a');
console.log('a:', a);

module.exports = {name: 'B'};
console.log('B loaded');

// index.js
const a = require('./file-a');

// Вывод:
// Loading A
// Loading B
// a: {} (пустой объект — A ещё не загрузился полностью)
// B loaded
// b: {name: 'B'}
// A loaded

Node.js возвращает неполный экспорт для предотвращения бесконечного цикла.

Типы циклических зависимостей

1. Прямая циклическая зависимость

// user.js
const role = require('./role');

class User {
  constructor(name, roleId) {
    this.name = name;
    this.roleId = roleId;
  }
  
  getRole() {
    return role.getRoleById(this.roleId);
  }
}

module.exports = User;

// role.js
const User = require('./user'); // ЦИКЛИЧЕСКАЯ ЗАВИСИМОСТЬ

class Role {
  getRoleById(id) {
    return {id, name: 'Admin'};
  }
}

module.exports = Role;

2. Косвенная циклическая зависимость

// a.js → b.js → c.js → a.js
const b = require('./b'); // a.js
// b.js
const c = require('./c');
// c.js
const a = require('./a'); // Цикл!

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

1. Undefined переменные

// database.js
const logger = require('./logger');

const db = {
  query: (sql) => {
    logger.log(`Executing: ${sql}`);
  }
};

module.exports = db;

// logger.js
const db = require('./database');

const logger = {
  log: (msg) => {
    // db undefined здесь!
    console.log(`[LOG] ${msg}`);
  }
};

module.exports = logger;

2. Race condition с async

// servicea.js
const serviceB = require('./serviceb');

async function init() {
  // serviceB может быть не полностью инициализирован
  await serviceB.getData();
}

module.exports = {init};

Решения для циклических зависимостей

1. Рефакторинг — создать отдельный модуль

// common/types.js (нейтральный модуль)
module.exports = {
  UserType: 'user',
  RoleType: 'role'
};

// user.js
const {UserType} = require('./common/types');
const role = require('./role');

class User {
  constructor(name) {
    this.name = name;
    this.type = UserType;
  }
}

module.exports = User;

// role.js
const {RoleType} = require('./common/types');

class Role {
  constructor(name) {
    this.name = name;
    this.type = RoleType;
  }
}

module.exports = Role;
// Нет циклических зависимостей!

2. Ленивая загрузка (Lazy Loading)

// user.js
class User {
  constructor(name) {
    this.name = name;
  }
  
  getRole() {
    // Загружаем только когда нужно
    const role = require('./role');
    return role.getRoleById(this.roleId);
  }
}

module.exports = User;

// role.js
class Role {
  constructor(name) {
    this.name = name;
  }
  
  getUser() {
    // Загружаем только когда нужно
    const User = require('./user');
    return new User('Unknown');
  }
}

module.exports = Role;

3. Dependency Injection (инъекция зависимостей)

// user.js
class User {
  constructor(name, roleService) {
    this.name = name;
    this.roleService = roleService; // Передаем как параметр
  }
  
  getRole() {
    return this.roleService.getRoleById(this.roleId);
  }
}

module.exports = User;

// role.js
class Role {
  getRoleById(id) {
    return {id, name: 'Admin'};
  }
}

module.exports = Role;

// app.js (главный модуль)
const User = require('./user');
const roleService = require('./role');

const user = new User('Иван', roleService);
console.log(user.getRole());

4. Отделение интерфейсов

// interfaces/user-interface.js
class IUser {
  getRole() {}
}

module.exports = IUser;

// user.js
const IUser = require('./interfaces/user-interface');

class User extends IUser {
  getRole() {
    const role = require('./role');
    return role.getRoleById(this.roleId);
  }
}

module.exports = User;

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

# Использование инструментов
npm install -D madge
madge --extensions js,ts --image out.svg .

# Или
npm install -D depcheck
depcheck --extra-extensions ts,jsx

Best Practices

  1. Используй архитектурные слои

    domain/ (бизнес-логика, БЕЗ зависимостей)
    application/ (use cases, сервисы)
    infrastructure/ (БД, API, утилиты)
    presentation/ (HTTP handlers, views)
    
  2. Избегай циклов с помощью Event Emitter

    // events.js
    const EventEmitter = require('events');
    module.exports = new EventEmitter();
    
    // user.js
    const events = require('./events');
    events.emit('user:created', user);
    
    // role.js
    const events = require('./events');
    events.on('user:created', (user) => {});
    
  3. Думай о зависимостях с самого начала

Вывод

Циклические зависимости можно избежать:

  • Правильной архитектурой с разделением слоев
  • Ленивой загрузкой модулей
  • Инъекцией зависимостей
  • Использованием Event Emitter для коммуникации
  • Регулярной проверкой инструментами как madge
Что такое циклические зависимости модулей в Node.js и как с ними работать? | PrepBro