Dépendance de classe circulaire

12

Est-ce une mauvaise conception d'avoir 2 classes qui ont besoin l'une de l'autre?

J'écris un petit jeu dans lequel j'ai une GameEngineclasse qui a quelques GameStateobjets. Pour accéder à plusieurs méthodes de rendu, ces GameStateobjets doivent également connaître la GameEngineclasse - c'est donc une dépendance circulaire.

Pourriez-vous appeler cette mauvaise conception? Je demande simplement, parce que je ne suis pas tout à fait sûr et à ce moment je suis encore en mesure de refactoriser ces choses.

shad0w
la source
2
Je serais très surpris si la réponse était oui.
doppelgreener
Puisqu'il y a deux questions, qui sont logiquement disjointes .. la réponse est oui à l'une d'entre elles. Pourquoi voulez-vous concevoir un état qui fait réellement quelque chose? vous devriez avoir un gestionnaire / contrôleur pour vérifier l'état et effectuer l'action. Si vous voulez le faire, C ++ est votre ami .
teodron
Mes classes GameState sont des choses comme l'intro, le jeu réel, un menu, etc. Le GameEngine stocke ces états dans une pile, donc je peux mettre en pause un état et ouvrir un menu, ou jouer une cinématique, ... Les classes GameState ont besoin de connaître le GameEngine, car le moteur crée la fenêtre et a le contexte de rendu .
shad0w
5
N'oubliez pas que toutes ces règles visent rapidement. Rapide à exécuter, rapide à créer, rapide à entretenir. Parfois, ces facettes sont en désaccord les unes avec les autres et vous devez effectuer une analyse coûts-avantages pour décider comment procéder. Si vous y pensez encore, et faites un appel à l'intestin, vous faites toujours mieux que 90% des développeurs. Il n'y a pas de monstre caché qui va vous tuer si vous le faites mal, il est plus un opérateur de péage caché.
DampeS8N

Réponses:

0

Ce n'est pas une mauvaise conception par nature, mais cela peut facilement devenir hors de contrôle. En fait, vous utilisez ce design dans vos codes de tous les jours. Par exemple, vector sait que c'est le premier itérateur et les itérateurs ont un pointeur sur leur conteneur.

Maintenant, dans votre cas, il est beaucoup mieux d'avoir deux classes distinctes pour GameEnigne et GameState. Puisque fondamentalement, ces deux-là font des choses différentes, et plus tard, vous pourriez définir de nombreuses classes qui héritent de GameState (comme un GameState pour chaque scène de votre jeu). Et personne ne peut nier leur besoin d'avoir accès les uns aux autres. Fondamentalement, GameEngine exécute des gamestates, il devrait donc avoir un pointeur vers eux. Et GameState utilise des ressources définies dans GameEngine (comme le rendu, le gestionnaire physique, etc.).

Vous ne pouvez pas et ne devez pas combiner ces deux classes l'une avec l'autre car elles font des choses différentes par nature et la combinaison se traduira par une très grande classe que personne n'aime.

Jusqu'à présent, nous savons que nous avons besoin d'une dépendance circulaire dans notre conception. Il existe plusieurs façons de créer cela en toute sécurité:

  1. Comme vous l'avez suggéré, nous pouvons mettre un pointeur de chacun d'eux dans un autre, c'est la solution la plus simple mais pourrait facilement devenir hors de contrôle.
  2. Vous pouvez également utiliser la conception singleton / multiton, avec cette méthode, vous devez définir votre classe GameEngine en tant que singleton et chacun de ces GameStates en tant que multiton. Bien que de nombreux développeurs considèrent à la fois singleton et multiton comme AntiPattern, je préfère cette conception un peu.
  3. Vous pouvez utiliser des variables globales, elles sont fondamentalement la même chose que Singleton / multiton mais elles ont une légère différence dans le fait qu'elles ne limitent pas le programmeur à ne pas créer d'instances à volonté.

Pour conclure sa réponse, vous pouvez utiliser l'une de ces trois méthodes, mais vous devez faire attention à ne pas trop utiliser l'une ou l'autre car, comme je l'ai dit, toutes ces conceptions sont vraiment dangereuses et pourraient facilement entraîner un code non maintenable.

Ali1S232
la source
6

Est-ce une mauvaise conception d'avoir 2 classes qui ont besoin l'une de l'autre?

C'est un peu une odeur de code , mais on peut partir avec. Si c'est le moyen le plus simple et le plus rapide de mettre votre jeu en place, foncez. Mais gardez cela à l'esprit car il y a de fortes chances que vous deviez le refactoriser à un moment donné.

Le problème avec C ++ est que les dépendances circulaires ne se compileront pas si facilement , il serait donc préférable de s'en débarrasser au lieu de passer du temps à réparer votre compilation.

Voir cette question sur SO pour quelques opinions supplémentaires.

Diriez-vous que [ma conception] est une mauvaise conception?

Non, c'est toujours mieux que de tout mettre dans une seule classe.

Ce n'est pas si génial, mais c'est en fait assez proche de la plupart des implémentations que j'ai vues. Habituellement, vous auriez une classe de gestionnaire pour les états de jeu ( méfiez-vous! ), Et une classe de rendu, et il est assez courant que ce soient des singletons. La dépendance circulaire est donc "cachée", mais elle est potentiellement là.

De plus, comme on vous l'a dit dans les commentaires, c'est un peu bizarre que les classes d'état de jeu effectuent une sorte de rendu. Ils doivent simplement contenir des informations d'état et le rendu doit être géré par un moteur de rendu ou par un composant graphique des objets de jeu eux-mêmes.

