2010-06-21 200 views
15

我正在研究一些Clojure代码,它在不同名称空间之间有一些循环依赖关系,我试图找出解决它们的最佳方法。解决Clojure循环依赖关系

  • 基本问题是,我得到一个“没有这样的变种:命名空间/ functionname”在
  • 我试图“申报”功能中的一个文件的错误,但然后将它与抱怨:“不能指到一个不存在的合格变量“
  • 我当然可以重构整个代码库,但这样做似乎不切实际,每次你有一个依赖关系来解决.....并且可能会对某些特定的圆形网络变得非常难看依赖关系
  • 我可以分离出一堆接口/协议/声明到一个单独的文件,并让所有的东西都指向那个....但是这似乎很糟糕d最终变得凌乱,并破坏当前很好的模块化结构,我与相关的功能组合在一起

有什么想法?在Clojure中处理这种循环依赖的最好方法是什么?

回答

23

我记得在Clojure命名空间上的一些讨论 - 在邮件列表和其他地方 - 我必须告诉你,共识(和AFAICT,Clojure设计的当前方向)是循环依赖一个设计的重构呼声。解决方法有时可能是可行的,但很难,可能是性能问题(如果你做的事情不必要地“动态”),不保证永远工作等。

现在你说,循环项目结构很好,模块化。但是,为什么你会这样说,如果一切都依赖于一切......?此外,如果您提前计划树状依赖关系结构,“每次有依赖解决时”都不应该经常出现。为了解决你在自己的命名空间中放置一些基本协议等的想法,我必须说很多时候我希望项目能够做到这一点。我发现它能够浏览一个代码库并获得它快速处理的抽象概念的能力,这是非常有帮助的。总结一下,我的投票是重构。

+1

感谢米哈尔的洞察力和有用的背景!我仍然不相信总是避免循环依赖必然是项目结构的最佳设计选择。请看看Clojure小组,看看它是否能说服我,否则:-) – mikera 2010-06-22 16:12:51

+2

一个小小的更新 - 将协议放在他们自己的命名空间中运行良好,解决了大部分问题,我通常最终添加了一个(:use [协议])到大多数其他ns申报和一切“只是工作”。唯一我仍然觉得难以解决的地方是你声明一个你想在声明之前引用的类(例如deftype)(例如作为协议定义中的类型提示!!) – mikera 2010-07-06 12:42:50

+1

感谢更新,很高兴听到!我认为用实际实现类的名字暗示协议/接口函数可能不是一个好主意,但实际上我的印象是协议方法根本不能被暗示,但接口方法可以和参数是相同):改为使用接口的名称。如果你正在处理一个'deftype'创建的类,它的所有方法无论如何都是'Object'/interface/protocol方法。我唯一使用提示指向类的时候是互操作所需要的。 – 2010-07-06 22:47:33

12

我也有类似的问题,一些GUI代码,我最后做的是,

(defn- frame [args] 
    ((resolve 'project.gui/frame) args)) 

这让我来解决运行时调用,这会从在框架的菜单项称为所以我100%肯定帧被定义,因为它是从帧本身调用的,请记住,解析可能会返回nil。

+1

这变得非常快。如果可能的话,我会建议重构命名空间。 – celwell 2015-10-12 21:51:56

1

将所有内容移动到一个巨大的源文件中,以便您没有外部依赖关系,否则重构。就我个人而言,我会与重构一起去,但是当你真正做到这一点时,就是关于美学。有些人喜欢KLOCS和意大利面代码,所以没有口味的味道。

8

我不断有这个相同的问题。尽管许多开发人员不想承认这一点,但这是该语言中严重的设计缺陷。循环依赖是真实对象的正常状态。没有一颗心,身体就无法生存,没有身体,心就无法生存。

解决在通话时间可能是可能的,但它不会是最佳的。考虑一下你有API的情况,因为API的一部分是错误报告方法,但API创建了一个拥有自己方法的对象,这些对象将需要错误报告,并且你有循环依赖。错误检查和报告功能将经常被调用,因此在被调用时解析不是一种选择。

在这种情况下,大多数情况下,解决方案是将没有依赖关系的代码移动到可以自由共享的独立(util)命名空间中。我还没有遇到过这种技术无法解决问题的情况。这使得维护完整的,功能性的业务对象几乎是不可能的,但它似乎是唯一的选择。 Clojure在成为一种能够准确建模现实世界的成熟语言之前还有很长的路要走,直到以不合逻辑的方式分割代码才是消除这些依赖关系的唯一方法。如果Aa()依赖于Ba()而Bb()依赖于Ab(),唯一的解决方案是将Ba()转换为Ca()和/或Ab()转换为Cb(),即使C在技术上不适用在现实世界中不存在。

+2

身体和心脏不是组成或设计为可组合。命名空间应该是。您只需“建模现实世界”即可获得组合性。 – 2014-04-10 11:33:45

+1

命名空间的存在仅仅是为了能够在不发生冲突的情况下在不同的上下文中重用相同的名称。通过建模真实世界得到的是一种直观且可维护的设计。我不会质疑心脏或身体的可组合性,但有很多情况表明它们确实是可组合的。 – 2014-04-10 12:27:25

+1

如果您在避免名称冲突的意义上严格地讨论命名空间,则应该知道不会强加依赖性约束。您可以制作名称空间符号和关键字。依赖关系来自'require'。有一个命令将lib加载*:LIB1 *需要* LIB2,因此LIB2将被加载*作为LIB1的一部分。你知道当LIB2 *需要* LIB1时会发生什么? - 当然。一个解决方案就是忽略它,只是等待看看运行时会发生什么。 Hickey评论了他为什么选择不去 2014-04-10 12:59:28

0

很好地仔细考虑设计。循环依赖可能告诉我们,我们对重要的事情感到困惑。

这里有一个技巧,我曾经在一种或两种情况下解决循环依赖问题。

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 
;; example/a.cljc 

(ns example.a 
    (:require [example.b :as b])) 

(defn foo [] 
    (println "foo")) 

#?(

    :clj 
    (alter-var-root #'b/foo (constantly foo))    ; <- in clojure do this 

    :cljs 
    (set! b/foo foo)           ; <- in clojurescript do this 

    ) 

(defn barfoo [] 
    (b/bar) 
    (foo)) 

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 
;; example/b.cljc 

(ns example.b) 

;; Avoid circular dependency. This gets set by example.a 
(defonce foo nil) 

(defn bar [] 
    (println "bar")) 

(defn foobar [] 
    (foo) 
    (bar)) 

我从Dan Holmsand's code in Reagent了解到这个把戏。