¿Cuándo es aceptable una referencia circular a un puntero principal?

24

Esta pregunta de desbordamiento de pila trata sobre un niño que hace referencia a su padre, a través de un puntero.

Los comentarios fueron bastante críticos inicialmente de que el diseño era una idea horrible.

Entiendo que probablemente esta no sea la mejor idea en general. Según una regla general, parece justo decir: "¡no hagas esto!"

Sin embargo, me pregunto qué tipo de condiciones existirían donde tendrías que hacer algo como esto. Esta pregunta aquí y las respuestas / comentarios asociados sugieren incluso que los gráficos no hagan algo como esto.

Enderland
fuente
1
La pregunta que vinculó parece bastante completa sobre el tema.
Lightness compite con Monica el
44
@LightnessRacesinOrbit "no hagas esto" no es realmente útil en cuanto a entender por qué .
enderland
2
Veo mucho más que "no hagas esto" allí. Veo pros y contras debatidos por múltiples expertos.
Lightness compite con Monica el
1
Es posible que tenga una lista bidireccional que deba atravesarse, algún tipo de búfer circular, tal vez esté representando dos piezas de carretera conectadas en un juego; si necesita representar algo circular, puede ser una buena idea.
Rhughes
2
Mi regla general pragmática es una pregunta "¿Puede existir un niño sin un padre?". (Si considera XmlDocuments y sus nodos: un nodo no puede existir sin el contexto de árbol de un documento. Es una tontería). Si la respuesta es no, entonces los enlaces bidireccionales están bien: tiene dos objetos que solo pueden existir juntos. Si los objetos pueden existir independientemente, entonces elimino uno de esos dos enlaces.
mikalai

Respuestas:

43

La clave aquí no es si dos objetos tienen referencias circulares, sino si esas referencias indican la propiedad del otro.

Dos objetos no pueden "ser dueños" entre sí: esto provoca un dilema intratable para el orden de inicialización y eliminación. Uno debe ser una referencia opcional, o indicar de otro modo que un objeto no administrará la vida útil del otro.

Considere una lista doblemente vinculada: dos nodos se vinculan entre sí, pero ninguno "posee" al otro (la lista los posee a ambos). Esto significa que ninguno de los nodos asigna memoria para el otro o es responsable de la identidad o la gestión de por vida del otro.

Los árboles tienen una relación similar, aunque los nodos en un árbol pueden asignar niños y los padres tienen hijos propios. El enlace de un niño a un padre ayuda con el recorrido, pero de nuevo no define la propiedad.

En la mayoría de los diseños OO, una referencia a otro objeto como miembro de datos de un objeto implica propiedad. Por ejemplo, supongamos que tenemos clases Car y Engine. Ninguno de los dos es muy útil por sí solo. Podemos decir que estos objetos dependen unos de otros: requieren la presencia del otro para realizar un trabajo útil. ¿Pero cuál "posee" el otro? En este caso, diríamos que Car posee Motor porque el automóvil es el "contenedor" en el que viven todos los componentes del automóvil. Tanto en un diseño OO como en el mundo real, el automóvil es la suma de sus partes, y todas esas partes están conectadas entre sí dentro del contexto del automóvil. El motor puede tener una referencia de regreso a Car, o puede tener una referencia a TorqueConverter,

Las referencias circulares pueden ser un mal olor de diseño, pero no necesariamente. Cuando se usan juiciosamente y se documentan correctamente, pueden facilitar el uso de estructuras de datos.

Intente atravesar un árbol sin referencias que vayan en ambos sentidos entre padres e hijos. Claro, podría llegar a un enfoque basado en la pila que sea frágil y complejo, o podría usar el enfoque basado en referencias que es trivialmente simple.


fuente
16

Hay varios aspectos a considerar en dicho diseño:

  • las dependencias estructurales
  • la relación de propiedad (es decir, composición versus otro tipo de asociación)
  • la navegación necesita

Dependencia estructural entre clases:

