使用 Node.js require 和 CoffeeScript 中的类解决循环依赖
我想知道在使用 CoffeeScript 类和 super
时,是否有一种方法可以惯用地避免 Node.js 的 require
的循环依赖问题.给定以下简化的 CoffeeScript 文件:
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:
一杯咖啡:
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
这里第一个明显的问题是 A 和 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:
一杯咖啡:
C = require './c'
class A extends C
module.exports = A
B = require './b'
_ = require 'underscore'
_.extend A::,
b: B
someMethod: ->
super
这有点小技巧,但似乎是解决循环依赖关系的一种常用方法,方法是将 module.exports
移动到依赖项 B 的 require
之前.由于 CoffeeScript 类无法重新打开,因此它使用了一些 extend
调用多样性(这可以是复制属性和方法的任何方式)到 A.prototype
(又名 A::
)以完成课程.现在的问题是 super
只能在类声明的上下文中正常工作,所以这段代码不会编译.我正在寻找一种方法来保留 super
和其他 CoffeScript 类功能.
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.
推荐答案
有几种规范的方法来处理这个问题.在我看来,它们都不是特别出色的.(节点真的需要支持在循环情况下实际上用导出的对象替换原始上下文中的临时对象.这样做的好处是值得做一些丑陋的,hacky V8诡计,IMO./咆哮)
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 )
你可以有一个更高级别"的模块,也许是你的库的入口模块,执行相互依赖的事物的最终设置:
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.
我们可以通过将设置移回每个单独的子模块的关注点来改进上述内容,并且只将 依赖管理 删除到更高级别的文件中.依赖关系将由更高级别的模块获取(没有循环),然后根据需要传递:
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.
太可怕了,因为:好吧,除了在这个特定场景中它绝对丑陋(!!?!),你只是把解决依赖问题的问题推到了堆栈上给消费者.在这种情况下,那个消费者仍然是你自己,这很好......但是现在,当你想通过 require 公开
?现在您必须向消费者说明他们必须使用 X、Y 和 Z 依赖项来参数化您的子模块……等等.进入兔子洞.A
alone 时会发生什么('my_library/a')
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.
因此,为了对上述内容进行迭代,我们可以通过直接在类上实现它来从消费者那里抽象出一些依赖混乱(从而也将关注点保持在本地):
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()
太糟糕了,因为:不幸的是,这仍然给你的 API 增加了一些概念上的开销:确保在使用 A 之前总是调用
A.finish()
代码>!"可能不会与您的用户相处得很好.类似地,它可能会导致您的子模块之间产生模糊、难以维护的错误依赖关系:现在,A 可以使用 B 的元素......除了 B 的依赖于 A 的部分.(以及哪些部分这些是,可能在开发过程中仍然不明显.)
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.)
我不能为你写这部分,但这是唯一不可怕的解决方案;如果您向他们提出这个问题,它是任何 Node 程序员都会向您提出的规范问题.我本着 Stack Overflow 假设的精神提供了上述内容,即您知道自己在做什么(并且有非常好的理由拥有周期性依赖关系,并且删除它们将是不平凡的,而且更多比上面列出的任何缺点都对您的项目有害)...但实际上,最有可能的情况是您只需要重新设计架构以避免循环依赖.(是的,我知道这个建议很糟糕.)
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.)
祝你好运!(=
相关文章