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!
fuente
Respuestas:
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.
fuente
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.
fuente
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).
fuente
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.
fuente
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.
fuente
Node
es una clase, que tiene otras referenciasNode
para 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.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 ...
fuente
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.
fuente
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.
fuente
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)
fuente
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.
fuente
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.
fuente
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.
fuente
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.
fuente
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.
fuente