Dans de nombreux endroits, vous pouvez lire que dynamic_cast
signifie "mauvaise conception". Mais je ne trouve aucun article avec un usage approprié (montrant un bon design, pas seulement "comment utiliser").
J'écris un jeu de société avec un tableau et de nombreux types de cartes décrits avec de nombreux attributs (certaines cartes peuvent être mises sur le tableau). J'ai donc décidé de le décomposer en classes/interfaces suivantes:
class Card {};
class BoardCard : public Card {};
class ActionCard : public Card {};
// Other types of cards - but two are enough
class Deck {
Card* draw_card();
};
class Player {
void add_card(Card* card);
Card const* get_card();
};
class Board {
void put_card(BoardCard const*);
};
Certains gars m'ont suggéré de n'utiliser qu'une classe décrivant une carte. Mais je voudrais parler de nombreux attributs s’excluant mutuellement. Et dans le cas de la fonction put_card(BoardCard const&)
de la classe Board, c'est une partie de l'interface que je ne peux mettre aucune carte sur la carte. Si je n'avais qu'un seul type de carte, je devrais le vérifier dans la méthode.
Je vois le flux comme suit:
J'utilise donc dynamic_cast
avant de mettre une carte sur le tableau. Je pense qu'il est hors de question d'utiliser une méthode virtuelle (en outre, je n'aurais aucun sens d'ajouter une action sur le tableau à chaque carte).
Ma question est donc la suivante: qu'est-ce que j'ai mal conçu? Comment pourrais-je éviter dynamic_cast
? Utiliser un attribut type et if
s serait une meilleure solution ...?
P.S . Toute source traitant de l'utilisation de dynamic_cast
dans le contexte du design est plus qu'appréciée.
Oui, dynamic_cast
est une odeur de code, mais il en va de même pour l’ajout de fonctions qui permettent de donner l’impression que vous avez une bonne interface polymorphe mais que vous avez en fait un dynamic_cast
, c’est-à-dire can_put_on_board
. J'irais jusqu'à dire que can_put_on_board
est pire - vous dupliquez du code implémenté autrement par dynamic_cast
et encombrant l'interface.
Comme avec toutes les odeurs de code, elles doivent vous rendre méfiant et ne signifient pas nécessairement que votre code est mauvais. Tout dépend de ce que vous essayez d’atteindre.
Si vous implémentez un jeu de société comportant 5 000 lignes de code, deux catégories de cartes, tout ce qui fonctionne convient. Si vous concevez quelque chose de plus grand, extensible et permettant éventuellement aux cartes d'être créées par des non-programmeurs (qu'il s'agisse d'un besoin réel ou que vous le fassiez pour la recherche), cela ne fonctionnera probablement pas.
En supposant que ce dernier point, examinons quelques alternatives.
Vous pourriez imposer à la carte d’appliquer correctement la carte au lieu d’un code externe. Par exemple. ajoutez une fonction play(Context& c)
à la carte (la Context
étant un moyen d’accéder au tableau et ainsi de suite, le cas échéant). Une carte de conseil saura qu’elle ne peut être appliquée qu’à un conseil et qu’elle n’est pas nécessaire de la lancer.
J'abandonnerais toutefois totalement l'utilisation de l'héritage. L’un de ses nombreux problèmes est de savoir comment il introduit une catégorisation de toutes les cartes. Laisse moi te donner un exemple:
BoardCard
et ActionCard
en plaçant toutes les cartes dans ces deux compartiments;Action
ou une carte Board
;BoardActionCard
ou tout autre moyen);RedBoardCard
, BlueBoardCard
, RedActionCard
etc.?D'autres exemples expliquent pourquoi il faut éviter les héritages et comment réaliser un polymorphisme à l'exécution, sinon vous voudrez peut-être regarder l'excellent "L'héritage est la classe de base du mal" de Sean Parent . Une bibliothèque prometteuse mettant en œuvre ce type de polymorphisme est dyno , je ne l’ai pas encore essayée.
Une solution possible pourrait être:
class Card final {
public:
template <class T>
Card(T model) :
model_(std::make_shared<Model<T>>(std::move(model)))
{}
void play(Context& c) const {
model_->play(c);
}
// ... any other functions that can be performed on a card
private:
class Context {
public:
virtual ~Context() = default;
virtual void play(Context& c) const = 0;
};
template <class T>
class Model : public Context {
public:
void play(Context& c) const override {
play(model_, c);
// or
model_.play(c);
// depending on what contract you want to have with implementers
}
private:
T model_;
};
std::shared_ptr<const Context> model_;
};
Ensuite, vous pouvez soit créer des classes par type de carte:
class Goblin final {
void play(Context& c) const {
// apply effects of card, e.g. take c.board() and put the card there
}
};
Ou implémentez des comportements pour différentes catégories, par ex. avoir un
template <class T>
void play(const T& card, Context& c);
template puis utilisez enable_if pour le gérer pour différentes catégories:
template <class T, class = std::enable_if<IsBoardCard_v<T>>
void play(const T& card, Context& c) {
c.board().add(Card(card));
}
où:
template <class T>
struct IsBoardCard {
static constexpr auto value = T::IS_BOARD_CARD;
};
template <class T>
using IsBoardCard_v = IsBoardCard<T>::value;
en définissant ensuite votre Goblin
comme:
class Goblin final {
public:
static constexpr auto IS_BOARD_CARD = true;
static constexpr auto COLOR = Color::RED;
static constexpr auto SUPERMAGIC = true;
};
ce qui vous permettrait de classer vos cartes dans de nombreuses dimensions tout en laissant également la possibilité de spécialiser entièrement le comportement en implémentant une fonction play
différente.
L'exemple de code utilise std :: shared_ptr pour stocker le modèle, mais vous pouvez certainement faire quelque chose de plus intelligent ici. J'aime utiliser un stockage de taille statique et n'autoriser que l'utilisation de Ts d'une certaine taille et alignement maximum. Sinon, vous pouvez utiliser un std :: unique_ptr (qui désactiverait toutefois la copie) ou une variante exploitant l'optimisation de petite taille.
dynamic_cast
dynamic_cast
est généralement mal aimé, car on peut facilement en abuser pour casser complètement les abstractions utilisées. Et il n'est pas sage de dépendre d'implémentations spécifiques. Bien sûr, cela peut être nécessaire, mais très rarement, de sorte que presque tout le monde adopte une règle empirique - vous ne devriez probablement pas l'utiliser. C'est une odeur de code qui peut impliquer que vous deviez repenser vos abstractions, car elles ne sont peut-être pas celles dont votre domaine a besoin. Peut-être que dans votre jeu la Board
ne devrait pas avoir la méthode put_card
- peut-être que la carte devrait avoir la méthode play(const PlaySpace *)
où Board
implémente PlaySpace
ou quelque chose comme ça. Même (CppCoreGuidelines) décourage l'utilisation de dynamic_cast
dans la plupart des cas .
Généralement, peu de gens ont des problèmes de ce genre, mais je les ai déjà rencontrés plusieurs fois. Le problème s'appelle Double (ou Multiple) Dispatch. Voici un article assez ancien, mais tout à fait pertinent sur la double dépêche (à part le auto_ptr
préhistorique): http://www.drdobbs.com/double-dispatch-revisited/184405527 }
Dans l'un de ses livres, Scott Meyers a également écrit quelque chose sur la construction de la matrice de double expédition avec dynamic_cast
. Mais dans l’ensemble, ces dynamic_cast
s sont «cachés» dans cette matrice - les utilisateurs ne savent pas quel type de magie se passe à l’intérieur.
À noter - l'envoi multiple est également considéré comme une odeur de code :-).
Découvrez le motif visiteur . Il peut être utilisé en remplacement de dynamic_cast
mais c’est aussi une sorte d’odeur de code.
Je recommande généralement d'utiliser dynamic_cast
et visiteur en dernier recours comme outils de résolution des problèmes de conception, car ils rompent l'abstraction, ce qui accroît la complexité.
Vous pouvez appliquer les principes sous-jacents à { COM de Microsoft } _ et fournir une série de interfaces, chaque interface décrivant un ensemble de comportements connexes. Dans COM, vous déterminez si une interface spécifique est disponible en appelant QueryInterface
, mais en C++ moderne, dynamic_cast
fonctionne de la même manière et est plus efficace.
class Card {
virtual void ~Card() {} // must have at least one virtual method for dynamic_cast
};
struct IBoardCard {
virtual void put_card(Board* board);
};
class BoardCard : public Card, public IBoardCard {};
class ActionCard : public Card {};
// Other types of cards - but two are enough
class Deck {
Card* draw_card();
};
class Player {
void add_card(Card* card);
Card const* get_card();
};
class Board {
void put_card(Card const* card) {
const IBoardCard *p = dynamic_cast<const IBoardCard*>(card);
if (p != null) p->put_card(this);
};
C'est peut-être un mauvais exemple, mais j'espère que vous avez compris l'idée.
J'ai toujours trouvé dans l'utilisation d'un casting une odeur de code, et d'après mon expérience, 90% du temps où le casting était dû à un mauvais design. J'ai constaté que dynamic_cast était utilisé dans une application critique pour le temps, qui fournissait davantage d'amélioration des performances que l'héritage de plusieurs interfaces ou la récupération d'une sorte d'énumération à partir de l'objet (comme un type). Donc, le code sentait bon, mais l'utilisation de la distribution dynamique en valait la peine.
Cela dit, j’éviterai la conversion dynamique dans votre cas ainsi que les héritages multiples de différentes interfaces.
Avant d’arriver à ma solution, votre description donne l’impression que de nombreux détails ont été omis au sujet du comportement des cartes ou de leurs conséquences sur le tableau .__ et le jeu lui-même. J'ai utilisé cela comme une contrainte supplémentaire, en essayant de garder les choses en boîte et maintenables.
J'irais pour une composition au lieu d'un héritage. Cela vous donnera également la possibilité d'utiliser la carte comme une "usine":
Voir [ https://en.wikipedia.org/wiki/Composition_over_inheritance pour plus de détails]. Je voudrais citer: La composition fournit également un domaine d’activité plus stable à long terme, car elle est moins sujette aux bizarreries des membres de la famille.En d’autres termes, il est préférable de composer ce qu’un objet peut faire ( A - A) que d’étendre ce qu’il est (IS - A). [1]
Une carte/élément peut ressembler à ceci:
//the card placed on the board.
class BoardElement {
public:
BoardElement() {}
virtual ~BoardElement() {};
//up to you if you want to add a read() methods to read data from the card description (XML / JSON / binary data)
// but that should not be part of the interface. Talking about a potential "Wizard", it's probably more related to the WizardCard - WizardElement relation/implementation
//some helpful methods:
// to be called by the board when placed
virtual void OnBoard() {}
virtual void Frame(const float time) { /*do something time based*/ }
virtual void Draw() {}
// to be called by the board when removed
virtual void RemovedFromBoard() {}
};
la carte peut représenter quelque chose à utiliser dans un deck ou dans les mains de l'utilisateur, je vais ajouter une interface de ce type
class Card {
public:
Card() {}
virtual ~Card() {}
//that will be invoked by the user in order to provide something to the Board, or NULL if nothing should be added.
virtual std::shared_ptr<BoardElement*> getBoardElement() { return nullptr; }
virtual void Frame(const float time) { /*do something time based*/ }
virtual void Draw() {}
//usefull to handle resources or internal states
virtual void OnUserHands() {}
virtual void Dropped() {}
};
Je voudrais ajouter que ce modèle permet de nombreuses astuces dans la méthode getBoardElement()
, consistant à agir en tant qu'usine (un élément doit donc être généré avec sa propre durée de vie), Renvoyant un membre de données Card
tel que std:shared_ptr<BoardElement> wizard3D;
(à titre d'exemple) , créez une liaison entre Card
et BoardElement
comme pour:
class WizardBoardElement : public BoardElement {
public:
WizardBoardElement(const Card* owner);
// other members omitted ...
};
La liaison peut être utile pour lire des données de configuration ou autre.
Ainsi, l'héritage de Card
et de BoardElement
sera utilisé pour implémenter les fonctionnalités exposées par les classes de base et non pour fournir d'autres méthodes accessibles uniquement via un dynamic_cast
.
Pour être complet:
class Player {
void add(Card* card) {
//..
card->OnUserHands();
//..
}
void useCard(Card* card) {
//..
//someway he's got to retrieve the board...
getBoard()->add(card->getBoardElement());
//..
}
Card const* get_card();
};
class Board {
void add(BoardElement* el) {
//..
el->OnBoard();
//..
}
};
De cette manière, nous n'avons pas de dynamic_cast. Le joueur et le forum font des choses simples sans connaître les détails intérieurs de la carte, ce qui permet de bien séparer les différents objets et d'augmenter la facilité de maintenance.
En parlant de la ActionCard
, et des "effets" pouvant être appliqués à d'autres joueurs ou à votre avatar, nous pouvons penser à une méthode comme:
enum EffectTarget {
MySelf, //a player on itself, an enemy on itself
MainPlayer,
Opponents,
StrongOpponents
//....
};
class Effect {
public:
//...
virtual void Do(Target* target) = 0;
//...
};
class Card {
public:
//...
struct Modifiers {
EffectTarget eTarget;
std::shared_ptr<Effect> effect;
};
virtual std::vector<Modifiers> getModifiers() { /*...*/ }
//...
};
class Player : public Target {
public:
void useCard(Card* card) {
//..
//someway he's got to retrieve the board...
getBoard()->add(card->getBoardElement());
auto modifiers = card->getModifiers();
for each (auto modifier in modifiers)
{
//this method is supposed to look at the board, at the player and retrieve the instance of the target
Target* target = getTarget(modifier.eTarget);
modifier.effect->Do(target);
}
//..
}
};
C’est un autre exemple du même schéma pour appliquer les effets de la carte, évitant aux cartes de connaître en détail le tableau et son statut, qui joue la carte et de conserver le code dans Player
assez simple.
J'espère que cela pourra vous aider, Passez une bonne journée, Stefano.
Comme je ne vois pas pourquoi vous n'utiliseriez pas de méthodes virtuelles, je vais vous présenter comment je le ferais. J'ai d'abord l'interface ICard
pour toutes les cartes. Ensuite, je ferais une distinction entre les types de cartes (c.-à-d. BoardCard et ActionCard et les cartes que vous avez). Et toutes les cartes héritent de l'un ou l'autre des types de cartes.
class ICard {
virtual void put_card(Board* board) = 0;
virtual void accept(CardVisitor& visitor) = 0; // See later, visitor pattern
}
class ActionCard : public ICard {
void put_card(Board* board) final {
// std::cout << "You can't put Action Cards on the board << std::endl;
// Or just do nothing, if the decision of putting the card on the board
// is not up to the user
}
}
class BoardCard : public ICard {
void put_card(Board* board) final {
// Whatever implementation puts the card on the board, mb something like:
board->place_card_on_board(this);
}
}
class SomeBoardCard : public BoardCard {
void accept(CardVisitor& visitor) final { // visitor pattern
visitor.visit(this);
}
void print_information(); // see BaseCardVisitor in the next code section
}
class SomeActionCard : public ActionCard {
void accept(CardVisitor& visitor) final { // visitor pattern
visitor.visit(this);
}
void print_information(); // see BaseCardVisitor
}
class Board {
void put_card(ICard* const card) {
card->put_card(this);
}
void place_card_on_board(BoardCard* card) {
// place it on the board
}
}
J'imagine que l'utilisateur doit savoir d'une manière ou d'une autre quelle carte il a piochée, c'est pourquoi je mettrais en œuvre le modèle de visiteur. Vous pouvez également placer la méthode accept
-, que j'ai placée dans les classes/cartes les plus dérivées, dans les types de carte (BoardCard, ActionCard), en fonction de l'endroit où vous souhaitez tracer une ligne sur les informations qui doivent être fournies à l'utilisateur.
template <class T>
class BaseCardVisitor {
void visit(T* card) {
card->print_information();
}
}
class CardVisitor : public BaseCardVisitor<SomeBoardCard>,
public BaseCardVisitor<SomeActionCard> {
}
class Player {
void add_card(ICard* card);
ICard const* get_card();
void what_is_this_card(ICard* card) {
card->accept(visitor);
}
private:
CardVisitor visitor;
};
Je pense que je finirais avec quelque chose comme ceci (compilé avec clang 5.0 avec -std = c ++ 17). Je suis attentif à vos commentaires. Donc, chaque fois que je veux gérer différents types de cartes, je dois instancier un répartiteur et lui fournir des méthodes avec les signatures appropriées.
#include <iostream>
#include <typeinfo>
#include <type_traits>
#include <vector>
template <class T, class... Args>
struct any_abstract {
static bool constexpr value = std::is_abstract<T>::value || any_abstract<Args...>::value;
};
template <class T>
struct any_abstract<T> {
static bool constexpr value = std::is_abstract<T>::value;
};
template <class T, class... Args>
struct StaticDispatcherImpl {
template <class P, class U>
static void dispatch(P* ptr, U* object) {
if (typeid(*object) == typeid(T)) {
ptr->do_dispatch(*static_cast<T*>(object));
return;
}
if constexpr (sizeof...(Args)) {
StaticDispatcherImpl<Args...>::dispatch(ptr, object);
}
}
};
template <class Derived, class... Args>
struct StaticDispatcher {
static_assert(not any_abstract<Args...>::value);
template <class U>
void dispatch(U* object) {
if (object) {
StaticDispatcherImpl<Args...>::dispatch(static_cast<Derived *>(this), object);
}
}
};
struct Card {
virtual ~Card() {}
};
struct BoardCard : Card {};
struct ActionCard : Card {};
struct Board {
void put_card(BoardCard const& card, int const row, int const column) {
std::cout << "Putting card on " << row << " " << column << std::endl;
}
};
struct UI : StaticDispatcher<UI, BoardCard, ActionCard> {
void do_dispatch(BoardCard const& card) {
std::cout << "Get row to put: ";
int row;
std::cin >> row;
std::cout << "Get row to put:";
int column;
std::cin >> column;
board.put_card(card, row, column);
}
void do_dispatch(ActionCard& card) {
std::cout << "Handling action card" << std::endl;
}
private:
Board board;
};
struct Game {};
int main(int, char**) {
Card* card;
ActionCard ac;
BoardCard bc;
UI ui;
card = ∾
ui.dispatch(card);
card = &bc;
ui.dispatch(card);
return 0;
}
Qu'est-ce que j'ai mal conçu?
Le problème est que vous devez toujours étendre ce code chaque fois qu'un nouveau type de Card
est introduit.
Comment pourrais-je éviter dynamic_cast?
Le moyen habituel pour éviter cela consiste à utiliser des interfaces (c'est-à-dire des classes abstraites pures):
struct ICard {
virtual bool can_put_on_board() = 0;
virtual ~ICard() {}
};
class BoardCard : public ICard {
public:
bool can_put_on_board() { return true; };
};
class ActionCard : public ICard {
public:
bool can_put_on_board() { return false; };
};
De cette façon, vous pouvez simplement utiliser une référence ou un pointeur sur ICard
et vérifier si le type réel qu'il détient peut être placé sur la Board
.
Mais je ne trouve aucun article avec un usage approprié (montrant un bon design, pas seulement "comment utiliser").
En général, je dirais qu’il n’ya pas d’excellents cas d’utilisation de la diffusion dynamique dans la vie réelle.
Parfois, je l'ai utilisé dans le code de débogage pour des réalisations CRTP telles que
template<typename Derived>
class Base {
public:
void foo() {
#ifndef _DEBUG
static_cast<Derived&>(*this).doBar();
#else
// may throw in debug mode if something is wrong with Derived
// not properly implementing the CRTP
dynamic_cast<Derived&>(*this).doBar();
#endif
}
};