¿Qué hay de malo con las referencias circulares?


160

Estuve involucrado en una discusión de programación hoy donde hice algunas declaraciones que básicamente asumían axiomáticamente que las referencias circulares (entre módulos, clases, lo que sea) son generalmente malas. Una vez que terminé con mi discurso, mi compañero de trabajo preguntó: "¿qué hay de malo en las referencias circulares?"

Tengo fuertes sentimientos al respecto, pero es difícil para mí verbalizar de manera concisa y concreta. Cualquier explicación que se me ocurra tiende a depender de otros elementos que yo también considero axiomas ("no se puede utilizar de forma aislada, por lo que no se puede probar", "comportamiento desconocido / indefinido como estado mutante en los objetos participantes", etc. .), pero me encantaría escuchar una razón concisa de por qué las referencias circulares son malas que no toman el tipo de saltos de fe que hace mi propio cerebro, después de pasar muchas horas a lo largo de los años desenredando para entender, arreglar, y extender varios bits de código.

Editar: no estoy preguntando sobre referencias circulares homogéneas, como las de una lista doblemente enlazada o puntero a padre. Esta pregunta realmente se refiere a referencias circulares de "mayor alcance", como libA llamando a libB que vuelve a llamar a libA. Sustituya 'módulo' por 'lib' si lo desea. ¡Gracias por todas las respuestas hasta ahora!


¿La referencia circular pertenece a bibliotecas y archivos de encabezado? En un flujo de trabajo, el nuevo código de ProjectB procesará un archivo que sale del código heredado de ProjectA. Esa salida de ProjectA es un nuevo requisito impulsado por ProjectB; ProjectB tiene un código que facilita la determinación genérica de qué campos van a dónde, etc. El punto es que el ProjectA heredado podría reutilizar el código en el nuevo ProjectB, y ProjectB sería una tontería no reutilizar el código de utilidad en el ProjectA heredado (por ejemplo: detección y transcodificación de juegos de caracteres, análisis de registros, validación y transformación de datos, etc.).
Luv2code

1
@ Luv2code Solo se vuelve tonto si corta y pega el código entre proyectos o posiblemente cuando ambos proyectos se compilan y enlazan en el mismo código. Si comparten recursos como este, colóquelos en una biblioteca.
dash-tom-bang

Respuestas:


220

Hay muchas cosas mal con las referencias circulares:

  • Las referencias de clase circulares crean un alto acoplamiento ; ambas clases se deben volver a compilar cada vez que se cambie una de ellas.

  • Las referencias de ensamblaje circular evitan la vinculación estática , porque B depende de A pero A no se puede ensamblar hasta que B esté completo.

  • Las referencias de objetos circulares pueden bloquear algoritmos recursivos ingenuos (como serializadores, visitantes e impresoras bonitas) con desbordamientos de pila. Los algoritmos más avanzados tendrán detección de ciclo y simplemente fallarán con un mensaje de excepción / error más descriptivo.

  • Las referencias de objetos circulares también hacen imposible la inyección de dependencia , reduciendo significativamente la capacidad de prueba de su sistema.

  • Los objetos con una gran cantidad de referencias circulares son a menudo objetos de Dios . Incluso si no lo son, tienden a conducir al Código de Espagueti .

  • Las referencias de entidades circulares (especialmente en bases de datos, pero también en modelos de dominio) evitan el uso de restricciones de no nulabilidad , que eventualmente pueden conducir a la corrupción de datos o al menos inconsistencia.

  • Las referencias circulares en general son simplemente confusas y aumentan drásticamente la carga cognitiva cuando se intenta comprender cómo funciona un programa.

Por favor, piense en los niños; evite referencias circulares siempre que pueda.


32
Aprecio especialmente el último punto, "carga cognitiva" es algo de lo que soy muy consciente pero que nunca tuve un término conciso.
dash-tom-bang

66
Buena respuesta. Sería mejor si dijeras algo sobre las pruebas. Si los módulos A y B son mutuamente dependientes, deben probarse juntos. Esto significa que en realidad no son módulos separados; juntos son un módulo roto
Kevin Cline

55
La inyección de dependencia no es imposible con referencias circulares, incluso con DI automático. Solo habrá que inyectarle una propiedad en lugar de un parámetro constructor.
BlueRaja - Danny Pflughoeft

3
@ BlueRaja-DannyPflughoeft: Considero que es un antipatrón, como lo hacen muchos otros practicantes de DI, porque (a) no está claro que una propiedad sea realmente una dependencia, y (b) el objeto que se "inyecta" no puede fácilmente realizar un seguimiento de sus propios invariantes. Peor aún, muchos de los marcos más sofisticados / populares como Castle Windsor no pueden dar mensajes de error útiles si no se puede resolver una dependencia; terminas con una molesta referencia nula en lugar de una explicación detallada de exactamente qué dependencia en qué constructor no se pudo resolver. Solo porque puedas , no significa que debas .
Aaronaught