Si su objetivo es reutilizar las clases de componentes, debe evitar la dependencia innecesaria y evitar tales estructuras circulares cerradas.

Sin embargo, a veces dos clases están conceptualmente fuertemente interconectadas. En este caso, evitar la dependencia no es una opción real. Ejemplo: un árbol y sus hojas, o más generalmente un compuesto y sus componentes .

Propiedad de los objetos:

¿Un objeto posee el otro? O dicho de otra manera: si un objeto es destruido, ¿el otro también será destruido?

Este tema fue abordado en profundidad por Snowman, por lo que no voy a abordarlo aquí.

Necesidades de navegación entre objetos:

Un último problema es la necesidad de navegación. Tomemos mi ejemplo favorito, el patrón de diseño compuesto de la Banda de cuatro .

Gamma y col. Mencione explícitamente la posible necesidad de tener una referencia primaria explícita: " Mantener la referencia de los componentes secundarios a sus padres puede simplificar el recorrido y la gestión de una estructura compuesta ". Por supuesto, puede imaginar un recorrido sistemático de arriba hacia abajo, pero para objetos compuestos muy grandes puede ralentizar significativamente las operaciones y de manera exponencial. Una referencia directa, incluso circular, puede facilitar significativamente la manipulación de sus compuestos.

Un ejemplo podría ser un modelo gráfico de un sistema electrónico. Una estructura compuesta podría representar los tableros electrónicos, circuitos, elementos. Para mostrar y manipular el modelo, necesitaría algunos proxies geométricos en una vista GUI. Sin duda, es mucho más fácil navegar desde el elemento GUI seleccionado por el usuario hasta el componente, para averiguar cuál es el elemento principal y con qué elementos relacionados hermano / hermana, que iniciar una búsqueda de arriba hacia abajo.

Por supuesto, como señalaron Gamma y otros, debe asegurarse de los invariantes de la relación circular. Esto puede ser complicado, como lo ha demostrado la pregunta SO a la que se refiere. Pero es perfectamente manejable y de manera segura.

Conclusión

La necesidad de navegación no debe ser subestimada. No sin razón UML lo ha abordado explícitamente en la notación de modelado. Y sí, hay una situación perfectamente válida donde se necesitan referencias circulares.

El único punto es que a veces las personas tienden a ir en esa dirección rápidamente. Por lo tanto, vale la pena considerar los 3 aspectos involucrados antes de tomar la decisión de hacerlo o no.

Christophe
fuente
1
Esta respuesta es, en mi humilde opinión, muy poco votada. El OP preguntó: "Dado que ya existe una relación padre-hijo, ¿cuándo está bien implementar esto por una referencia circular". Entonces, la estructura y la "propiedad" (en el sentido mencionado aquí) ya están claras. Eso significa que el único criterio para agregar referencias en un lado u otro son las preguntas de "necesidades de navegación" y "reutilización independiente del niño".
Doc Brown
@DocBrown: siempre puedes recompensar esta pregunta una vez que sea elegible. :-)
1
@ GlenH7: eso no le daría a esta respuesta más votos, solo la respuesta de Snowman (para lo cual creo que extraña un poco el punto de la pregunta).
Doc Brown
1
@DocBrown - ¡Pero el repz, hombre, el repz!
... y si agrega opciones de visibilidad como público-privado-protegido, eso le da 9 opciones diferentes :)
mikalai
8

Por lo general, las referencias circulares son una muy mala idea porque significan dependencias circulares. Probablemente ya sepa por qué las dependencias circulares son malas, pero para completar, la versión tl; dr es que cada vez que las clases A y B dependen una de la otra, es imposible entender / corregir / optimizar / etc. A o B sin también entendiendo / arreglando / optimizando / etc la otra clase al mismo tiempo. Lo que rápidamente lleva a bases de código donde no puedes cambiar nada sin cambiarlo todo.

