Circular dependency resolution with Node.js requires classes in CoffeeScript

I want to know if there is a way to idiomatically avoid circular dependency issues with Node.js require when using the CoffeeScript and super classes. 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 problem here is that there is a circular relationship between A and B. Regardless of which one being evaluated will first have {} as a link to another. To resolve this in the general case, I can try to do something like this:

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 it seems to be one of the common ways to resolve circular dependencies by moving module.exports to require for dependency B. Since CoffeeScript classes cannot be reopened , then extend calls of some variety are used to complete the class (this may be any way to copy properties and methods) to A.prototype (aka A:: A.prototype . Now the problem is that super only works in the context of a class declaration, so this code will not compile. I am looking for a way to save super and other functions of the CoffeScript class.

+7
javascript coffeescript require circular-dependency
source share
1 answer

There are several canonical ways to handle this. None of them, in my opinion, were particularly excellent. (Node really needs to support the actual replacement of the temporary object in the original context by the exported object in cyclical situations. The benefits are worth doing some ugly hacker V8 cheaters, IMO./rant)

Late construction

You may have a higher-level module, perhaps an input module to your library, prepare the final configuration of interdependent 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' Ab = new B Ba = new A module.exports = A: A, B: B 

Awful because:. Now you have focused on the higher-level module and removed this installation code from the context in which it makes sense (and in which, we hope, it will remain.) A great way to watch how everything goes out of sync.

Dependency Inclusion

We can improve this by moving the setting back to the care of each individual submodule and excluding dependency management in a higher-level file. Dependencies will be obtained by a module of a higher level (without cycles), and then will be passed 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. 

It's terrible because: Well, besides the fact that it is absolutely ugly in this particular scenario (!!?!), You just pushed the problem of solving the problem-dependency to the consumer stack. In this situation, this consumer is still himself, which works well ... but what happens when you want to expose A only through require('my_library/a') ? Now you need to document the consumer that they should parameterize your submodules with dependencies X, Y and Z ... and blah, blah, blah. Down the rabbit hole.

Incomplete classes

So, to repeat the above, we can abstract some of these consumer dependency disorders by implementing them directly in the class (thereby preserving local problems 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() 

It's terrible because: Unfortunately, this still adds some conceptual overhead for your API: "Be sure to always call A.finish() before using A !" may not match your users. Likewise, this can cause obscure, hard-to-maintain error dependencies between your submodules: now A can use elements of B ... except for parts of B that depend on A. (And which parts are likely to remain unobvious during development.)

Allow circular dependencies

I cannot write this part for you, but it is the only ugly solution; and this is canonical, some Node programmer will come to you if you bring them this question. I presented the above assumptions about stack overflow that you know what you are doing (and you have every reason for cyclic dependencies, and removing them would be nontrivial and more detrimental to your project than any of the above) ... but in all reality, the most likely situation is that you just need to redesign your architecture to avoid circular dependencies. (Yes, I know this advice sucks.)

Good luck (=

+27
source share

All Articles