J'ai participé à une discussion de programmation aujourd'hui où j'ai fait quelques déclarations qui supposaient essentiellement axiomatiquement que les références circulaires (entre modules, classes, peu importe) sont généralement mauvaises. Une fois que j'ai fini mon pitch, mon collègue a demandé: "Qu'est-ce qui ne va pas avec les références circulaires?"
J'ai des sentiments forts à ce sujet, mais il m'est difficile de verbaliser de manière concise et concrète. Toute explication que je pourrais trouver a tendance à s'appuyer sur d'autres éléments que je considère moi aussi comme des axiomes ("ne peut pas utiliser isolément, donc ne peut pas tester", "comportement inconnu/indéfini car l'état change dans les objets participants", etc. .), mais j'aimerais entendre une raison concise pour laquelle les références circulaires sont mauvaises et ne prennent pas le genre de sauts de foi que fait mon propre cerveau, après avoir passé de nombreuses heures au fil des ans à les démêler pour comprendre, réparer, et étendre divers bits de code.
Edit: Je ne pose pas de questions sur les références circulaires homogènes, comme celles d'une liste à double liaison ou d'un pointeur vers le parent. Cette question pose vraiment sur les références circulaires "de plus grande portée", comme libA appelant libB qui rappelle à libA. Remplacez "module" par "lib" si vous le souhaitez. Merci pour toutes les réponses jusqu'à présent!
Il y a beaucoup de choses beaucoup mal avec les références circulaires:
Les références circulaires de classe créent un couplage élevé ; les deux les classes doivent être recompilées à chaque fois que soit est changé.
Les références circulaires d'assemblage empêchent la liaison statique , car B dépend de A mais A ne peut pas être assemblé tant que B n'est pas terminé.
Les références d'objets circulaires peuvent planter des algorithmes récursifs naïfs (comme les sérialiseurs, les visiteurs et jolies imprimantes) avec débordements de pile. Les algorithmes les plus avancés auront une détection de cycle et échoueront simplement avec un message d'exception/d'erreur plus descriptif.
Les références circulaires aux objets rendent également l'injection de dépendance impossible , ce qui réduit considérablement le testabilité de votre système.
Les objets avec un très grand nombre de références circulaires sont souvent God Objects . Même s'ils ne le sont pas, ils ont tendance à conduire à Spaghetti Code .
Les références circulaires d'entité (en particulier dans les bases de données, mais aussi dans les modèles de domaine) empêchent l'utilisation de contraintes de non-nullité , ce qui peut é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 lorsque essayer de comprendre comment fonctionne un programme.
S'il vous plaît, pensez aux enfants; évitez les références circulaires chaque fois que vous le pouvez.
Une référence circulaire est le double du couplage d'une référence non circulaire.
Si Foo connaît Bar et Bar connaît Foo, vous avez deux choses à changer (lorsque l'exigence vient que Foos et Bars ne doivent plus se connaître). Si Foo connaît Bar, mais qu'un bar ne connaît pas Foo, vous pouvez changer Foo sans toucher Bar.
Les références cycliques peuvent également provoquer des problèmes d'amorçage, au moins dans des environnements qui durent longtemps (services déployés, environnements de développement basés sur des images), où Foo dépend du fonctionnement de Bar pour se charger, mais Bar dépend également du fonctionnement de Foo pour charge.
Lorsque vous liez deux bits de code ensemble, vous avez effectivement un gros morceau de code. La difficulté de conserver un peu de code est au moins le carré de sa taille, et peut-être plus élevée.
Les gens regardent souvent la complexité d'une seule classe (/ function/file/etc.) Et oublient que vous devriez vraiment considérer la complexité de la plus petite unité séparable (encapsulable). 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 que vous réalisiez que cela nécessite également des modifications dans les fichiers 2-127).
Ils peuvent être mauvais non pas par eux-mêmes mais comme indicateur d'une éventuelle mauvaise conception. 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.
Hmm ... cela dépend de ce que vous entendez par dépendance circulaire, car il y a en fait des dépendances circulaires qui je pense sont très bénéfiques.
Considérez 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 collecte des ordures ou similaire, la structure est circulaire.
C'est comme le problème poulet ou œuf .
Il existe de nombreux cas dans lesquels la référence circulaire est inévitable et utile, mais, par exemple, dans le cas suivant, cela ne fonctionne pas:
Le projet A dépend du projet B et B dépend de A. A doit être compilé pour être utilisé dans B qui nécessite que B soit compilé avant A qui nécessite que B soit compilé avant A qui ...
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 parent 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 conteneur de sorte que ce qui était auparavant le "parent" est maintenant un frère, mais il n'est pas toujours possible de re-factoriser le code existant pour ce faire.
L'autre alternative est de transmettre toutes les données dont un enfant pourrait avoir besoin dans son constructeur, qui finissent par être tout simplement horribles.
En termes de base de données, des références circulaires avec des relations PK/FK appropriées rendent impossible l'insertion ou la suppression de données. Si vous ne pouvez pas supprimer du tableau a sauf si l'enregistrement a disparu du tableau b et que vous ne pouvez pas supprimer du tableau b sauf si l'enregistrement est parti du tableau A, vous ne pouvez pas supprimer. Idem avec 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 établir ce type de relations sans que le PK/Fk ne soit officiellement déclaré, mais vous aurez (100% du temps selon mon expérience) des problèmes d'intégrité des données. C'est juste une mauvaise conception.
Je vais prendre cette question du point de vue de la modélisation.
Tant que vous n'ajoutez aucune relation qui n'existe pas réellement, 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, c'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 l'auto-référence est inoffensive du point de vue de la qualité du code (aucune dépendance ajoutée).
Je crois qu'au moment où vous commencez à avoir besoin d'une référence non-auto, vous devez immédiatement demander si vous ne pouvez pas la modéliser sous forme de graphique (réduire les multiples entités en un seul nœud). Peut-être qu'il y a un cas entre les deux où vous faites une référence circulaire mais la modéliser sous forme de graphique n'est pas approprié, mais j'en doute fortement.
Il y a un danger que les gens pensent qu'ils ont besoin d'une référence circulaire mais en fait ils n'en ont pas. Le cas le plus courant est "le cas unique". Par exemple, vous avez un client avec plusieurs adresses dont l'une doit être marquée comme adresse principale. Il est très tentant de modéliser cette situation comme deux relations distinctes has_address et is_primary_address_of mais ce n'est pas correct. La raison en est que étant l'adresse principale n'est pas une relation distincte entre les utilisateurs et les adresses mais plutôt n attribut du relation a une adresse . Pourquoi donc? Parce que son domaine est limité aux adresses de l'utilisateur et non à toutes les adresses qui existent. Vous choisissez l'un des liens et le marquez comme le plus fort (principal).
(Je vais parler des bases de données maintenant) 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 une sorte de 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 ordonné. 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 fondamentalement pas d'autre choix. Mais au moment où vous le dégénérez, il y a un choix et tout à fait horrible - de modéliser quelque chose qui n'est pas une relation comme une relation. Voilà donc la redondance des relations qui n'est certainement pas quelque chose à sous-estimer. L'exigence d'unicité devrait être imposée d'une autre manière, par exemple par des index partiels uniques.
Donc, je ne permettrais pas qu'une référence circulaire se produise à moins qu'il ne soit absolument clair qu'elle provient de la chose que je modélise.
(Remarque: cela est légèrement biaisé par rapport à la conception de la base de données, mais je parierais que cela s'applique également à d'autres domaines)
Je répondrais à cette question par une autre question:
Quelle situation pouvez-vous me donner où conserver un modèle de référence circulaire est le modèle meilleur pour ce que vous essayez de construire?
D'après mon expérience, le meilleur modèle n'impliquera pratiquement pas de références circulaires comme je pense que vous le pensez. Cela étant dit, il existe de nombreux modèles où vous utilisez tout le temps des références circulaires, c'est tout simplement extrêmement basique. Parent -> Relations avec les enfants, n'importe quel modèle de graphique, etc., mais ce sont des modèles bien connus et je pense que vous faites référence à autre chose.
Certains ramasseurs de déchets ont du mal à les nettoyer, car chaque objet est référencé par un autre.
EDIT: Comme indiqué par les commentaires ci-dessous, cela n'est vrai que pour une extrêmement tentative naïve de ramasse-miettes, pas une que vous rencontriez dans la pratique.
Une construction de référence circulaire est problématique, non seulement du point de vue de la conception, mais également du point de vue de la capture d'erreurs.
Considérez la possibilité d'une défaillance du code. Vous n'avez placé la capture d'erreur appropriée dans aucune des classes, soit parce que vous n'avez pas encore développé vos méthodes, soit parce que vous êtes paresseux. Quoi qu'il en soit, vous n'avez pas de message d'erreur pour vous dire ce qui s'est passé et vous devez le déboguer. En tant que bon concepteur de programmes, vous savez quelles méthodes sont liées à quels processus, vous pouvez donc les limiter aux méthodes pertinentes pour le processus qui a provoqué l'erreur.
Avec des références circulaires, vos problèmes ont maintenant doublé. Parce que vos processus sont étroitement liés, vous n'avez aucun moyen de savoir quelle méthode dans quelle classe a pu causer l'erreur, ou d'où l'erreur est venue, car une classe dépend de l'autre dépend de l'autre. Vous devez maintenant passer du temps à tester les deux classes conjointement pour savoir laquelle est vraiment responsable de l'erreur.
Bien sûr, une capture d'erreur appropriée résout ce problème, mais uniquement si vous savez quand une erreur est susceptible de se produire. Et si vous utilisez des messages d'erreur génériques, vous n'êtes pas beaucoup mieux.
Les références circulaires dans les structures de données sont parfois le moyen naturel d'exprimer un modèle de données. En ce qui concerne le codage, ce n'est certainement pas idéal et peut être (dans une certaine mesure) résolu par l'injection de dépendances, poussant le problème du code aux données.