解决与Node.js的循环依赖关系需要和CoffeeScript中的类

时间:2022-10-04 11:59:06

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:

我想知道是否有一种方法可以在使用CoffeeScript类和super时以惯用方式避免使用Node.js需要的循环依赖性问题。鉴于以下简化的CoffeeScript文件:

a.coffee:

一杯咖啡:

C = require './c'
B = require './b'

class A extends C
    b: B

    someMethod: ->
        super

module.exports = A

b.coffee:

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和B之间存在循环依赖关系。无论哪个首先评估,都将{}作为对另一个的引用。要在一般情况下解决这个问题,我可能会尝试在每个上执行以下操作:

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.

一些hack,但似乎是通过在依赖关系B的需求之前移动module.exports来解决循环依赖关系的一种常见方法。由于CoffeeScript类无法重新打开,因此它使用了一些扩展调用变种(这可以是任何复制属性和方法的方法)到A.prototype(又名A::)来完成课程。现在的问题是super只能在类声明的上下文中正常工作,所以这段代码不会编译。我正在寻找一种方法来保留超级和其他CoffeScript类功能。

1 个解决方案

#1


27  

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 )

有几种规范方法可以解决这个问题。在我看来,他们都不是特别优秀。 (节点确实需要支持在周期性情况下用导出的对象实际替换原始上下文中的临时对象。这样做的好处值得做一些丑陋,hacky的V8技巧,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.

可怕的原因是:嗯,除了它在这个特定场景中绝对丑陋(!!?!)之外,你只是将解决依赖问题问题“推向堆栈”推向消费者。在这种情况下,那个消费者仍然是你自己,这可以解决...但是现在,当你想通过require('my_library / a')单独公开A时会发生什么?现在你必须向消费者记录他们必须使用X,Y和Z依赖项来参数化你的子模块......以及blah,blah,blah。在兔子洞下面。

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.)

可怕的原因是:不幸的是,这仍然会给你的API增加一些概念上的开销:“确保你在使用A之前总是调用A.finish()!”可能不会与你的用户相提并论。类似地,它可能会导致子模块之间存在模糊,难以维护的错误依赖关系:现在,A可以使用B的元素...除了依赖于A的B部分(以及那些部分可能会保留)在开发过程中不明显。)

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.)

我不能为你写这部分,但它是唯一不可怕的解决方案;如果你把这个问题带给他们,那么任何Node程序员都会向你发出规范。我已经在Stack Overflow假设的精神中提供了上述内容,你知道你正在做什么(并且有充分的理由拥有周期性的依赖关系,删除它们对你的项目来说是非常重要的,对任何上面列出的缺点)...但实际上,最可能的情况是你只需要重新设计你的架构以避免循环依赖。 (是的,我知道这个建议很糟糕。)

Best of luck! (=

祝你好运! (=

#1


27  

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 )

有几种规范方法可以解决这个问题。在我看来,他们都不是特别优秀。 (节点确实需要支持在周期性情况下用导出的对象实际替换原始上下文中的临时对象。这样做的好处值得做一些丑陋,hacky的V8技巧,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.

可怕的原因是:嗯,除了它在这个特定场景中绝对丑陋(!!?!)之外,你只是将解决依赖问题问题“推向堆栈”推向消费者。在这种情况下,那个消费者仍然是你自己,这可以解决...但是现在,当你想通过require('my_library / a')单独公开A时会发生什么?现在你必须向消费者记录他们必须使用X,Y和Z依赖项来参数化你的子模块......以及blah,blah,blah。在兔子洞下面。

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.)

可怕的原因是:不幸的是,这仍然会给你的API增加一些概念上的开销:“确保你在使用A之前总是调用A.finish()!”可能不会与你的用户相提并论。类似地,它可能会导致子模块之间存在模糊,难以维护的错误依赖关系:现在,A可以使用B的元素...除了依赖于A的B部分(以及那些部分可能会保留)在开发过程中不明显。)

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.)

我不能为你写这部分,但它是唯一不可怕的解决方案;如果你把这个问题带给他们,那么任何Node程序员都会向你发出规范。我已经在Stack Overflow假设的精神中提供了上述内容,你知道你正在做什么(并且有充分的理由拥有周期性的依赖关系,删除它们对你的项目来说是非常重要的,对任何上面列出的缺点)...但实际上,最可能的情况是你只需要重新设计你的架构以避免循环依赖。 (是的,我知道这个建议很糟糕。)

Best of luck! (=

祝你好运! (=