Quel est le problème avec les références circulaires?


160

Aujourd’hui, j’ai pris part à une discussion sur la programmation au cours de laquelle j’ai fait des déclarations qui supposaient essentiellement que les références circulaires (entre modules, classes, peu importe) sont généralement mauvaises. Une fois ma présentation terminée, mon collègue m'a demandé: "Qu'est-ce qui ne va pas avec les références circulaires?"

J'ai de forts sentiments à ce sujet, mais il m'est difficile de verbaliser de manière concise et concrète. Toute explication que je pourrais trouver tend à s'appuyer sur d'autres éléments que je considère également comme axiomes ("ne peut pas être utilisé isolément, donc ne peut pas tester", "comportement inconnu / indéfini lorsque l'état passe aux objets participants", etc. .), mais j'aimerais entendre une raison concise des raisons pour lesquelles les références circulaires sont mauvaises et qui ne prend pas le genre de sauts de foi que mon propre cerveau fait, ayant passé de nombreuses heures au cours des années à les démêler pour comprendre, réparer, et étendre divers bits de code.

Edit: Je ne parle pas de références circulaires homogènes, comme celles d’une liste à double lien ou d’un pointeur sur parent. Cette question concerne en réalité les références circulaires de "portée plus large", comme libA appelant libB qui rappelle à libA. Remplacez 'module' par 'lib' si vous voulez. Merci pour toutes les réponses jusqu'à présent!


La référence circulaire concerne-t-elle les bibliothèques et les fichiers d’en-tête? Dans un flux de travail, le nouveau code ProjectB traitera un fichier généré à partir du code ProjectA hérité. Ce résultat de ProjectA est une nouvelle exigence motivée par ProjectB; ProjectB a un code qui permet de déterminer génériquement quels champs vont où, etc. Le ProjectA hérité pourrait réutiliser le code dans le nouveau ProjectB, et ProjectB aurait tort de ne pas réutiliser le code utilitaire dans ProjectA (par exemple: détection et transcodage de jeux de caractères). enregistrement, validation et transformation des données, etc.).
Luv2code

1
@ Luv2code Cela devient insensé seulement si vous copiez et collez le code entre les projets ou éventuellement lorsque les deux projets sont compilés et liés dans le même code. S'ils partagent de telles ressources, mettez-les dans une bibliothèque.
dash-tom-bang

Réponses:


220

Il y a beaucoup de problèmes avec les références circulaires:

  • Les références de classe circulaires créent un couplage élevé ; les deux classes doivent être recompilées chaque fois que l’ une d’elles est modifiée.

  • Les références d' assemblages circulaires empêchent la liaison statique , car B dépend de A mais A ne peut pas être assemblé tant que B n'est pas complet.

  • Les références d' objets circulaires peuvent bloquer les algorithmes récursifs naïfs (tels que les sérialiseurs, les visiteurs et les jolies imprimantes) avec des débordements de pile. Les algorithmes plus avancés auront une détection de cycle et échoueront simplement avec un message d'erreur / d'exception plus descriptif.

  • Les références d' objets circulaires rendent également l' injection de dépendance impossible , ce qui réduit considérablement la testabilité de votre système.

  • Les objets avec un très grand nombre de références circulaires sont souvent des objets divins . Même s'ils ne le sont pas, ils ont tendance à conduire au code spaghetti .

  • Les références d' entités circulaires (en particulier dans les bases de données, mais également dans les modèles de domaine) empêchent l'utilisation de contraintes de non-nullabilité , pouvant éventuellement conduire à une corruption des données ou au moins à une incohérence.

  • Les références circulaires en général sont simplement déroutantes et augmentent considérablement la charge cognitive lorsqu'on tente de comprendre le fonctionnement d'un programme.

S'il vous plaît, pensez aux enfants; Évitez les références circulaires chaque fois que vous le pouvez.


32
J'apprécie particulièrement le dernier point, "la charge cognitive" est une chose dont je suis très conscient mais que je n'ai jamais eu de terme très concis.
dash-tom-bang