Sin embargo, es posible tener una referencia circular sin crear dependencias circulares malvadas. Esto funciona siempre que la referencia sea estrictamente opcional en un sentido funcional. Con eso quiero decir que podrías eliminarlo fácilmente de las clases y seguirían funcionando, incluso si funcionan más lentamente. El principal caso de uso que conozco para tales referencias circulares que no crean dependencia es permitir el recorrido rápido de estructuras de datos basadas en nodos, como listas vinculadas, árboles y montones. Por ejemplo, en principio, cualquier operación que pueda realizar en una lista doblemente enlazada que también puede realizar en una lista individualmente vinculada, solo hay algunas operaciones (como retroceder a través de la lista) que tienen mucho mejor O con la versión doblemente vinculada.

Ixrec
fuente
6

La razón por la que generalmente no es una buena idea hacer esto es porque viola el Principio de Inversión de Dependencia . La gente ha escrito mucho sobre esto con mucho más detalle de lo que puedo cubrir adecuadamente en esta publicación, pero se reduce a hacer que sea difícil de mantener, porque el acoplamiento es muy apretado. Cambiar cualquiera de las clases casi siempre requiere un cambio en la otra, mientras que si las dependencias solo apuntan en una dirección, los cambios en un lado de la interfaz están aislados. Si ambas clases apuntan a una interfaz abstracta, aún mejor.

La única excepción principal es cuando no tiene dos clases diferentes en diferentes niveles de abstracción, sino dos nodos de la misma clase, como en un árbol, una lista doblemente vinculada, etc. Aquí es más una relación estructural que una Relación de abstracción. Las referencias circulares en aras de la eficiencia algorítmica son aceptables e incluso alentadas en este tipo de casos.

Karl Bielefeldt
fuente
5

[...] sugiere incluso que los gráficos no hagan algo como esto.

A veces solo necesita acceder a las cosas de abajo hacia arriba desde una estructura de datos diferente que el árbol, mientras que el árbol necesita acceder a las cosas de arriba hacia abajo.

Como ejemplo, un quadtree podría almacenar elementos en un software de gráficos vectoriales. Sin embargo, la selección del usuario se almacena en una lista de selección separada de referencias / punteros de elementos vectoriales. Cuando el usuario quiere eliminar esa selección, tenemos que actualizar el árbol cuádruple, y allí podría ser mucho más eficiente actualizar el árbol de abajo hacia arriba comenzando desde las hojas en lugar de arriba hacia abajo. De lo contrario, tendría que, para cada elemento, trabajar desde la raíz hasta la hoja y luego volver a realizar una copia de seguridad.


fuente
1
Sí, este ejemplo es realmente apropiado. Exactamente el tipo de cosas que estaba tratando de describir en mi argumento sobre la navegación en un compuesto: el puntero de fondo acelera considerablemente la velocidad. ¡Gracias por su interesante anécdota de rendimiento y diagrama muy claro! +1
Christophe
@Christophe ¡También me gusta tu ejemplo! No estaba seguro de si realmente estaba respondiendo la pregunta correctamente, ya que tal vez se trataba más de "propiedad circular" que de simples punteros que nos permitían atravesar una estructura de datos hacia arriba / hacia atrás. Pero estaba respondiendo principalmente a esa última parte "gráfica" de la pregunta.
2

Doom 3 tiene un ejemplo de un objeto hijo con un puntero a un objeto padre. Específicamente utiliza listas intrusivas . Para resumir, una lista intrusiva es como una lista vinculada, excepto que cada nodo contiene un puntero a la lista misma.

Ventajas:

  • Cuando los objetos pueden existir en varias listas simultáneamente, la memoria para los nodos de la lista debe asignarse y desasignarse solo una vez.

  • Cuando un objeto necesita ser destruido, puede eliminarlo fácilmente de todas las listas en las que se encuentra sin buscar en cada lista linealmente.

Creo que este es un escenario bastante específico, pero si entiendo su pregunta, es un ejemplo de un uso aceptable de un objeto secundario que contiene un puntero a su objeto principal.

usuario2023861
fuente