3
No decía que fuera una buena práctica, solo señalaba que no es imposible como se afirma en la respuesta.
BlueRaja - Danny Pflughoeft

22

Una referencia circular es dos veces el acoplamiento de una referencia no circular.

Si Foo sabe acerca de Bar, y Bar sabe acerca de Foo, tiene dos cosas que deben cambiarse (cuando se requiere que Foos y Bars ya no se sepan entre sí). Si Foo sabe sobre Bar, pero un Bar no sabe sobre Foo, puedes cambiarlo sin tocar Bar.

Las referencias cíclicas también pueden causar problemas de arranque, al menos en entornos que duran mucho tiempo (servicios implementados, entornos de desarrollo basados ​​en imágenes), donde Foo depende de que Bar funcione para cargar, pero Bar también depende de que Foo funcione para carga.


17

Cuando unes dos bits de código, tienes un gran código. La dificultad de mantener un poco de código es al menos el cuadrado de su tamaño, y posiblemente mayor.

Las personas a menudo miran la complejidad de una sola clase (/ función / archivo / etc.) y olvidan que realmente debería considerar la complejidad de la unidad separable (encapsulable) más pequeña. Tener una dependencia circular aumenta el tamaño de esa unidad, posiblemente de forma invisible (hasta que comience a intentar cambiar el archivo 1 y se dé cuenta de que también requiere cambios en los archivos 2-127).


14

Pueden ser malos no por sí mismos sino como un indicador de un posible diseño deficiente. Si Foo depende de Bar y Bar depende de Foo, se justifica preguntarse por qué son dos en lugar de un FooBar único.


10

Hmm ... eso depende de lo que quieras decir con dependencia circular, porque en realidad hay algunas dependencias circulares que creo que son muy beneficiosas.

Considere un DOM XML: tiene sentido que cada nodo tenga una referencia a su padre y que cada padre tenga una lista de sus hijos. La estructura es lógicamente un árbol, pero desde el punto de vista de un algoritmo de recolección de basura o similar, la estructura es circular.


1
¿No sería eso un árbol?
Conrad Frix

@Conrad: supongo que podría considerarse como un árbol, sí. ¿Por qué?
Billy ONeal

1
No creo que los árboles sean circulares porque puede navegar hacia abajo por sus elementos secundarios y terminará (independientemente de la referencia principal). A menos que un nodo tuviera un hijo que también fuera un antepasado, que en mi opinión lo convierte en un gráfico y no en un árbol.
Conrad Frix

55
Una referencia circular sería si uno de los hijos de un nodo regresara a un antepasado.
Matt Olenik

Esto no es realmente una dependencia circular (al menos no de una manera que cause problemas). Por ejemplo, imagine que Nodees una clase, que tiene otras referencias Nodepara los niños dentro de sí misma. Debido a que solo hace referencia a sí misma, la clase es completamente autónoma y no está acoplada a nada más. --- Con este argumento, podría argumentar que una función recursiva es una dependencia circular. Que es (de un tirón), pero no en una mala manera.
byxor

9

Es como el problema del huevo o la gallina .

Hay muchos casos en los que la referencia circular es inevitable y útil, pero, por ejemplo, en el siguiente caso no funciona:

El proyecto A depende del proyecto B y B depende de A. A debe compilarse para usarse en B, lo que requiere que B se compile antes que A, lo que requiere que B se compile antes que A ...


6

Si bien estoy de acuerdo con la mayoría de los comentarios aquí, me gustaría defender un caso especial para la referencia circular "padre" / "hijo".

Una clase a menudo necesita saber algo sobre su clase principal o propietaria, tal vez el comportamiento predeterminado, el nombre del archivo del que provienen los datos, la instrucción sql que seleccionó la columna o la ubicación de un archivo de registro, etc.

Puede hacer esto sin una referencia circular al tener una clase contenedor de modo que lo que antes era el "padre" ahora sea un hermano, pero no siempre es posible re-factorizar el código existente para hacer esto.

La otra alternativa es pasar todos los datos que un niño pueda necesitar en su constructor, lo que termina siendo simplemente horrible.


En una nota relacionada, hay dos razones comunes por las que X puede tener una referencia a Y: X puede pedirle a Y que haga cosas en nombre de X, o Y puede esperar que X le haga cosas a Y, en nombre de Y. Si las únicas referencias que existen a Y son para el propósito de otros objetos que desean hacer cosas en nombre de Y, entonces los titulares de dichas referencias deben saber que los servicios de Y ya no son necesarios y que deben abandonar sus referencias a Y en Su conveniencia.
supercat

5

