Quand une référence circulaire à un pointeur parent est-elle acceptable?

24

Cette question Stack Overflow concerne un enfant ayant une référence à son parent, via un pointeur.

Les commentaires étaient assez critiques au départ sur le fait que la conception était une horrible idée.

Je comprends que ce n'est probablement pas la meilleure idée en général. D'un point de vue général, il semble juste de dire "ne faites pas ça!"

Cependant, je me demande quelles sortes de conditions existeraient où vous auriez besoin de faire quelque chose comme ça. Cette question ici et les réponses / commentaires associés suggèrent même que les graphiques ne fassent pas quelque chose comme ça.

enderland
la source
1
La question que vous avez liée semble assez complète sur le sujet.
Courses de légèreté avec Monica le
4
@LightnessRacesinOrbit "ne faites pas cela" n'est pas vraiment utile pour comprendre pourquoi .
enderland
2
J'y vois beaucoup plus que «ne fais pas ça» là-bas. Je vois les avantages et les inconvénients débattus par plusieurs experts.
Courses de légèreté avec Monica le
1
Vous pouvez avoir une liste bidirectionnelle à parcourir, une sorte de tampon circulaire, peut-être que vous représentez deux morceaux de route connectée dans un jeu - si vous devez représenter quelque chose de circulaire, alors cela peut être une bonne idée.
rhughes
2
Ma règle pragmatique est une question "Un enfant peut-il exister sans parent?". (Si vous considérez XmlDocuments et ses nœuds: un nœud ne peut pas exister sans le contexte d'arborescence d'un document. C'est un non-sens). Si la réponse est non, les liens bidirectionnels sont corrects: vous avez deux objets qui ne peuvent exister qu'ensemble. Si des objets peuvent exister indépendamment, je supprime l'un de ces deux liens.
mikalai

Réponses:

43

La clé ici n'est pas de savoir si deux objets ont des références circulaires, mais si ces références indiquent la propriété de l'autre.

Deux objets ne peuvent pas se "posséder": cela provoque un dilemme insoluble pour l'ordre d'initialisation et de suppression. L'un doit être une référence facultative, sinon indiquer qu'un objet ne gérera pas la durée de vie de l'autre.

Considérons une liste doublement liée: deux nœuds se lient l'un à l'autre, mais aucun ne "possède" l'autre (la liste les possède tous les deux). Cela signifie qu'aucun des nœuds n'alloue de mémoire à l'autre ou n'est autrement responsable de la gestion de l'identité ou de la durée de vie de l'autre.

Les arbres ont une relation similaire, bien que les nœuds d'un arbre puissent allouer des enfants et les parents possèdent des enfants. Le lien d'un enfant au parent facilite la traversée, mais là encore, ne définit pas la propriété.

Dans la plupart des conceptions OO, une référence à un autre objet en tant que membre de données d'un objet implique la propriété. Par exemple, supposons que nous ayons des classes Car et Engine. Aucun des deux n'est très utile en soi. On peut dire que ces objets dépendent les uns des autres: ils nécessitent la présence de l'autre pour effectuer un travail utile. Mais qui "possède" l'autre? Dans ce cas, nous dirions que Car possède le moteur, car la voiture est le "conteneur" dans lequel vivent tous les composants automobiles. Dans un design OO et réel, la voiture est la somme de ses parties, et toutes ces parties sont connectées ensemble dans le contexte de la voiture. Le moteur peut avoir une référence à Car, ou il peut avoir une référence à TorqueConverter,

Les références circulaires peuvent être une mauvaise odeur de conception, mais pas nécessairement. Lorsqu'ils sont utilisés judicieusement et correctement documentés, ils peuvent faciliter l'utilisation des structures de données.

Essayez de parcourir un arbre sans références allant dans les deux sens entre les parents et les enfants. Bien sûr, vous pouvez proposer une approche basée sur la pile qui est fragile et complexe, ou vous pouvez utiliser l'approche basée sur les références qui est trivialement simple.


la source
16

Il y a plusieurs aspects à considérer dans une telle conception:

  • les dépendances structurelles
  • la relation de propriété (iecomposition vs autre type d'association)
  • les besoins de navigation

Dépendance structurelle entre les classes:

Si vous visez à réutiliser des classes de composants, vous devez éviter les dépendances inutiles et éviter ces structures circulaires fermées.

Néanmoins, parfois, deux classes sont conceptuellement fortement liées. Dans ce cas, éviter la dépendance n'est pas une vraie option. Exemple: un arbre et ses feuilles, ou plus généralement un composite et ses composants .

Propriété des objets:

Un objet est-il propriétaire de l'autre? Ou autrement dit: si un objet est détruit, l'autre doit-il également être détruit?

Ce sujet a été abordé en profondeur par Snowman, donc je ne vais pas en parler ici.

Besoins de navigation entre les objets:

Un dernier problème est le besoin de navigation. Prenons mon exemple préféré, le motif de conception composite du Gang of four .

Gamma & al. mentionner explicitement le besoin potentiel d'avoir une référence parent explicite: "Le maintien de la référence des composants enfants à leur parent peut simplifier la traversée et la gestion d'une structure composite " Bien sûr, vous pourriez imaginer une traversée systématique de haut en bas, mais pour les très grands objets composites, il peut considérablement ralentir les opérations et de manière exponentielle. Une référence directe, même circulaire peut faciliter considérablement la manipulation de vos composites.

Un exemple pourrait être un modèle graphique d'un système électronique. Une structure composite pourrait représenter les cartes électroniques, les circuits, les éléments. Pour afficher et manipuler le modèle, vous auriez besoin de proxys géométriques dans une vue GUI. Il est alors certainement beaucoup plus facile de naviguer de l'élément GUI sélectionné par l'utilisateur vers le composant, pour savoir quel est le parent et avec quels sont les éléments frère / sœur associés, que de lancer une recherche descendante.

Bien sûr, comme Gamma & al l'a souligné, vous devez vous assurer des invariants de la relation circulaire. Cela peut être délicat, comme l'a montré la question SO à laquelle vous vous référez. Mais c'est parfaitement gérable et d'une manière sûre.

Conclusion

Le besoin de navigation ne doit pas être sous-estimé. Ce n'est pas sans raison que UML l'a explicitement adressé dans la notation de modélisation. Et oui, il existe une situation parfaitement valable où des références circulaires sont nécessaires.

Le seul point est que parfois les gens ont tendance à aller trop vite dans une telle direction. Il vaut donc la peine de considérer tous les 3 aspects impliqués avant de prendre la décision d'y aller ou non.

Christophe
la source
1
Cette réponse est à mon humble avis largement sous-évaluée. Le PO a demandé: "Étant donné qu'il existe déjà une relation parent-enfant, quand est-il acceptable de la mettre en œuvre par une référence circulaire". La structure et la «propriété» (au sens mentionné ici) sont donc déjà claires. Cela signifie que les seuls critères pour ajouter des références d'un côté ou de l'autre sont les questions des "besoins de navigation" et de la "réutilisation indépendante de l'enfant".
Doc Brown
@DocBrown - Vous pouvez toujours mettre une prime sur cette question une fois qu'elle est éligible. :-)
1
@ GlenH7: cela ne donnerait pas plus de votes à cette réponse, seulement la réponse de Snowman (pour laquelle je pense qu'il manque un peu le point de la question).
Doc Brown
1
@DocBrown - Mais le repz, mec, le repz!
... et si vous ajoutez des choix de visibilité comme public-privé-protégé, cela vous donne 9 options différentes :)
mikalai
8

Habituellement, les références circulaires sont une très mauvaise idée car elles signifient des dépendances circulaires. Vous savez probablement déjà pourquoi les dépendances circulaires sont mauvaises, mais pour des raisons d'exhaustivité, la version tl; dr est que chaque fois que les classes A et B dépendent l'une de l'autre, il est impossible de comprendre / corriger / optimiser / etc A ou B sans aussi comprendre / fixer / optimiser / etc l'autre classe en même temps. Ce qui conduit rapidement à des bases de code où vous ne pouvez rien changer sans tout changer.

Cependant, il est possible d'avoir une référence circulaire sans créer de mauvaises dépendances circulaires. Cela fonctionne tant que la référence est strictement facultative au sens fonctionnel. J'entends par là que vous pourriez facilement le retirer des cours et ils fonctionneraient toujours, même s'ils se trouvent ralentir. Le cas d'utilisation principal que je connais pour de telles références circulaires ne créant pas de dépendance est de permettre une traversée rapide des structures de données basées sur les nœuds telles que les listes liées, les arbres et les tas. Par exemple, en principe, toute opération que vous pouvez effectuer sur une liste à double liaison, vous pouvez également effectuer sur une liste à liaison unique, il se trouve qu'il y a juste quelques opérations (comme revenir en arrière dans la liste) qui ont une bien meilleure grande- O avec la version à double liaison.

Ixrec
la source
6

La raison pour laquelle ce n'est généralement pas une bonne idée de le faire, c'est parce que cela viole le principe d'inversion de dépendance . Les gens ont écrit beaucoup à ce sujet de manière beaucoup plus détaillée que je ne peux en parler de manière adéquate dans ce post, mais cela se résume à la rendre difficile à maintenir, car le couplage est si serré. Changer l'une ou l'autre classe nécessite presque toujours un changement dans l'autre, alors que si les dépendances ne pointent que dans un sens, les changements d'un côté de l'interface sont isolés. Si les deux classes pointent vers une interface abstraite, c'est encore mieux.

La seule exception principale est lorsque vous n'avez pas deux classes différentes à différents niveaux d'abstraction, mais deux nœuds de la même classe, comme dans un arbre, une liste doublement liée, etc. Ici, il s'agit plus d'une relation structurelle que d'une relation d'abstraction. Dans un souci d'efficacité algorithmique, les références circulaires sont acceptables et même encouragées dans ce type de cas.

Karl Bielefeldt
la source
5

[...] suggère même aux graphiques de ne pas faire quelque chose comme ça.

Parfois, il vous suffit d'accéder aux éléments de façon ascendante à partir d'une structure de données différente de celle de l'arborescence, tandis que l'arborescence doit accéder aux éléments de manière descendante.

Par exemple, un quadtree peut stocker des éléments dans un logiciel de graphisme vectoriel. Cependant, la sélection de l'utilisateur est stockée dans une liste de sélection distincte de références / pointeurs d'éléments vectoriels. Lorsque l'utilisateur souhaite supprimer cette sélection, nous devons mettre à jour le quadtree, et il pourrait être beaucoup plus efficace de mettre à jour l'arbre de façon ascendante à partir des feuilles plutôt que de haut en bas. Sinon, vous devrez, pour chaque élément, travailler de la racine à la feuille, puis sauvegarder à nouveau.


la source
1
Oui, cet exemple est vraiment approprié. Exactement le genre de choses que j'essayais de décrire dans mon argument sur la navigation dans un composite: le backpointer accélère considérablement la vitesse. Merci pour votre anecdote de performance intéressante et votre diagramme très clair! +1
Christophe
@Christophe J'aime aussi ton exemple! Je ne savais pas si je répondais correctement à la question, car il s'agissait peut-être davantage de "propriété circulaire" que de simples pointeurs arrière qui nous permettaient de parcourir une structure de données vers le haut / vers l'arrière. Mais je répondais principalement à cette dernière partie "graphique" de la question.
2

Doom 3 contient un exemple d'objet enfant avec un pointeur sur un objet parent. Plus précisément, il utilise des listes intrusives . Pour résumer, une liste intrusive est comme une liste liée, sauf que chaque nœud contient un pointeur sur la liste elle-même.

Avantages:

  • Lorsque des objets peuvent exister dans plusieurs listes simultanément, la mémoire des nœuds de liste doit être allouée et désallouée une seule fois.

  • Lorsqu'un objet doit être détruit, vous pouvez facilement le supprimer de toutes les listes dans lesquelles il se trouve sans rechercher linéairement dans chaque liste.

Je pense que c'est un scénario assez spécifique, mais si je comprends votre question, c'est un exemple d'une utilisation acceptable d'un objet enfant contenant un pointeur sur son objet parent.

user2023861
la source