Comment éviter les casts dynamiques en cascade?

Oct 12 2020

Ainsi, en coulée générale et dynamic_casten particulier sont à éviter. Mais je ne vois pas d'alternative appropriée à cela:

List<DerivedA*> ListA;
List<DerivedB*> ListB;

Bool Add(Base* obj)
{
  if(DerivedA* AsA = dynamic_cast<DerivedA*>(obj)
  {
    ListA.Add(AsA);
  }
  else if(DerivedB* AsB = dynamic_cast<DerivedB*>(obj)
  {
    ListA.Add(AsB);
  }
}

La plupart des réponses que j'ai trouvées, tout en cherchant une solution, suggèrent qu'il s'agit essentiellement d'un symptôme d'une mauvaise architecture, alors voici un contexte pour mon cas spécifique.

Je travaille sur un petit jeu de tir où le joueur peut porter différentes armes avec son propre type de munitions et différents types de grenades. La quantité de grenades et d'armes à transporter en même temps est limitée.

En tant que représentation visuelle, j'ai un acteur de ramassage qui contient une référence à l'élément réel. C'est de là que vient la base * d'une classe dérivée. Maintenant, lors de la collecte d'un objet, je dois essentiellement ajouter des armes et des grenades à différents inventaires et pour les munitions, je dois vérifier si le joueur possède l'arme respective et augmenter le nombre de munitions en conséquence.

Les alternatives généralement mentionnées sont

  1. Utiliser des méthodes virtuelles: ce qui voudrait dire Item.addTo(inventory* inv)Mais cela nécessite que les éléments connaissent l'implémentation de l'inventaire et si je souhaite passer à une sorte d'inventaire de style RPG qui stocke tous les éléments dans le même tableau, je devrais changer chaque classe dérivée.
  2. Modèle de visiteur; Utilisé ça pour autre chose une fois. Je n'ai pas trouvé de bonne solution pour avoir une implémentation par défaut au cas où je n'aurais pas besoin de comportement spécial pour une classe dérivée et cela mis à part il est très probable que de nouvelles classes dérivées seront ajoutées, nécessitant de changer toutes les classes d'inventaire à chaque fois.

Alors; qu'est-ce qui ne va pas, qu'est-ce que je manque? Ou est-ce réellement un cas où le code ci-dessus est nécessaire?

Réponses

5 amon Oct 11 2020 at 23:03

Vous n'avez rien manqué. Certaines choses sont simplement difficiles à faire.

Les lancers dynamiques ne sont pas intrinsèquement mauvais, ils sont juste fragiles: si vous ajoutez un DerivedCtype, vos lancers continueront de fonctionner mais sauteront simplement les objets de ce type sans erreur de compilation. Cela est très analogue au problème selon lequel une Object::addTo(Inventory&)méthode devrait être synchronisée avec la structure de l'inventaire.

La question centrale est de savoir selon quelles dimensions vous souhaitez simplifier les changements et selon quelles dimensions vous pouvez sacrifier la facilité de changement.

  • Si vous souhaitez modifier facilement le système d'inventaire mais ne pas ajouter de nouveaux types d'objets, les lancers dynamiques ou le modèle de visiteur semblent judicieux.
  • Si vous voulez garder le système d'inventaire fixe mais ajouter facilement de nouveaux types d'articles, cela Object::addTo(Inventory&)semble mieux.

Le modèle de visiteur peut parfois aider en séparant les objets dans une hiérarchie des opérations sur cette hiérarchie, d'une manière qui s'accorde bien avec le typage statique. L'inconvénient est que la hiérarchie des objets devient fixe et ne peut pas être étendue plus tard sans briser les visiteurs existants.

Essayer d'étendre à la fois la hiérarchie et les opérations disponibles s'appelle le problème de l' expression et est très délicat. Je pense qu'il y a eu un certain succès avec l'utilisation de modèles C ++, mais l'intégration de telles solutions avec le modèle de données de votre jeu est probablement exagérée.

Pour votre scénario, tous ces problèmes sont des compromis mais pas des compromis: vous ne rendez aucun changement impossible, certains changements deviennent simplement plus difficiles. Vous pouvez toujours refactoriser plus tard. Personnellement:

  • J'utiliserais l'approche basée sur le modèle de visiteur parce que j'apprécie très fortement la vérification de type statique.
  • Votre solution originale basée sur le downcasting est tout aussi bonne si vous avez une stratégie d'assurance qualité qui trouverait probablement les cas manquants. J'ajouterais une branche else qui enregistre tous les cas manquants et (si en mode test / débogage) coredumps le programme.
  • J'éviterais cette addTo()approche car elle permet de diffuser les connaissances sur la gestion des stocks autour de l'ensemble de l'application (faible cohésion, couplage élevé entre les différentes pièces).

Vous devez également déterminer s'il est judicieux de modéliser vos types DerivedA et DerivedB en tant que classes C ++ distinctes. Surtout pour les jeux, il peut être plus logique de contourner le système de type C ++ et d'implémenter votre propre système vérifié dynamiquement. Bien que ce soit plus sujet aux erreurs, cela offre également beaucoup plus de flexibilité qui (a) facilite les interactions de script, et (b) permet des mécanismes plus complexes, par exemple un type de munitions qui peut également être utilisé comme une grenade. Veuillez lire le chapitre Modèle d'objet de type dans le livre Modèles de programmation de jeu .

1 Noname Oct 15 2020 at 00:53

Je vais sauter avec la réponse grossière / controversée ici et dire que tout le problème ici est la programmation orientée objet (pas un diss sur la POO en général, mais ici). Vous essayez de créer un jeu avec des interactions d'objets complexes à ma connaissance. Vous concevez donc les données dont vous avez besoin et vous cachez le tout derrière des objets avec ces belles interfaces publiques abstraites avec la priorité de maintenir les invariants sur les états internes de ces objets.

Et puis vous essayez de concevoir ces interfaces abstraites centralisées, sauf qu'elles n'en font pas assez. Ils ne gèrent pas le cas où une fonction qui implique des interactions complexes entre des objets comme ramasser des munitions et vérifier si le joueur a le type d'arme respectif ou quelque chose du genre ... ou vérifier s'il pleut pour permettre aux grenouilles de se régénérer. .. ou votre concepteur a malheureusement une nouvelle idée après 6 mois de développement pour une nouvelle arme qui fait quelque chose qui brise tout votre modèle mental de la façon dont les armes devraient même fonctionner.

Vous pouvez donc essayer de continuer à développer ces interfaces publiques et faire en sorte que tous les sous-types pertinents implémentent les fonctions que vous ajoutez pour effectuer les calculs et les mutations nécessaires. Sauf dans le processus, vous créez très rapidement des interfaces monolithiques dont les fonctions ne sont pas applicables à tout ce qui les implémente / hérite, ou faites face à la tentation de descendre à gauche et à droite avec dynamic_casts vérifiant des choses beaucoup plus spécifiques que l'interface généralisée que vous avez construite, perdre une grande partie des avantages de la création de ces abstractions en premier lieu et du polymorphisme qu'elles permettent ... ou rendre vos abstractions si fuyantes en termes de détails qu'elles ne peuvent plus maintenir du tout les invariants. Et lorsque vous avez atteint ce stade, dans mon esprit, ce ne sont pas des séries de modèles de conception qui viennent à la rescousse. C'est se rendre compte que les abstractions que vous avez construites gênent fréquemment ce que vous voulez faire plutôt que de faciliter ce que vous voulez faire ... et de vous en débarrasser humblement au profit, par exemple, d'une procédure ou approche fonctionnelle dans ces cas où ils s’intègrent mieux.

Lorsque vous voulez fréquemment radiographier vos abstractions et atteindre autour d'elles, abattre leur généralité pour aller au concret, je commencerais vraiment à vous demander si ces abstractions devraient même exister. C'est la question la plus productive que je pense que l'on puisse se poser. Est-ce qu'ils vous facilitent vraiment la vie ou sont-ils constamment en train de vous gêner?