Maintenant, il pourrait y avoir le design ultime . Je suis curieux de voir si d'autres réponses apportent une bonne idée. Pourtant, vous êtes probablement celui qui peut trouver le meilleur design pour votre jeu.

Laurent Couvidou
la source
Pourquoi serait-ce une odeur de code? La plupart des objets ayant une relation parent-enfant doivent se voir.
Kikaimaru
4
Parce que c'est le moyen le plus simple de ne pas définir quelle classe est responsable de quoi. Il se retrouve facilement dans deux classes qui sont si profondément couplées que l'ajout de nouveau code peut être effectué dans l'une ou l'autre, de sorte qu'elles ne sont plus séparées conceptuellement. C'est aussi une porte ouverte pour le code spaghetti: la classe A appelle la classe B pour cela, mais B a besoin de ces informations de A, etc. Donc non, je ne dirais pas qu'un objet enfant devrait connaître son parent. Si possible, c'est mieux si ce n'est pas le cas.
Laurent Couvidou
C'est une erreur de pente glissante. Les classes statiques peuvent aussi conduire à de mauvaises choses, mais les classes util ne sont pas une odeur de code. Et je doute vraiment qu'il existe un "bon" moyen de faire des choses comme Scene a SceneNodes et SceneNode a référence à Scene, donc vous ne pouvez pas l'ajouter à deux scènes différentes ... Et où vous arrêteriez-vous? A est-il requis B, B nécessite C et C nécessite A une odeur de code?
Kikaimaru
En effet, c'est exactement comme les classes statiques. Manipulez-le avec précaution, utilisez-le uniquement si vous le devez. Maintenant, je parle par expérience et je ne suis pas le seul à penser de cette façon , mais c'est vraiment une question d'opinion. À mon avis, dans le contexte donné par le PO, c'est en effet malodorant.
Laurent Couvidou
Il n'y a aucune mention de ce cas dans cet article de wikipedia. Et vous ne pouvez pas dire que c'est un cas d '"intimité inappropriée" tant que vous n'avez pas vu ces cours.
Kikaimaru
6

C'est souvent considéré comme une mauvaise conception d'avoir à avoir 2 classes qui se réfèrent directement l'une à l'autre, oui. En termes pratiques, il peut être plus difficile de suivre le flux de contrôle à travers le code, la propriété des objets et leur durée de vie peuvent être compliquées, cela signifie qu'aucune classe n'est réutilisable sans l'autre, cela pourrait signifier que le flux de contrôle devrait vraiment vivre en dehors des deux de ces classes dans une troisième classe «médiateur», etc.

Cependant, il est très courant que 2 objets se réfèrent l'un à l'autre, et la différence ici est que généralement la relation dans une direction est plus abstraite. Par exemple, dans un système Model-View-Controller, l'objet View peut contenir une référence à l'objet Model, et saura tout à son sujet, pouvant accéder à toutes ses méthodes et propriétés afin que la vue puisse se remplir elle-même avec les données pertinentes. . L'objet Model peut contenir une référence à la vue afin qu'il puisse effectuer la mise à jour de la vue lorsque ses propres données ont changé. Mais plutôt que le modèle ayant une référence de vue - ce qui rendrait le modèle dépendant de la vue - généralement, la vue implémente une interface observable, souvent avec seulement 1Update()et le modèle contient une référence à un objet observable, qui peut être une vue. Lorsque le modèle change, il fait appel Update()à tous ses observables et la vue implémente Update()en rappelant dans le modèle et en récupérant toutes les informations mises à jour. L'avantage ici est que le modèle ne sait rien du tout sur les vues (et pourquoi devrait-il le faire?), Et peut être réutilisé dans d'autres situations, même celles sans vues.

Vous avez une situation similaire dans votre jeu. Le GameEngine connaît normalement les GameStates. Mais le GameState n'a pas besoin de tout savoir sur le GameEngine - il a juste besoin d'accéder à certaines méthodes de rendu sur le GameEngine. Cela devrait déclencher une petite alarme dans votre tête qui dit que (a) GameEngine essaie de faire trop de choses dans une classe, et / ou (b) GameState n'a pas besoin de tout le moteur de jeu, juste la partie rendable.

Trois approches pour résoudre ce problème comprennent:

  • Faites en sorte que GameEngine dérive d'une interface Renderable et transmettez cette référence dans GameState. Si jamais vous refactorisez la partie de rendu, il vous suffit de vous assurer qu'elle conserve la même interface.
  • Factorisez la partie de rendu dans un nouvel objet Renderer et passez-le à GameStates à la place.
  • Le laisser tel qu'il est. Peut-être qu'à un moment donné, vous voudrez accéder à toutes les fonctionnalités de GameEngine à partir de GameState, après tout. Mais gardez à l'esprit que les logiciels faciles à entretenir et à étendre nécessitent généralement que chaque classe se réfère aussi peu à l'extérieur que possible, c'est pourquoi diviser les choses en sous-objets et interfaces qui effectuent une seule et bien- la tâche définie est préférable.
Kylotan
la source
0

Il est généralement considéré comme une bonne pratique d'avoir une cohésion élevée avec un faible couplage. Voici quelques liens concernant le couplage et la cohésion.

couplage wikipedia

cohésion wikipedia

faible couplage, haute cohésion

stackoverflow sur les meilleures pratiques

Avoir deux classes se référencent, c'est avoir un couplage élevé. Le cadre google guice vise à atteindre une cohésion élevée avec un faible couplage grâce à des moyens d'injection de dépendance. Je vous suggère de lire un peu plus sur le sujet et ensuite de faire votre propre appel en fonction de votre contexte.

avanderw
la source