I want to know if there is a way to idiomatically avoid issues with circular dependencies with Node.js's require
while using CoffeeScript classes and super
. Given the following simplified CoffeeScript files:
a.coffee:
C = require './c'
B = require './b'
class A extends C
b: B
someMethod: ->
super
module.exports = A
b.coffee:
C = require './c'
A = require './a'
class B extends C
a: A
someMethod: ->
super
module.exports = B
The first obvious issue here is that there is a circular dependency between A and B. Whichever one evaluates first will have {}
as a reference to the other. To resolve this in the general case, I might try to do something like this on each:
a.coffee:
C = require './c'
class A extends C
module.exports = A
B = require './b'
_ = require 'underscore'
_.extend A::,
b: B
someMethod: ->
super
This is a bit of a hack, but seems to be one common way of resolving circular dependencies by moving the module.exports
before the require
for the dependency B. Since CoffeeScript classes can't be reopened, it then uses an extend
call of some variety (this could be any way of copying properties and methods) onto A.prototype
(aka A::
) to finish the class. The problem with this now is that super
only works properly in the context of the class declaration, so this code won't compile. I'm looking for a way to preserve super
and other CoffeScript class functionality.
There's several canonical ways to handle this. None of them, in my opinion, particularly excellent. (Node really needs to support actually replacing the temporary object in the original context with the exported object, in cyclical situations. The benefits of that are worth doing some ugly, hacky V8 trickery, IMO. /rant )
Late construction
You could have a ‘higher-level’ module, perhaps the entry module to your library, preform the final setup of mutually-dependant things:
# <a.coffee>
module.exports =
class A extends require './c'
someMethod: ->
super
# <b.coffee>
module.exports =
class B extends require './c'
someMethod: ->
super
# <my_library.coffee>
A = require './a'
B = require './b'
A.b = new B
B.a = new A
module.exports = A: A, B: B
Horrible because: You've now conflated concerns in the higher-level module, and removed that setup-code from the context in which it makes sense (and in which it would hopefully remain maintained.) Great way to watch things get out of sync.
Dependency injection
We can improve on the above by moving the setup back into the concern of each individual submodule, and only removing the dependency management into the higher-level file. The dependencies will be acquired by the higher-level module (with no cycles), and then passed around as necessary:
# <a.coffee>
module.exports = ({B})-> ->
# Each module, in addition to being wrapped in a closure-producing
# function to allow us to close over the dependencies, is further
# wrapped in a function that allows us to defer *construction*.
B = B()
class A extends require './c'
b: new B
someMethod: ->
super
# <b.coffee>
module.exports = ({A})-> ->
# Each module, in addition to being wrapped in a closure-producing
# function to allow us to close over the dependencies, is further
# wrapped in a function that allows us to defer *construction*.
A = A()
class B extends require './c'
a: new A
someMethod: ->
super
# <my_library.coffee>
A = require './a'
B = require './b'
# First we close each library over its dependencies,
A = A(B)
B = B(A)
# Now we construct a copy of each (which each will then construct its own
# copy of its counterpart)
module.exports = A: A(), B: B()
# Consumers now get a constructed, final, 'normal' copy of each class.
Horrible because: Well, besides it being absolutely ugly in this specific scenario (!!?!), you've just pushed the solving-the-dependency-problem issue ‘up the stack’ to a consumer. In this situation, that consumer is still yourself, which works out okay ... but what happens, now, when you want to expose A
alone, via require('my_library/a')
? Now you've got to document to the consumer that they have to parameterize your submodules with X, Y, and Z dependencies ... and blah, blah, blah. Down the rabbit-hole.
Incomplete classes
So, to iterate on the above, we can abstract some of that dependency mess away from the consumer by implementing it directly on the class (thus keeping concerns local, as well):
# <a.coffee>
module.exports =
class A extends require './c'
@finish = ->
require './b'
@::b = new B
someMethod: ->
super
# <b.coffee>
module.exports =
class B extends require './c'
@finish = ->
require './a'
@::a = new A
someMethod: ->
super
# <my_library.coffee>
A = require './a'
B = require './b'
module.exports = A: A.finish(), B: B.finish()
Horrible because: Unfortunately, this is still adding some conceptual overhead to your API: “Make sure you always call A.finish()
before using A
!” might not go over well with your users. Similarly, it can cause obscure, hard-to-maintain bug-dependencies between your submodules: now, A can use elements of B ... except parts of B that depend on A. (And which parts those are, is likely to remain non-obvious during development.)
Resolve the cyclical dependencies
I can't write this part for you, but it's the only non-horrible solution; and it's the canonical one any Node programmer will come at you with, if you bring them this question. I've provided the above in the spirit of the Stack Overflow assumption that you know what you're doing (and have very good reason to have cyclical dependencies, and removing them would be non-trivial and more detrimental to your project than any of the downsides listed above) ... but in all reality, the most likely situation is that you just need to redesign your architecture to avoid cyclic dependencies. (Yes, I know this advice sucks.)
Best of luck! (=