6
Bonne réponse. Ce serait mieux si vous disiez quelque chose à propos des tests. Si les modules A et B dépendent l'un de l'autre, ils doivent être testés ensemble. Cela signifie qu'ils ne sont pas vraiment des modules séparés; ensemble, ils forment un module cassé.
kevin cline

5
L'injection de dépendance n'est pas impossible avec des références circulaires, même avec une DI automatique. Il suffit d’injecter une propriété plutôt qu’en tant que paramètre constructeur.
BlueRaja - Danny Pflughoeft Le

3
@ BlueRaja-DannyPflughoeft: Je considère qu'un anti-motif, comme le font beaucoup d'autres praticiens du DI, car (a) il n'est pas clair qu'une propriété est en réalité une dépendance, et (b) l'objet étant "injecté" ne peut pas facilement garder une trace de ses propres invariants. Pire encore, de nombreux frameworks les plus sophistiqués / populaires tels que Castle Windsor ne peuvent pas donner de messages d'erreur utiles si une dépendance ne peut pas être résolue; vous vous retrouvez avec une référence null ennuyante au lieu d'une explication détaillée de la dépendance dans laquelle le constructeur ne peut pas être résolu. Ce n'est pas parce que vous le pouvez que vous devriez .
Aaronaught

3
Je ne prétendais pas que c'était une bonne pratique, je soulignais simplement que ce n'est pas impossible, comme le prétend la réponse.
BlueRaja - Danny Pflughoeft

22

Une référence circulaire est le double du couplage d'une référence non circulaire.

Si Foo est au courant de l'existence de Bar et que Bar est au courant de Foo, vous avez deux choses à changer (lorsque vient le temps que Foos et Bars ne doivent plus se connaître). Si Foo est au courant de l'existence de Bar, mais qu'un bar n'en a pas connaissance, vous pouvez changer de Foo sans toucher Bar.

Les références cycliques peuvent également causer des problèmes d’amorçage, du moins dans des environnements qui durent longtemps (services déployés, environnements de développement basés sur une image), où Foo dépend du travail de Bar pour se charger, mais également du travail de Foo afin de charge.


17

Lorsque vous associez deux bouts de code, vous avez effectivement un gros morceau de code. La difficulté de conserver un peu de code est au moins le carré de sa taille, voire plus.

Les gens regardent souvent la complexité d'une classe (/ fonction / fichier / etc.) et oublient que vous devriez vraiment considérer la complexité de la plus petite unité séparable (pouvant être encapsulée). Avoir une dépendance circulaire augmente la taille de cette unité, éventuellement de manière invisible (jusqu'à ce que vous commenciez à essayer de modifier le fichier 1 et à vous rendre compte que cela nécessite également des modifications dans les fichiers 2 à 127).


14

Ils peuvent être mauvais non pas en eux-mêmes mais en tant qu’indicateur d’une mauvaise conception possible. Si Foo dépend de Bar et Bar dépend de Foo, il est justifié de se demander pourquoi ils sont deux au lieu d'un FooBar unique.


10

Hmm ... cela dépend de ce que vous entendez par dépendance circulaire, car il existe en fait des dépendances circulaires qui, à mon avis, sont très bénéfiques.

Considérons un DOM XML - il est logique que chaque nœud ait une référence à son parent et que chaque parent ait une liste de ses enfants. La structure est logiquement un arbre, mais du point de vue d'un algorithme de récupération de place ou similaire, la structure est circulaire.


1
ne serait-ce pas un arbre?
Conrad Frix

@Conrad: Je suppose que cela pourrait être considéré comme un arbre, oui. Pourquoi?
Billy ONeal

1
Je ne pense pas aux arbres comme étant circulaires parce que vous pouvez naviguer dans ses enfants et qu'ils se termineront (quelle que soit la référence du parent). À moins qu'un nœud ait un enfant qui était aussi un ancêtre, ce qui en fait un graphe et non un arbre.
Conrad Frix

5
Une référence circulaire serait si l'un des enfants d'un nœud retournait à un ancêtre.
Matt Olenik

Ce n'est pas vraiment une dépendance circulaire (du moins pas d'une manière qui cause des problèmes). Par exemple, imaginons que cette Nodeclasse comporte d'autres références Nodepour les enfants. Comme elle ne fait que se référencer, la classe est complètement autonome et n'est couplée à rien d'autre. --- Avec cet argument, vous pourriez soutenir qu'une fonction récursive est une dépendance circulaire. Il est (à un étirement), mais pas une mauvaise façon.
byxor