En términos de base de datos, las referencias circulares con relaciones PK / FK adecuadas hacen que sea imposible insertar o eliminar datos. Si no puede eliminar de la tabla a menos que el registro haya desaparecido de la tabla b y no puede eliminarlo de la tabla b a menos que el registro haya desaparecido de la tabla A, no puede eliminarlo. Lo mismo con los insertos. Es por eso que muchas bases de datos no le permiten configurar actualizaciones o eliminaciones en cascada si hay una referencia circular porque en algún momento, no es posible. Sí, puede establecer este tipo de relaciones sin que la PK / Fk se declare formalmente, pero luego (100% del tiempo en mi experiencia) tendrá problemas de integridad de datos. Eso es solo un mal diseño.


4

Tomaré esta pregunta desde el punto de vista de modelado.

Mientras no agregue ninguna relación que no esté realmente allí, estará a salvo. Si los agrega, obtendrá menos integridad en los datos (porque hay redundancia) y un código más estrechamente acoplado.

Lo que ocurre específicamente con las referencias circulares es que no he visto un caso en el que realmente se necesiten, excepto una: auto referencia. Si modela árboles o gráficos, lo necesita y está perfectamente bien porque la autorreferencia es inofensiva desde el punto de vista de la calidad del código (no se agrega dependencia).

Creo que en el momento en que comienza a necesitar una referencia que no sea de uno mismo, inmediatamente debe preguntar si no puede modelarla como un gráfico (contraiga las múltiples entidades en un solo nodo). Tal vez hay un caso intermedio en el que haces una referencia circular pero modelarlo como gráfico no es apropiado, pero lo dudo mucho.

Existe el peligro de que las personas piensen que necesitan una referencia circular, pero de hecho no la necesitan. El caso más común es "El caso uno de muchos". Por ejemplo, tiene un cliente con varias direcciones, desde el cual se debe marcar como la dirección principal. Es muy tentador modelar esta situación como dos relaciones separadas has_address y is_primary_address_of pero no es correcto. La razón es que ser la dirección principal no es una relación separada entre usuarios y direcciones, sino que es un atributo de la relación que tiene dirección. ¿Porqué es eso? Porque su dominio está limitado a las direcciones del usuario y no a todas las direcciones que hay. Elija uno de los enlaces y márquelo como el más fuerte (primario).

(Ahora vamos a hablar sobre bases de datos) Muchas personas optan por la solución de dos relaciones porque entienden que "primario" es un puntero único y una clave externa es una especie de puntero. Entonces, la clave externa debería ser lo que hay que usar, ¿verdad? Incorrecto. Las claves foráneas representan relaciones, pero "primario" no es una relación. Es un caso degenerado de un orden donde un elemento está por encima de todo y el resto no está ordenado. Si necesita modelar un orden total, por supuesto lo consideraría como un atributo de la relación porque básicamente no hay otra opción. Pero en el momento en que lo degeneras, hay una opción y una muy horrible: modelar algo que no es una relación como una relación. Así que aquí viene: redundancia en las relaciones que ciertamente no es algo que deba subestimarse.

Por lo tanto, no permitiría que ocurriera una referencia circular a menos que sea absolutamente claro que proviene de lo que estoy modelando.

(nota: esto está ligeramente sesgado al diseño de la base de datos, pero apuesto a que también es bastante aplicable a otras áreas)


2

Yo respondería esa pregunta con otra pregunta:

¿Qué situación me puede dar donde mantener un modelo de referencia circular es el mejor modelo para lo que está tratando de construir?

Desde mi experiencia, el mejor modelo casi nunca involucrará referencias circulares en la forma en que creo que lo dices en serio. Dicho esto, hay muchos modelos en los que usas referencias circulares todo el tiempo, es extremadamente básico. Relaciones padre -> Hijo, cualquier modelo de gráfico, etc., pero estos son modelos bien conocidos y creo que te estás refiriendo a algo completamente diferente.


1
PUEDE ser que una lista circular enlazada (simple o doble) sea una estructura de datos excelente para la cola de eventos central para un programa que se supone que "nunca se detiene" (pegue las N cosas importantes en la cola, con un conjunto de marcadores "no eliminar", luego simplemente recorra la cola hasta que esté vacío; cuando se necesiten nuevas tareas (transitorias o permanentes), péguelas en un lugar adecuado en la cola; siempre que sirva un evento sin el indicador "no eliminar" , hazlo y luego sácalo de la cola).
Vatine

1

Las referencias circulares en las estructuras de datos son a veces la forma natural de expresar un modelo de datos. En cuanto a la codificación, definitivamente no es ideal y puede resolverse (hasta cierto punto) mediante inyección de dependencia, empujando el problema del código a los datos.


1

Una construcción de referencia circular es problemática, no solo desde el punto de vista del diseño, sino también desde el punto de vista de la captura de errores.

