Couplage faible et cohésion étroite


11

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 worldclasse qui a une variable membre vector<monster> monsters. Lorsque la monsterclasse communique avec le world, dois-je alors préférer utiliser une fonction de rappel ou dois-je avoir un pointeur vers la worldclasse à l'intérieur de la monsterclasse?


Mis à part l'orthographe dans le titre de la question, elle n'est pas réellement formulée sous la forme d'une question. Je pense que le reformuler pourrait aider à consolider ce que vous demandez ici, car je ne pense pas que vous puissiez obtenir une réponse utile à cette question dans sa forme actuelle. Et ce n'est pas non plus une question de conception de jeu, c'est une question sur la structure de programmation (je ne sais pas si cela signifie que la balise de conception est ou n'est pas appropriée, je ne me souviens pas d'où nous sommes descendus sur la `` conception de logiciels '' et les balises)
MrCranky

Réponses:


10

Il existe trois façons principales pour une classe de parler à une autre sans y être étroitement liée:

  1. Grâce à une fonction de rappel.
  2. Grâce à un système d'événements.
  3. Grâce à une interface.

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:

  1. C ++ ne prend pas bien en charge les rappels qui conservent leur thispointeur, il est donc difficile d'utiliser des rappels dans du code orienté objet.

  2. 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 monsterdoit réellement communiquer world. En supposant, je ferais quelque chose comme:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

L'idée ici est que vous ne mettez IWorld(qui est un nom de merde) que le strict minimum qui Monsterdoit être accessible. Sa vision du monde doit être aussi étroite que possible.


1
+1 Les délégués (rappels) deviennent généralement plus nombreux au fil du temps. Donner une interface aux monstres afin qu'ils puissent se familiariser avec les choses est une bonne façon de procéder à mon avis.
Michael Coleman

12

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.


+1 Donner au monstre un moyen limité d'appeler le parent est à mon avis une bonne manière intermédiaire.
Michael Coleman

7

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.


3

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.


2

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:

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

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é.


1

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:

  • Il garde mes objets plus lâchement couplés. Mes objets peuvent encapsuler d'autres objets, mais ils ne dépendront jamais du contexte de l'appelant et le contexte ne dépendra jamais des objets encapsulés.
  • Il garde mon flux de contrôle facile à raisonner. C'est agréable de pouvoir supposer que le seul code qui peut changer l'état interne selfpendant 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.
  • Il protège les invariants sur les données encapsulées de mes objets. Les méthodes publiques sont autorisées à dépendre des invariants, et ces invariants peuvent être violés si une méthode peut être appelée en externe alors qu'une autre est déjà en cours d'exécution.

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.


0

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.


2
Je dirais probablement un peu plus succinctement: "N'ayez pas peur de refactoriser".
Tetrad

Les singletons sont mauvais! Les globaux sont bien meilleurs. Toutes les vertus singleton que vous avez énumérées sont les mêmes pour un global. J'utilise des pointeurs globaux (en fait des fonctions globales renvoyant des références) à mes différents sous-systèmes et les initialise / détruis les dans ma fonction principale. Les pointeurs globaux évitent les problèmes d'ordre d'initialisation, les singletons pendant pendant le démontage, la construction non triviale des singletons, etc. Je répète que les singletons sont mauvais.
deft_code

@Tetrad, très d'accord. C'est l'une des meilleures compétences que vous puissiez avoir.
ZorbaTHut
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.