Bien sûr, cela dépend de la situation. Mais lorsqu'un objet ou un système à levier inférieur communique avec un système de niveau supérieur, les rappels ou les événements devraient-ils être préférés à la conservation d'un pointeur vers un objet de niveau supérieur?
Par exemple, nous avons une world
classe qui a une variable membre vector<monster> monsters
. Lorsque la monster
classe communique avec le world
, dois-je alors préférer utiliser une fonction de rappel ou dois-je avoir un pointeur vers la world
classe à l'intérieur de la monster
classe?
Réponses:
Il existe trois façons principales pour une classe de parler à une autre sans y être étroitement liée:
Les trois sont étroitement liés les uns aux autres. À bien des égards, un système d'événements n'est qu'une liste de rappels. Un rappel est plus ou moins une interface avec une seule méthode.
En C ++, j'utilise rarement les rappels:
C ++ ne prend pas bien en charge les rappels qui conservent leur
this
pointeur, il est donc difficile d'utiliser des rappels dans du code orienté objet.Un rappel est essentiellement une interface à une méthode non extensible. Au fil du temps, je trouve que je finis presque toujours par avoir besoin de plusieurs méthodes pour définir cette interface et un seul rappel suffit rarement.
Dans ce cas, je ferais probablement une interface. Dans votre question, vous ne précisez pas réellement à quoi
monster
doit réellement communiquerworld
. En supposant, je ferais quelque chose comme:L'idée ici est que vous ne mettez
IWorld
(qui est un nom de merde) que le strict minimum quiMonster
doit être accessible. Sa vision du monde doit être aussi étroite que possible.la source
N'utilisez pas de fonction de rappel pour masquer la fonction que vous appelez. Si vous deviez mettre en place une fonction de rappel, et que cette fonction de rappel aura une et une seule fonction assignée, alors vous ne rompez pas vraiment le couplage du tout. Vous le masquez simplement avec une autre couche d'abstraction. Vous ne gagnez rien (à part peut-être le temps de compilation), mais vous perdez de la clarté.
Je n'appellerais pas exactement cela une meilleure pratique, mais c'est un modèle courant d'avoir des entités contenues par quelque chose pour avoir un pointeur vers leur parent.
Cela étant dit, il pourrait être utile d'utiliser le modèle d'interface pour donner à vos monstres un sous-ensemble limité de fonctionnalités qu'ils peuvent invoquer dans le monde.
la source
En général, j'essaie d'éviter les liens bidirectionnels, mais si je dois les avoir, je m'assure absolument qu'il existe une méthode pour les créer et une pour les rompre, afin que vous n'obteniez jamais d'incohérences.
Souvent, vous pouvez éviter entièrement la liaison bidirectionnelle en transmettant des données si nécessaire. Une refactorisation triviale consiste à faire en sorte qu'au lieu de laisser le monstre garder un lien avec le monde, vous passiez le monde par référence aux méthodes des monstres qui en ont besoin. Mieux encore est de ne passer qu'une interface pour les parties du monde dont le monstre a strictement besoin, ce qui signifie que le monstre ne dépend pas de l'implémentation concrète du monde. Cela correspond au principe de ségrégation d'interface et au principe d'inversion de dépendance , mais ne commence pas à introduire l'abstraction excessive que vous pouvez parfois obtenir avec des événements, des signaux + des slots, etc.
D'une certaine manière, vous pouvez affirmer que l'utilisation d'un rappel est une mini-interface très spécialisée, et c'est très bien. Vous devez décider si vous pouvez atteindre plus efficacement vos objectifs via une collection de méthodes dans un objet d'interface ou plusieurs assorties dans différents rappels.
la source
J'essaie d'éviter que des objets contenus appellent leur conteneur car je trouve que cela crée de la confusion, cela devient trop facile à justifier, ça va devenir surutilisé et ça crée des dépendances qui ne peuvent pas être gérées.
À mon avis, la solution idéale est que les classes de niveau supérieur soient suffisamment intelligentes pour gérer les classes de niveau inférieur. Par exemple, le monde sachant déterminer si une collision entre un monstre et un chevalier s'est produit sans que l'autre soit au courant de l'autre est mieux moi aussi que le monstre demandant au monde s'il est entré en collision avec un chevalier.
Une autre option dans votre cas, je trouverais probablement pourquoi la classe monstre a besoin de connaître la classe mondiale et vous constaterez très probablement qu'il y a quelque chose dans la classe mondiale qui peut être divisé en une classe à part sens pour la classe de monstre à connaître.
la source
Vous n'irez pas très loin sans événements, évidemment, mais avant même de commencer à écrire (et à concevoir) un système d'événements, vous devriez vous poser la vraie question: pourquoi le monstre communiquerait-il avec la classe mondiale? Faut-il vraiment?
Prenons une situation "classique", un monstre attaquant un joueur.
Le monstre attaque: le monde peut très bien identifier la situation où un héros est à côté d'un monstre et dire au monstre d'attaquer. Ainsi, la fonction dans monstre serait:
Mais le monde (qui connaît déjà le monstre) n'a pas besoin d'être connu du monstre. En fait, le monstre peut ignorer l'existence même de la classe mondiale, ce qui est probablement mieux.
Même chose lorsque le monstre se déplace (je laisse les sous-systèmes obtenir la créature et gérer le calcul / l'intention de déplacement pour elle, le monstre n'étant qu'un sac de données, mais beaucoup de gens diraient que ce n'est pas une vraie POO).
Mon point étant: les événements (ou rappels) sont grands, bien sûr, mais ils ne sont pas la seule réponse à tous les problèmes auxquels vous serez confronté.
la source
Chaque fois que je le peux, j'essaie de restreindre la communication entre les objets à un modèle de demande et réponse. Il y a un ordre partiel implicite sur les objets dans mon programme de telle sorte qu'entre deux objets A et B, il puisse y avoir un moyen pour A d'appeler directement ou indirectement une méthode de B ou pour B d'appeler directement ou indirectement une méthode de A , mais il n'est jamais possible pour A et B d'appeler mutuellement leurs méthodes. Parfois, bien sûr, vous voulez avoir une communication en amont avec l'appelant d'une méthode. Il y a plusieurs façons que j'aime faire cela, et ni l'un ni l'autre ne sont des rappels.
Une façon consiste à inclure plus d'informations dans la valeur de retour de l'appel de méthode, ce qui signifie que le code client doit décider quoi en faire une fois que la procédure lui a rendu le contrôle.
L'autre façon consiste à appeler un objet enfant mutuel. Autrement dit, si A appelle une méthode sur B et que B a besoin de communiquer des informations à A, B appelle une méthode sur C, où A et B peuvent tous deux appeler C, mais C ne peut pas appeler A ou B. L'objet A serait alors chargé d'obtenir les informations de C après que B retourne le contrôle à A. Notez que ce n'est pas vraiment fondamentalement différent de la première façon que j'ai proposée. L'objet A ne peut toujours récupérer les informations qu'à partir d'une valeur de retour; aucune des méthodes de l'objet A n'est invoquée par B ou C. Une variante de cette astuce consiste à passer C comme paramètre à la méthode, mais les restrictions sur la relation de C à A et B s'appliquent toujours.
Maintenant, la question importante est pourquoi j'insiste pour faire les choses de cette façon. Il y a trois raisons principales:
self
pendant l'exécution d'une méthode est cette méthode et aucune autre. C'est le même genre de raisonnement qui pourrait conduire à mettre des mutex sur des objets concurrents.Je ne suis pas contre toutes les utilisations des rappels. Conformément à ma politique de ne jamais "appeler l'appelant", si un objet A appelle une méthode sur B et lui passe un rappel, le rappel ne peut pas changer l'état interne de A, et cela inclut les objets encapsulés par A et le objets dans le contexte de A. En d'autres termes, le rappel ne peut appeler que des méthodes sur des objets qui lui sont donnés par B. Le rappel, en effet, est soumis aux mêmes restrictions que B.
Une dernière extrémité lâche à rattacher est que je permettrai l'invocation de toute fonction pure, indépendamment de cet ordre partiel dont j'ai parlé. Les fonctions pures sont un peu différentes des méthodes dans la mesure où elles ne peuvent pas changer ou dépendre d'un état mutable ou d'effets secondaires, donc ne vous inquiétez pas de les confondre.
la source
Personnellement? J'utilise juste un singleton.
Ouais, d'accord, mauvaise conception, pas orientée objet, etc. Tu sais quoi? Je m'en fiche . J'écris un jeu, pas une vitrine technologique. Personne ne va me noter sur le code. Le but est de créer un jeu amusant, et tout ce qui me gênera donnera un jeu moins amusant.
Aurez-vous jamais deux mondes en cours d'exécution à la fois? Peut être! Peut-être que vous le ferez. Mais à moins que vous ne puissiez penser à cette situation en ce moment, vous ne le ferez probablement pas.
Donc, ma solution: faire un singleton mondial. Appelez des fonctions dessus. Finissez avec tout le bordel. Vous pouvez passer un paramètre supplémentaire à chaque fonction - et ne vous y trompez pas, c'est là que cela mène. Ou vous pouvez simplement écrire du code qui fonctionne.
Le faire de cette façon nécessite un peu de discipline pour nettoyer les choses quand cela devient salissant (c'est "quand", pas "si") mais il n'y a aucun moyen d'empêcher le code de devenir salissant - soit vous avez le problème des spaghettis, soit des milliers -problème de couches d'abstraction. Au moins de cette façon, vous n'écrivez pas de grandes quantités de code inutile.
Et si vous décidez de ne plus vouloir de singleton, il est généralement assez simple de s'en débarrasser. Prend un peu de travail, nécessite de passer un milliard de paramètres, mais ce sont des paramètres que vous devez de toute façon contourner.
la source