9

Est comme le problème du poulet ou de l'oeuf .

Il existe de nombreux cas dans lesquels les références circulaires sont inévitables et utiles, mais cela ne fonctionne pas dans le cas suivant:

Le projet A dépend du projet B et B dépend de A. A doit être compilé pour pouvoir être utilisé dans B, ce qui nécessite que B soit compilé avant A, ce qui nécessite que B soit compilé avant A qui ...


6

Bien que je sois d’accord avec la plupart des commentaires ici, je voudrais plaider un cas spécial pour la référence circulaire "parent" / "enfant".

Une classe a souvent besoin de savoir quelque chose sur sa classe mère ou propriétaire, peut-être son comportement par défaut, le nom du fichier d'où proviennent les données, l'instruction SQL qui a sélectionné la colonne ou l'emplacement d'un fichier journal, etc.

Vous pouvez le faire sans référence circulaire en ayant une classe contenante de sorte que ce qui était auparavant le "parent" soit maintenant un frère, mais il n'est pas toujours possible de re-factoriser le code existant pour le faire.

L’autre solution consiste à transmettre toutes les données dont un enfant peut avoir besoin dans son constructeur, qui sont tout simplement horribles.


Sur une note connexe, il y a deux raisons courantes pour lesquelles X peut retenir une référence à Y: X peut vouloir demander à Y de faire des choses au nom de X, ou Y peut s'attendre à ce que X fasse des choses à Y, au nom de Y. Si les seules références existantes à Y concernent des objets souhaitant faire quelque chose pour le compte de Y, les détenteurs de ces références doivent être informés du fait que les services de Y ne sont plus nécessaires et qu'ils doivent abandonner leurs références à Y leur commodité.
Supercat

5

En termes de base de données, les références circulaires avec les relations PK / FK appropriées empêchent l’insertion ou la suppression de données. Si vous ne pouvez pas supprimer de la table a sauf si l'enregistrement est supprimé de la table b et si vous ne pouvez pas supprimer de la table b sauf si l'enregistrement est supprimé de la table A, vous ne pouvez pas supprimer. Même avec des inserts. C'est pourquoi de nombreuses bases de données ne vous permettent pas de configurer des mises à jour en cascade ou des suppressions s'il existe une référence circulaire car, à un moment donné, cela devient impossible. Oui, vous pouvez configurer ce type de relations sans que le PK / Fk soit officiellement déclaré, mais vous aurez alors (100% du temps, selon mon expérience) des problèmes d’intégrité des données. C'est juste une mauvaise conception.


4

Je vais prendre cette question du point de vue de modélisation.

Tant que vous n'ajoutez pas de relations qui n'y sont pas, vous êtes en sécurité. Si vous les ajoutez, vous obtenez moins d'intégrité dans les données (car il y a une redondance) et un code plus étroitement couplé.

Le problème avec les références circulaires, en particulier, est que je n’ai pas vu de cas où elles seraient réellement nécessaires, sauf une - référence personnelle. Si vous modélisez des arbres ou des graphiques, vous en avez besoin et tout va bien, car la référence à soi est sans danger du point de vue de la qualité du code (aucune dépendance ajoutée).

Je pense qu'au moment où vous commencez à avoir besoin d'une référence non auto-référente, vous devez immédiatement demander si vous ne pouvez pas le modéliser sous forme de graphique (réduire les entités multiples en un seul nœud). Peut-être existe-t-il un cas entre deux références où vous faites une référence circulaire, mais la modélisation sous forme de graphique n'est pas appropriée, mais j'en doute fortement.

