2010-06-21 23 views
15

Estoy trabajando en algunos códigos Clojure que tienen algunas dependencias circulares entre diferentes espacios de nombres y estoy tratando de encontrar la mejor manera de resolverlos.Resolviendo las dependencias circulares de Clojure

  • problema básico es que me sale un "No existe tal var: espacio de nombres/functionname" error en uno de los archivos
  • he tratado de "declarar" la función, pero luego se queja con: "No se puede hacer referencia a una var calificada que no existe "
  • Podría, por supuesto, refactorizar toda la base de código, pero parece poco práctico hacerlo cada vez que tenga una dependencia para resolver ..... y podría ser muy desagradable para ciertas redes de circular dependencias
  • Podría separar un montón de interfaces/protocolos/declaraciones en un archivo separado y hacer que todo se refiera a eso .... pero parece que eso d terminé desordenado y arruiné la agradable estructura modular actual que tengo con funcionalidades relacionadas agrupadas juntas

¿Alguna idea? ¿Cuál es la mejor forma de manejar este tipo de dependencia circular en Clojure?

Respuesta

23

Recuerdo una serie de discusiones sobre espacios de nombres en Clojure, en la lista de correo y en otros lugares, y debo decirles que el consenso (y, AFAICT, la orientación actual del diseño de Clojure) es que las dependencias circulares un grito de diseño para refactorizar. Las soluciones pueden ser ocasionalmente posibles, pero feas, posiblemente problemáticas para el rendimiento (si hace las cosas innecesariamente "dinámicas"), no garantiza que funcionen para siempre, etc.

Ahora dice que la estructura circular del proyecto es agradable y modular. Pero, ¿por qué lo llamarías así si todo depende de todo ...? Además, "cada vez que tenga una dependencia para resolver" no debería ser muy frecuente si planifica una estructura de dependencia similar a un árbol antes de tiempo. Y para abordar su idea de poner algunos protocolos básicos y similares en su propio espacio de nombres, debo decir que muchas veces deseé que los proyectos hicieran precisamente eso. Encuentro tremendamente útil mi habilidad para escanear una base de código y tener una idea de con qué tipo de abstracciones está trabajando rápidamente.

Para resumir, mi voto va a la refactorización.

+1

¡Gracias a Michal por la información y el trasfondo útil! Todavía no estoy convencido de que evitar las dependencias circulares sea necesariamente la mejor opción de diseño para la estructuración de proyectos. Echaré un vistazo al grupo de Clojure y veré si eso puede convencerme de lo contrario :-) – mikera

+2

Una pequeña actualización: poner los protocolos en su propio espacio de nombres ha funcionado bien y solucionó la mayoría de los problemas, generalmente termino agregando un (: uso [protocolos]) a la mayoría de las declaraciones de ns y todo "simplemente funciona". Lo único que todavía me resulta desagradable es el hecho de que declares una clase (por ejemplo, un deftype) a la que quieras hacer referencia antes de declararla (por ejemplo, como una sugerencia de tipo en una definición de protocolo). – mikera

+1

Gracias por la actualización, feliz de escuchar eso! Creo que insinuar funciones de protocolo/interfaz con los nombres de las clases reales de implementación puede no ser una muy buena idea, (en realidad tenía la impresión de que los métodos de protocolo no se podían insinuar en absoluto, pero los métodos de interfaz pueden y el argumento es el mismo): sugerencia con el nombre de la interfaz en su lugar. Si se trata de una clase creada con 'deftype', todos sus métodos serán' Object'/interface/protocol methods de todos modos. La única vez que usaría sugerencias para apuntar a las clases es cuando eso es necesario para la interoperabilidad. –

12

He tenido un problema similar con algún código GUI, lo que terminó haciendo es,

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

Esto me permitió resolver la llamada en tiempo de ejecución, esto se llama desde un elemento de menú en el marco de lo que era Se definió el 100% de marco seguro porque se estaba llamando desde el marco en sí, tenga en cuenta que la resolución puede ser nula.

+1

Esto se pone feo muy rápido. Sugeriría refactorizar espacios de nombres si es posible. – celwell

1

Mueva todo a un archivo fuente gigante para que no tenga dependencias externas, o bien refactorice. Personalmente, me gustaría ir con Refactor, pero cuando realmente se llega a eso, se trata de estética. A algunas personas les gusta KLOCS y el código de spaghetti, por lo que no hay ninguna explicación para el gusto.

8

Estoy teniendo este mismo problema constantemente. Por mucho que muchos desarrolladores no quieran admitirlo, es un defecto de diseño serio en el lenguaje. Las dependencias circulares son una condición normal de los objetos reales. Un cuerpo no puede sobrevivir sin un corazón, y el corazón no puede sobrevivir sin el cuerpo.

Resolviendo en el tiempo de llamada puede ser posible, pero no será óptimo. Considere el caso en el que tiene una API, ya que parte de esa API son los métodos de informe de error, pero la API crea un objeto que tiene sus propios métodos, esos objetos necesitarán informes de errores y usted tendrá su dependencia circular.A menudo se invocarán las funciones de informe y comprobación de errores, por lo que no es posible resolver en el momento en que se llaman.

La solución en este caso, y en la mayoría de los casos, es mover el código que no tiene dependencias a espacios de nombres separados (utilitarios) donde se pueden compartir libremente. Todavía no me he encontrado con un caso en el que el problema no se pueda resolver con esta técnica. Esto hace que el mantenimiento de objetos comerciales completos y funcionales sea casi imposible, pero parece ser la única opción. Clojure tiene un largo camino por recorrer antes de que sea un lenguaje maduro capaz de modelar con precisión el mundo real, hasta entonces dividir el código de manera ilógica es la única forma de eliminar estas dependencias.

Si Aa() depende de Ba() y Bb() depende de Ab() la única solución es mover Ba() a Ca() y/o Ab() a Cb() aunque C técnicamente no existen en el mundo real

+2

El cuerpo y el corazón no están compuestos ni diseñados para ser compostables. Los espacios de nombres deberían ser No obtienes compibilidad simplemente "modelando el mundo real". –

+1

Los espacios de nombres existen con el único propósito de poder reutilizar los mismos nombres en diferentes contextos sin una colisión. Lo que obtienes modelando el mundo real es un diseño intuitivo y fácil de mantener. No voy a disputar la composición de los corazones o cuerpos, pero hay muchos casos que muestran que son de hecho composable. –

+1

Si está hablando de espacios de nombres estrictamente en el sentido de evitar la colisión de nombres, debe saber que no se imponen restricciones de dependencia. Puede fabricar tanto símbolos de nombre como palabras clave. Las dependencias vienen con 'require'. Hay un orden en el que las bibliotecas están * cargadas *: LIB1 * requiere * LIB2, por lo tanto, LIB2 se * cargará * como parte de LIB1. ¿Sabes lo que sucede cuando LIB2 * requiere * LIB1? - Por supuesto. Una solución sería ignorar eso y simplemente esperar a ver qué pasa en tiempo de ejecución. Hickey comentó por qué eligió no

0

Es bueno pensar detenidamente sobre el diseño. Las dependencias circulares pueden estar diciéndonos que estamos confundidos acerca de algo importante.

He aquí un truco que he utilizado para solucionar las dependencias circulares en uno o dos casos.

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

aprendí este truco de Dan Holmsand's code in Reagent.

Cuestiones relacionadas