Considere la posibilidad de una falla en el código. No ha colocado la detección de errores adecuada en ninguna de las clases, ya sea porque aún no ha desarrollado sus métodos o porque es flojo. De cualquier manera, no tiene un mensaje de error que le diga qué ocurrió, y necesita depurarlo. Como buen diseñador de programas, usted sabe qué métodos están relacionados con qué procesos, por lo que puede reducirlo a los métodos relevantes para el proceso que causó el error.

Con referencias circulares, sus problemas ahora se han duplicado. Debido a que sus procesos están estrechamente vinculados, no tiene forma de saber qué método en qué clase pudo haber causado el error, o de dónde vino el error, porque una clase depende de la otra depende de la otra. Ahora debe pasar tiempo probando ambas clases en conjunto para averiguar cuál es realmente responsable del error.

Por supuesto, la captura correcta de errores resuelve esto, pero solo si sabe cuándo es probable que ocurra un error. Y si está utilizando mensajes de error genéricos, todavía no está mucho mejor.


1

Algunos recolectores de basura tienen problemas para limpiarlos, porque cada objeto está siendo referenciado por otro.

EDITAR: Como se señala en los comentarios a continuación, esto es cierto solo para un intento extremadamente ingenuo de un recolector de basura, no uno que se encuentre en la práctica.


11
Hmm ... cualquier recolector de basura tropezado con esto no es un verdadero recolector de basura.
Billy ONeal

11
No conozco ningún recolector de basura moderno que tenga problemas con las referencias circulares. Las referencias circulares son un problema si está utilizando recuentos de referencias, pero la mayoría de los recolectores de basura tienen un estilo de rastreo (donde comienza con la lista de referencias conocidas y las sigue para encontrar todas las demás, recolectando todo lo demás).
Dean Harding

44
Consulte sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf, quien explica los inconvenientes a varios tipos de recolectores de basura, si marca y barre y comienza a optimizarlo para reducir los largos tiempos de pausa (por ejemplo, creando generaciones) , comienza a tener problemas con las estructuras circulares (cuando los objetos circulares están en diferentes generaciones). Si toma recuentos de referencias y comienza a solucionar el problema de referencia circular, termina introduciendo los largos tiempos de pausa característicos de la marca y el barrido.
Ken Bloom

Si un recolector de basura miraba a Foo y desasignaba su memoria, que en este ejemplo hace referencia a Bar, debería manejar la eliminación de Bar. Por lo tanto, en este punto no es necesario que el recolector de basura continúe y elimine la barra porque ya lo hizo. O viceversa, si elimina Bar que hace referencia a Foo, también debe eliminar Foo y, por lo tanto, no tendrá que eliminar Foo porque lo hizo cuando eliminó Bar. Por favor, corríjame si estoy equivocado.
Chris

1
En el objetivo-c, las referencias circulares hacen que el recuento de referencias no llegue a cero cuando suelte, lo que dispara el recolector de basura.
DexterW

-2

En mi opinión, tener referencias sin restricciones facilita el diseño del programa, pero todos sabemos que algunos lenguajes de programación carecen de soporte para ellos en algunos contextos.

Usted mencionó referencias entre módulos o clases. En ese caso, es algo estático, predefinido por el programador, y es claramente posible que el programador busque una estructura que carece de circularidad, aunque puede que no se adapte al problema de manera limpia.

El verdadero problema viene en la circularidad en las estructuras de datos de tiempo de ejecución, donde algunos problemas en realidad no se pueden definir de una manera que elimine la circularidad. Al final, sin embargo, es el problema el que debe dictar y requerir cualquier otra cosa es forzar al programador a resolver un rompecabezas innecesario.

Yo diría que es un problema con las herramientas, no un problema con el principio.


Agregar una opinión de una oración no contribuye significativamente a la publicación ni explica la respuesta. ¿Podrías dar más detalles sobre esto?

Bueno, dos puntos, el póster en realidad menciona referencias entre módulos o clases. En ese caso, es algo estático, predefinido por el programador, y es claramente posible que el programador busque una estructura que carece de circularidad, aunque puede que no se adapte al problema de manera limpia. El verdadero problema viene en la circularidad en las estructuras de datos de tiempo de ejecución, donde algunos problemas en realidad no se pueden definir de una manera que elimine la circularidad. Al final, sin embargo, es el problema el que debe dictar y requerir cualquier otra cosa es forzar al programador a resolver un rompecabezas innecesario.
Josh S

He descubierto que hace que sea más fácil poner en marcha su programa, pero que, en general, hace que sea más difícil mantener el software, ya que descubre que los cambios triviales tienen efectos en cascada. A realiza llamadas a B, que devuelve las llamadas a A, que devuelve las llamadas a B ... He descubierto que es difícil comprender realmente los efectos de los cambios de esta naturaleza, especialmente cuando A y B son polimórficos.
dash-tom-bang
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.