Les gens risquent de penser qu’ils ont besoin d’une référence circulaire, mais ce n’est pas le cas. Le cas le plus commun est "Le cas un-de-plusieurs". Par exemple, vous avez un client avec plusieurs adresses parmi lesquelles une doit être marquée comme adresse principale. Il est très tentant de modéliser cette situation sous la forme de deux relations distinctes has_address et is_primary_address_of mais ce n'est pas correct. La raison en est qu'être l'adresse principale n'est pas une relation séparée entre les utilisateurs et les adresses, mais bien un attribut de la relation à l' adresse. Pourquoi donc? Parce que son domaine est limité aux adresses de l'utilisateur et non à toutes les adresses existantes. Vous choisissez l'un des liens et le marquez comme le plus fort (primaire).

(Parlons maintenant des bases de données) Beaucoup de gens optent pour la solution à deux relations parce qu'ils comprennent que «primaire» est un pointeur unique et qu'une clé étrangère est en quelque sorte un pointeur. Donc, la clé étrangère devrait être la chose à utiliser, non? Faux. Les clés étrangères représentent des relations mais "primaire" n'est pas une relation. C'est un cas dégénéré de commande où un élément est avant tout et le reste n'est pas commandé. Si vous aviez besoin de modéliser un ordre total, vous le considéreriez bien sûr comme un attribut de relation, car il n'y a pas d'autre choix. Mais au moment où vous le dégénérez, il existe un choix et un choix horrible: modéliser quelque chose qui n'est pas une relation en tant que relation. Alors voilà, la redondance des relations n’est certainement pas à sous-estimer.

Donc, je ne laisserais pas une référence circulaire se produire à moins qu'il soit absolument clair que cela vient de la chose que je modélise.

(note: ceci est légèrement biaisé par la conception de la base de données mais je parierais que c'est assez applicable à d'autres domaines aussi)


2

Je répondrais à cette question par une autre question:

Quelle situation pouvez-vous me donner où le maintien d'un modèle de référence circulaire est le meilleur modèle pour ce que vous essayez de construire?

D'après mon expérience, le meilleur modèle n'impliquera pratiquement jamais de références circulaires, comme je le pense. Cela étant dit, il existe de nombreux modèles dans lesquels vous utilisez des références circulaires à tout moment, c’est extrêmement simple. Relations parent -> enfant, tout modèle graphique, etc., mais ce sont des modèles bien connus et je pense que vous faites allusion à autre chose.


