2

I have a problem with circular dependencies when doing module.exports = <ClassName>.

Here is an example. ClassC requires ClassB, ClassB requires ClassA, ClassA requires ClassC. And in main.js we just create a new instance of ClassC and call the method, that will do a chain of calls:

/* classA.js */
'use strict';

const ClassC = require('./classC.js');
class ClassA {
    foo() {
        console.log('ClassA.foo');
        var classC = new ClassC();
        classC.bar();
    }
}

module.exports = ClassA;

/* classB.js */
'use strict';

const ClassA = require('./classA.js');
class ClassB {
    foo() {
        console.log('ClassB.foo');
        var classA = new ClassA();
        classA.foo();
    }
}

module.exports = ClassB;

/* classC.js */
'use strict';

const ClassB = require('./classB.js');
class ClassC {
    foo() {
        console.log('ClassC.foo');
        var classB = new ClassB();
        classB.foo();
    }

    bar() {
        console.log('ClassC.bar');
    }
}

module.exports = ClassC;

/* main.js */
'use strict';

const ClassC = require('./classC.js');

var classC = new ClassC();
classC.foo();

So when I do call node main.js i get an obvious error that ClassC is not resolved.

D:\Projects\test-circular-reference-es6\classA.js:8
        var classC = new ClassC();
                     ^
TypeError: ClassC is not a function
    at ClassA.foo (D:\Projects\test-circular-reference-es6\classA.js:8:22)
    at ClassB.foo (D:\Projects\test-circular-reference-es6\classB.js:9:16)
    at ClassC.foo (D:\Projects\test-circular-reference-es6\classC.js:9:16)
    at Object.<anonymous> (D:\Projects\test-circular-reference-es6\main.js:7:8)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)

This happens because when ClassC was required in ClassA it was still a process of ClassC loading, so an empty object was returned and later at the end of classC.js the module.exports was overwritten with ClassC while that reference to empty object in classA.js remained as it was.

So the question is how to deal with this, what is the best practice to have both circular references and exporting classes in NodeJS 4.x (ES6)?

From my side I see the following possible ways:

  1. Do not overwrite module.exports and do something like module.exports.class = ClassC; and then instantiate like new ClassC.class(); But this also introduces more difficulties for inheritance and clumsy code in general.
  2. Use TypeScript instead, because it will handle this stuff.

Any suggestions are welcome.

Aides
  • 467
  • 2
  • 5
  • 16
  • 2
    *Use TypeScript instead, because it will handle this stuff.* really? – nils Jul 01 '16 at 08:06
  • 1
    @nils and change language each time you meet new trouble! – Paul Rumkin Jul 01 '16 at 08:12
  • I guess there's a variant #3 with doing `require` of ClassB inside the constructor of the ClassC. By the moment of constructor invocation the object with ClassB will already be in cache and thus ready for use. The problem is the code becomes really messy. – dazewell Jul 01 '16 at 08:24
  • This would work if you used ES6 imports. It cannot work with `module.exports = …`. – Bergi Jul 01 '16 at 08:26
  • @Bergi, if your comment addressed to me, than you're probably wrong. It works pretty well with node.js' exports. I would also argue on the duplicate status of this question since the talk here is about ES6 classes. – dazewell Jul 01 '16 at 08:31
  • @dazewell: No, it was directed at the OPs code. Regarding the dupe, it doesn't matter whether you use `function` or `class`, the problem is CommonJS exports. It would be a different question if it was about ES6 modules. – Bergi Jul 01 '16 at 08:32
  • @Bergi, just to clarify, do you mean imports could work if they were implemeted, or I can use them right now? I mean I'm not aware of possibility to use imports in NodeJs 4.x at least. – Aides Jul 01 '16 at 08:48
  • @Aides: They are not implemented, but you can use them with the help of a transpiler. – Bergi Jul 01 '16 at 09:32

1 Answers1

2

Try to move require call into the end of file with class definition. This is working example:

// a.js
class A {
    foo() {
        var c = new C();
        c.foo();
    }
}

module.exports = A;
const C = require('./c.js');

// b.js
class B {
    foo() {
        var c = new C();
        c.foo();
    }
}

module.exports = B;
const C = require('./c.js');

// c.js
class C {
    foo() {
        var a = new A();
        a.foo();
    }
}

module.exports = C;
const A = require('./a.js');

// main.js
const C = require('./c.js');
const B = require('./b.js');

const c = new C();
const b = new B();

c.foo();
b.foo();

NOTE That call c.foo() will cause max call stack error. But all class references are solved as you expected.

Paul Rumkin
  • 6,737
  • 2
  • 25
  • 35
  • Yes, it will work, thanks! But a minor downside is that I can't put all requires at the end of the file, e.g. for inheritance I need to require base class before class declaration any way. It looks like some requires will be at the top of the file and some at the end. Other than that this looks good. – Aides Jul 01 '16 at 09:06
  • @Aides: You cannot have circular inheritance anyway. The parent should not import its subclasses. – Bergi Jul 01 '16 at 09:33
  • @Aides yep there is a little caveat with separated dependency block. But this is the only way to resolve circular dependencies in node.js at definition time. – Paul Rumkin Jul 01 '16 at 09:45