Resolución de dependencias circulares de Clojure
circular-dependency (5)
Estoy trabajando en un código de Clojure que tiene algunas dependencias circulares entre diferentes espacios de nombres y estoy tratando de encontrar la mejor manera de resolverlos.
- El problema básico es que obtengo un error "No such var var: namespace / functionname" en uno de los archivos
- Traté de "declarar" la función pero luego me queja con: "No se puede referir a una var calificada que no existe"
- Por supuesto, podría 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 dependencias circulares
- 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 terminaría siendo un desastre y arruinaría la agradable estructura modular actual que tengo con la funcionalidad relacionada agrupada juntos
¿Alguna idea? ¿Cuál es la mejor forma de manejar este tipo de dependencia circular en Clojure?
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.
Recuerdo varias 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 son un grito de diseño para refactorización En ocasiones, las soluciones pueden ser posibles, pero feas, posiblemente problemáticas para el rendimiento (si haces las cosas innecesariamente "dinámicas"), no se garantiza que funcionen para siempre, etc.
Ahora dices que la estructura del proyecto circular 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.
Tuve un problema similar con un código GUI, lo que terminé haciendo es,
(defn- frame [args]
((resolve ''project.gui/frame) args))
Esto me permitió resolver la llamada durante el tiempo de ejecución, esto se llama desde un elemento de menú en el marco, así que estaba 100% seguro de que el marco se definía porque se llamaba desde el propio marco, tenga en cuenta que resolver puede devolver nada.
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.
La resolución en el momento de la llamada puede ser posible, pero no será óptima. 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 exista en el mundo real.
Es bueno pensar cuidadosamente sobre el diseño. Las dependencias circulares pueden estar diciéndonos que estamos confundidos acerca de algo importante.
Aquí hay un truco que he usado 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 del código de Dan Holmsand en Reactivo .