1
Il se peut qu'une liste chaînée circulaire (simple ou double) soit une excellente structure de données pour la file d'attente d'événements centrale d'un programme supposé "ne jamais s'arrêter" (coller les N éléments importants dans la file d'attente, avec un Indicateur "ne pas supprimer", puis traverser simplement la file jusqu'à ce qu'il soit vide; quand de nouvelles tâches (transitoires ou permanentes) sont nécessaires, collez-les à un endroit approprié de la file d'attente; chaque fois que vous signalez un événement sans le drapeau "ne pas supprimer" , faites-le puis retirez-le de la file d'attente).
Vatine

1

Les références circulaires dans les structures de données constituent parfois le moyen naturel d’exprimer un modèle de données. En ce qui concerne le codage, ce n’est certainement pas l’idéal et peut être résolu (dans une certaine mesure) par l’injection de dépendances, faisant passer le problème du code aux données.


1

Une construction de référence circulaire pose problème, non seulement du point de vue de la conception, mais également de la détection des erreurs.

Envisagez la possibilité d’une défaillance du code. Vous n'avez pas placé d'erreur appropriée dans les deux classes, soit parce que vous n'avez pas encore développé vos méthodes, soit parce que vous êtes paresseux. Dans les deux cas, vous n'avez pas de message d'erreur vous indiquant ce qui s'est passé et vous devez le déboguer. En tant que bon concepteur de programme, vous savez quelles méthodes sont associées à quels processus. Vous pouvez donc les limiter aux méthodes pertinentes pour le processus à l'origine de l'erreur.

Avec des références circulaires, vos problèmes ont maintenant doublé. Comme vos processus sont étroitement liés, vous n’avez aucun moyen de savoir quelle méthode dans quelle classe pourrait avoir causé l’erreur, ni d’où vient l’erreur, car une classe dépend de l’autre dépend de l’autre. Vous devez maintenant passer du temps à tester les deux classes en même temps pour déterminer lequel est réellement responsable de l'erreur.

Bien sûr, la détection correcte des erreurs résout ce problème, mais uniquement si vous savez quand une erreur risque de se produire. Et si vous utilisez des messages d'erreur génériques, vous n'êtes toujours pas mieux loti.


1

Certains éboueurs ont du mal à les nettoyer, car chaque objet est référencé par un autre.

EDIT: Comme indiqué dans les commentaires ci-dessous, cela n’est vrai que pour une tentative extrêmement naïve de dépoussiéreur, qui n’est pas celle que vous rencontreriez dans la pratique.


11
Hmm .. n'importe quel éboueur déclenché par ceci n'est pas un vrai éboueur.
Billy ONeal

11
Je ne connais aucun éboueur moderne qui aurait des problèmes avec les références circulaires. Les références circulaires posent un problème si vous utilisez des comptages de références, mais la plupart des éboueurs utilisent un style de traçage (vous commencez avec la liste des références connues et suivez-les pour rechercher toutes les autres, en collectant tout le reste).
Dean Harding

4
Voir sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf qui explique les inconvénients de divers types de ramasseurs de déchets - s'il faut marquer et balayer et commencer à l'optimiser pour réduire les longs temps de pause (par exemple, créer des générations) , vous commencez à avoir des problèmes avec les structures circulaires (lorsque les objets circulaires appartiennent à des générations différentes). Si vous prenez des comptes de référence et commencez à résoudre le problème de la référence circulaire, vous finissez par introduire les temps de pause longs qui sont caractéristiques de la marque et du balayage.
Ken Bloom

Si un éboueur regarde Foo et libère sa mémoire qui, dans cet exemple, fait référence à Bar, il doit gérer la suppression de Bar. Ainsi, à ce stade, il n’est pas nécessaire que le ramasse-miettes continue et supprime la barre car il l’a déjà fait. Ou inversement, si elle supprime Bar avec les références Foo, doit-elle également supprimer Foo et n'aura donc pas besoin d'enlever Foo, car elle l'a fait lorsqu'elle a supprimé Bar? S'il vous plait corrigez moi si je me trompe.
Chris

1
Dans Objective-c, les références circulaires font en sorte que le nombre de références ne soit pas égal à zéro lorsque vous relâchez, ce qui déclenche le ramasse-miettes.
DexterW

-2

À mon avis, disposer de références illimitées facilite la conception de programmes, mais nous savons tous que certains langages de programmation ne les prennent pas en charge dans certains contextes.

Vous avez mentionné des références entre modules ou classes. Dans ce cas, c'est une chose statique, prédéfinie par le programmeur, et il est clairement possible pour le programmeur de rechercher une structure dépourvue de circularité, même si cela ne convient pas nécessairement au problème.

Le problème réel vient de la circularité dans les structures de données d'exécution, où certains problèmes ne peuvent en réalité pas être définis de manière à éliminer la circularité. En fin de compte, c’est le problème qui devrait dicter et imposer quoi que ce soit d’obliger le programmeur à résoudre un casse-tête inutile.

Je dirais que c'est un problème avec les outils, pas un problème avec le principe.


Ajouter une opinion d'une phrase ne contribue pas de manière significative à la publication et n'explique pas la réponse. Pourriez-vous élaborer sur ceci?

En fait, l’affiche mentionne des références entre modules ou classes. Dans ce cas, c'est une chose statique, prédéfinie par le programmeur, et il est clairement possible pour le programmeur de rechercher une structure dépourvue de circularité, même si cela ne convient pas nécessairement au problème. Le problème réel vient de la circularité dans les structures de données d'exécution, où certains problèmes ne peuvent en réalité pas être définis de manière à éliminer la circularité. En fin de compte, c’est le problème qui devrait dicter et imposer quoi que ce soit d’obliger le programmeur à résoudre un casse-tête inutile.
Josh S

J'ai constaté que cela rend plus facile la mise en route de votre programme mais qu'en règle générale, il est plus difficile de maintenir le logiciel, car vous constaterez que les modifications mineures ont des effets en cascade. A effectue des appels dans B qui effectue des appels vers A qui effectue des appels vers B ... Il m'est difficile de vraiment comprendre les effets de changements de cette nature, en particulier lorsque A et B sont polymorphes.
dash-tom-bang
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.