Skip to main content

Що таке «циклічна залежність»?

1. Визначення

Circular dependency (циклічна залежність) - це ситуація, коли два (чи більше) модулі залежать один від одного напряму чи опосередковано, утворюючи замкнене коло залежностей.

Простіше кажучи:

  • Модуль A імпортує модуль B
  • Модуль B імпортує модуль A

Або ще гірше - через ланцюжок:

javascript
ABCA

2. Простий приклад у Node.js

a.js

javascript
const b = require('./b'); console.log('Модуль A завантажено'); module.exports = 'A';

b.js

javascript
const a = require('./a'); console.log('Модуль B завантажено'); module.exports = 'B';

main.js

javascript
const a = require('./a'); const b = require('./b');

Результат під час запуску:

javascript
Модуль B завантажено Модуль A завантажено

Зверни увагу: порядок не той, якого ти очікував.

3. Що реально відбувається під капотом

Коли Node.js зустрічає require(), він:

  1. Починає завантажувати модуль a;
  2. Бачить, що a вимагає b;
  3. Починає завантажувати b;
  4. Але b теж вимагає a, а a ще не встиг повністю завантажитися;
  5. Node.js повертає частково завантажений (незавершений) об'єкт a у b.

Тому за циклічної залежності один із модулів отримає неповний експорт (undefined чи частковий об'єкт).

4. Демонстрація частково завантаженого модуля

a.js

javascript
exports.loaded = false; const b = require('./b'); console.log('b.loaded =', b.loaded); exports.loaded = true; console.log('a завантажено');

b.js

javascript
exports.loaded = false; const a = require('./a'); console.log('a.loaded =', a.loaded); exports.loaded = true; console.log('b завантажено');

main.js

javascript
require('./a');

Вивід:

javascript
a.loaded = false b завантажено b.loaded = true a завантажено

Тобто коли b імпортував a, він отримав ще не завершений об'єкт - a.loaded = false.

5. Чому це відбувається

Node.js кешує кожен модуль при require(). Коли модуль завантажується, він додається в require.cache, навіть якщо не встиг виконатися повністю.

Тому за циклічної залежності один із модулів "побачить" недозавантажену версію іншого.

6. У реальному житті

Такі цикли часто виникають не напряму, а через ланцюжок імпортів:

javascript
A → utils.js → logger.jsA

чи в архітектурі:

javascript
controller → service → model → controller

Зазвичай це ознака поганого проєктування (надто сильна зв'язаність між модулями).

7. Ознаки, що в тебе «circular dependency»

  • При імпорті отримуєш undefined;

  • Код виконується не в очікуваному порядку;

  • Під час запуску Node.js виводить попередження:

    javascript
    Warning: Accessing non-existent property 'X' of module exports inside circular dependency
  • Поведінка нестабільна чи відрізняється між середовищами.

8. Як уникнути циклічних залежностей

Спосіб 1. Розірвати цикл через третій модуль

Створи "спільний" модуль, який зберігає те, що потрібно обом.

Погано:

javascript
ABA

Добре:

javascript
ACommon BCommon

common.js

javascript
module.exports = { sharedValue: 42 };

a.js

javascript
const common = require('./common');

b.js

javascript
const common = require('./common');

Спосіб 2. Використовувати відкладене завантаження (require() усередині функції)

Якщо модуль потрібен лише під час виклику функції, можна викликати require() пізніше:

javascript
function useB() { const b = require('./b'); // завантажиться лише під час виклику b.doSomething(); }

Це розриває "момент циклу" під час старту застосунку.

Спосіб 3. Змінити архітектуру

  • Розділи відповідальність між модулями;
  • Позбався "взаємних" залежностей - нехай один модуль керує іншим;
  • Використовуй ін'єкцію залежностей (dependency injection).

Спосіб 4. Використовувати ESM (ES Modules)

В ES Modules (з import/export) циклічні залежності розв'язуються коректно, тому що імпорт створює живі посилання (live bindings), а не статичні значення, як у CommonJS.

Приклад (працює стабільно):

javascript
// a.mjs import { b } from './b.mjs'; export const a = 'A'; console.log('a бачить b =', b); // b.mjs import { a } from './a.mjs'; export const b = 'B'; console.log('b бачить a =', a);

9. Інтуїтивно: чому це проблема

Уяви два файли, які запускають один одного одночасно:

javascript
A запускає BB запускає AA запускає B...

Node.js, щоб не піти в нескінченність, "зупиняє" цикл і повертає те, що встиг завантажити на момент циклу. Тому ти бачиш "половину об'єкта".

10. Інструменти для пошуку циклів

Якщо проєкт великий, можна використати утиліти:

javascript
npm install madge -g madge src/

Вона покаже, які файли утворюють циклічні залежності.

Коротке резюме

ТермінЗначення
Circular dependencyКоли модулі імпортують один одного (напряму чи через ланцюжок)
ПричинаМодуль вимагає інший, який ще не встиг виконатися
НаслідокПовернення undefined чи частково завантаженого модуля
РішенняРозділення логіки, відкладені імпорти, рефакторинг, ESM

Одним реченням

Циклічна залежність - це ситуація, коли модулі Node.js імпортують один одного, утворюючи замкнене коло, через що один із них завантажується не повністю, що призводить до помилок і undefined при require().

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.