Héritage vs composition pour les pièces d'échecs

9

Une recherche rapide de cet échange de pile montre qu'en général la composition est généralement considérée comme plus flexible que l'héritage mais comme toujours cela dépend du projet, etc. et il y a des moments où l'héritage est le meilleur choix. Je veux faire un jeu d'échecs en 3D où chaque pièce a un maillage, éventuellement des animations différentes et ainsi de suite. Dans cet exemple concret, il semble que vous pourriez plaider en faveur des deux approches, ai-je tort?

L'héritage ressemblerait à quelque chose comme ça (avec le bon constructeur, etc.)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

qui obéit certainement au principe "is-a" alors que la composition ressemblerait à quelque chose comme ça

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

Est-ce plus une question de préférence personnelle ou une approche est-elle clairement un choix plus intelligent que l'autre ici?

EDIT: Je pense que j'ai appris quelque chose de chaque réponse, donc je me sens mal de n'en avoir choisi qu'une. Ma solution finale s'inspirera de plusieurs des articles ici (toujours en cours). Merci à tous ceux qui ont pris le temps de répondre.

CanISleepYet
la source
Je crois que les deux peuvent exister côte à côte, ces deux sont à des fins différentes, mais ce ne sont pas exclusifs les uns des autres .. Les échecs sont composés de différentes pièces et planches, et les pièces sont de types différents. Ces pièces partagent certaines propriétés et comportements de base, et ont également des propriétés et des comportements spécifiques. Donc, selon moi, la composition devrait être appliquée par dessus bord et pièces, tandis que l'héritage devrait être suivi sur les types de pièces.
Hitesh Gaur
Bien que certaines personnes puissent prétendre que tout l'héritage est mauvais, je pense que l'idée générale est que l'héritage de l'interface est bon / fin et que l'héritage de l'implémentation est utile mais peut être problématique. Tout ce qui dépasse un seul niveau d'héritage est discutable, selon mon expérience. Cela peut être OK au-delà de cela, mais ce n'est pas simple de s'en tirer sans faire un gâchis alambiqué.
JimmyJames
Le modèle de stratégie est courant dans la programmation de jeux pour une raison. var Pawn = new Piece(howIMove, howITake, whatILookLike)me semble beaucoup plus simple, plus gérable et plus facile à gérer qu’une hiérarchie d’héritage.
Ant P
@AntP C'est un bon point, merci!
CanISleepYet
@AntP Faites-en une réponse! ... Cela a beaucoup de mérite!
svidgen

Réponses:

4

À première vue, votre réponse prétend que la solution "composition" n'utilise pas l'héritage. Mais je suppose que vous avez simplement oublié d'ajouter ceci:

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(et plusieurs de ces dérivations, une pour chacun des six types de pièces). Maintenant, cela ressemble plus au modèle classique de «stratégie», et il utilise également l'héritage.

Malheureusement, cette solution a un défaut: chacun Piececontient désormais deux fois ses informations de type:

  • à l'intérieur de la variable membre type

  • à l'intérieur movementComponent(représenté par le sous-type)

Cette redondance est ce qui pourrait vraiment vous causer des ennuis - une classe simple comme a Piecedevrait fournir une "source unique de vérité", pas deux sources.

Bien sûr, vous pouvez essayer de stocker les informations de type uniquement dans typeet ne créer aucune classe enfant MovementComponent. Mais cette conception conduirait très probablement à une énorme " switch(type)" déclaration dans la mise en oeuvre de GetValidMoveSquares. Et c'est certainement une indication forte que l'héritage est le meilleur choix ici .

Note dans la conception « d'héritage », il est assez facile de fournir le champ « type » d'une manière non redondante: ajouter une méthode virtuelle GetType()pour BasePieceet mettre en œuvre en conséquence dans chaque classe de base.

Concernant la "promotion": je suis ici avec @svidgen, je trouve discutable les arguments présentés dans la réponse de @ TheCatWhisperer .

Interpréter la "promotion" comme un échange physique de pièces au lieu de l'interpréter comme un changement du type d'une même pièce me semble tout à fait plus naturel. Donc, l'implémentation de la même manière - en échangeant une pièce par une autre d'un type différent - ne causera probablement pas d'énormes problèmes - du moins pas pour le cas spécifique des échecs.

Doc Brown
la source
Merci de m'avoir donné le nom du motif, je ne savais pas qu'il s'appelait Stratégie. Vous avez également raison, j'ai manqué l'héritage de la composante mouvement dans ma question. Le type est-il vraiment redondant? Si je veux illuminer toutes les reines du tableau, il semblerait étrange de vérifier quel composant de mouvement elles avaient, plus je devrais lancer le composant de mouvement pour voir de quel type il s'agit, ce qui serait super laid non?
CanISleepYet
@CanISleepYet: le problème avec le champ "type" proposé est qu'il pourrait diverger du sous-type de movementComponent. Voir ma modification comment fournir un champ de type de manière sûre dans la conception "héritage".
Doc Brown
4

Je préférerais probablement l'option plus simple, sauf si vous avez des raisons explicites et bien articulées ou de fortes préoccupations d'extensibilité et de maintenance.

L'option d'héritage est moins de ligne de code et moins de complexité. Chaque type de pièce a une relation 1: 1 «immuable» avec ses caractéristiques de mouvement. (Contrairement à sa représentation, qui est de 1 à 1, mais pas nécessairement immuable - vous voudrez peut-être offrir différents "skins".)

Si vous rompez cette relation immuable 1-à-1 entre les pièces et leur comportement / mouvement en plusieurs classes, vous n'ajoutez probablement que de la complexité - et pas grand-chose d'autre. Donc, si je passais en revue ou héritais de ce code, je m'attendrais à voir de bonnes raisons documentées de la complexité.

Cela dit, une option encore plus simple , l'OMI, est de créer une Piece interface que vos pièces individuelles implémentent . Dans la plupart des langues, cela est en réalité très différent de l' héritage , car une interface ne vous empêchera pas d'implémenter une autre interface . ... Vous n'obtenez tout simplement aucun comportement de classe de base gratuitement dans ce cas: vous voudriez mettre le comportement partagé ailleurs.

svidgen
la source
Je n'achète pas que la solution d'héritage est «moins de lignes de code». Peut-être dans cette classe, mais pas dans l'ensemble.
JimmyJames
@JimmyJames Vraiment? ... Il suffit de compter les lignes de code dans l'OP ... certes pas la solution entière, mais pas un mauvais indicateur de quelle solution est plus LOC et complexité à maintenir.
svidgen
Je ne veux pas trop insister là-dessus parce que tout est théorique à ce stade, mais oui, je le pense vraiment. Mon argument étant que ce morceau de code est négligeable par rapport à l'ensemble de l'application. Si cela simplifie la mise en œuvre des règles (discutable), vous pouvez enregistrer des dizaines voire des centaines de lignes de code.
JimmyJames
Merci, je n'ai pas pensé à l'interface mais vous avez peut-être raison de dire que c'est la meilleure façon de procéder. Plus simple est presque toujours meilleur.
CanISleepYet
2

Le problème avec les échecs est que les règles du jeu et des pièces sont prescrites et fixes. Tout design qui fonctionne est bien - utilisez ce que vous voulez! Expérimentez et essayez-les tous.

Dans le monde des affaires, cependant, rien n'est aussi strictement prescrit comme celui-ci - les règles et exigences commerciales changent avec le temps, et les programmes doivent changer avec le temps pour s'adapter. C'est là que les données is-a vs has-a vs font la différence. Ici, la simplicité facilite la maintenance de solutions complexes au fil du temps et de l'évolution des exigences. En général, dans les affaires, nous devons également faire face à la persistance, ce qui peut également impliquer une base de données. Ainsi, nous avons des règles comme ne pas utiliser plusieurs classes lorsqu'une seule classe fera le travail, et n'utilisez pas l'héritage lorsque la composition est suffisante. Mais ces règles visent toutes à rendre le code maintenable à long terme face à l'évolution des règles et des exigences - ce qui n'est tout simplement pas le cas des échecs.

Avec les échecs, le chemin de maintenance à long terme le plus probable est que votre programme doit devenir plus intelligent et plus intelligent, ce qui signifie que les optimisations de vitesse et de stockage seront dominantes. Pour cela, vous devrez généralement faire des compromis qui sacrifient la lisibilité pour les performances, et donc même la meilleure conception OO finira par disparaître.

Erik Eidt
la source
Notez qu'il y a eu beaucoup de jeux réussis qui prennent un concept et ensuite jettent de nouvelles choses dans le mélange. Si vous souhaitez le faire à l'avenir avec votre jeu d'échecs, vous devez en fait prendre en compte d'éventuelles modifications futures.
Flater
2

Je commencerais par faire quelques observations sur le domaine du problème (c'est-à-dire les règles des échecs):

  • L'ensemble des mouvements valides pour un morceau dépend non seulement du type du morceau mais de l'état du plateau (le pion peut avancer si le carré est vide / peut se déplacer en diagonale s'il capture un morceau).
  • Certaines actions impliquent de retirer une pièce existante du jeu (promotion / capture d'une pièce) ou de déplacer plusieurs pièces (roque).

Celles-ci sont difficiles à modéliser en tant que responsabilité de la pièce elle-même, et ni l'héritage ni la composition ne se sentent bien ici. Il serait plus naturel de modéliser ces règles en tant que citoyens de première classe:

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRuleest une interface simple mais flexible qui permet aux implémentations de mettre à jour l'état de la carte de la manière requise pour prendre en charge des mouvements complexes tels que le roque et la promotion. Pour les échecs standard, vous auriez une implémentation pour chaque type de pièce, mais vous pouvez également brancher différentes règles dans une Gameinstance pour prendre en charge différentes variantes d'échecs.

casablanca
la source
Approche intéressante - Bonne matière à réflexion
CanISleepYet
1

Je pense que dans ce cas, l'héritage serait le choix le plus propre. La composition est peut-être un peu plus élégante, mais elle me semble un peu plus forcée.

Si vous envisagez de développer d'autres jeux qui utilisent des pièces mobiles qui se comportent différemment, la composition pourrait être un meilleur choix, surtout si vous avez utilisé un modèle d'usine pour générer les pièces nécessaires pour chaque jeu.

Kurt Haldeman
la source
2
Comment géreriez-vous la promotion par héritage?
TheCatWhisperer
4
@TheCatWhisperer Comment gérez-vous cela lorsque vous jouez physiquement au jeu? (J'espère que vous remplacez la pièce - vous ne re-sculptez pas le pion, n'est-ce pas!?)
svidgen
0

qui obéit certainement au principe "is-a"

Bon, vous utilisez donc l'héritage. Vous pouvez toujours composer au sein de votre classe Piece, mais au moins vous aurez une colonne vertébrale. La composition est plus flexible mais la flexibilité est surestimée, en général, et certainement dans ce cas parce que les chances que quelqu'un introduise un nouveau type de pièce qui vous obligerait à abandonner votre modèle rigide sont nulles. Le jeu d'échecs ne changera pas de sitôt. L'héritage vous donne un modèle de guidage, quelque chose à relier aussi, un code d'enseignement, une direction. La composition n'est pas tellement, les entités de domaine les plus importantes ne se démarquent pas, c'est sans spin, il faut comprendre le jeu pour comprendre le code.

Martin Maat
la source
merci d'avoir ajouté le fait qu'avec la composition, il est plus difficile de raisonner sur le code
CanISleepYet
0

Veuillez ne pas utiliser l'héritage ici. Alors que les autres réponses ont certainement de nombreux joyaux de sagesse, l'utilisation de l'héritage ici serait certainement une erreur. L'utilisation de la composition vous aide non seulement à résoudre les problèmes que vous n'anticipez pas, mais je vois déjà un problème ici que l'héritage ne peut pas gérer avec élégance.

La promotion, lorsqu'un pion est converti en une pièce plus précieuse, pourrait être un problème d'héritage. Techniquement, vous pouvez résoudre ce problème en remplaçant une pièce par une autre, mais cette approche a ses limites. L'une de ces limitations serait le suivi des statistiques par pièce où vous ne voudrez peut-être pas réinitialiser les informations sur la pièce lors de la promotion (ou écrire le code supplémentaire pour les copier).

Maintenant, en ce qui concerne votre conception de composition dans votre question, je pense que c'est inutilement compliqué. Je ne pense pas que vous ayez besoin d'un composant distinct pour chaque action et que vous puissiez vous en tenir à une classe par type de pièce. L'énumération de type peut également être inutile.

Je définirais une Piececlasse (comme vous l'avez déjà), ainsi qu'une PieceTypeclasse. Les PieceTypedéfinit la classe toutes les méthodes qui un pion, reine, ect doit définir, comme CanMoveTo, GetPieceTypeName, ect. Ensuite , les classes Pawnand Queen, ect hériteraient virtuellement de la PieceTypeclasse.

TheCatWhisperer
la source
3
Les problèmes que vous voyez avec la promotion et le remplacement sont triviaux, OMI. La séparation des préoccupations exige à peu près que ce "suivi par pièce" (ou autre) soit géré indépendamment de toute façon . ... Tous les avantages potentiels de votre solution sont éclipsés par les préoccupations imaginaires que vous essayez de résoudre, OMI.
svidgen
5
Pour clarifier: la solution proposée présente sans aucun doute des avantages. Mais, ils sont difficiles à trouver dans votre réponse, qui se concentre principalement sur des problèmes qui sont vraiment très faciles à résoudre dans la "solution d'héritage". ... Ce genre de choses porte atteinte (pour moi en tout cas) à tous les mérites que vous essayez de communiquer à propos de votre opinion.
svidgen
1
S'il Piecen'est pas virtuel, je suis d'accord avec cette solution. Bien que la conception de la classe puisse sembler un peu plus complexe à première vue, je pense que la mise en œuvre sera globalement plus simple. Les principales choses qui seraient associées à Pieceet non à l'est PieceTypesont son identité et potentiellement son histoire de mouvement.
JimmyJames
1
@svidgen Une implémentation qui utilise une pièce composée et une implémentation qui utilise une pièce basée sur l'héritage semblera fondamentalement identique à part les détails décrits dans cette solution. Cette solution de composition ajoute un seul niveau d'indirection. Je ne vois pas cela comme quelque chose à éviter.
JimmyJames
2
@JimmyJames À droite. Mais, l'historique des mouvements de plusieurs pièces doit être suivi et corrélé - pas quelque chose dont une seule pièce devrait être responsable, OMI. C'est une "préoccupation distincte" si vous aimez les choses SOLIDES.
svidgen
0

De mon point de vue, avec les informations fournies, il n'est pas judicieux de répondre si l'héritage ou la composition devrait être la voie à suivre.

  • L'héritage et la composition ne sont que des outils dans la ceinture d'outils du concepteur orienté objet.
  • Vous pouvez utiliser ces outils pour concevoir de nombreux modèles différents pour le domaine du problème.
  • Étant donné qu'il existe de nombreux modèles de ce type qui peuvent représenter un domaine, selon le point de vue du concepteur sur les exigences, vous pouvez combiner l'héritage et la composition de plusieurs manières pour proposer des modèles fonctionnellement équivalents.

Vos modèles sont totalement différents, dans le premier que vous avez modelé autour du concept de pièces d'échecs, dans le second autour du concept de mouvement des pièces. Ils pourraient être fonctionnellement équivalents, et je choisirais celui qui représente mieux le domaine et m'aide à le raisonner plus facilement.

De plus, dans votre deuxième design, le fait que votre Piececlasse ait un typechamp révèle clairement qu'il y a un problème de design ici car la pièce elle-même peut être de plusieurs types. Ne semble-t-il pas que cela devrait probablement utiliser une sorte d'héritage, où la pièce elle-même est le type?

Donc, vous voyez, il est très difficile de discuter de votre solution. Ce qui importe n'est pas de savoir si vous avez utilisé l'héritage ou la composition, mais si votre modèle reflète fidèlement le domaine du problème et s'il est utile de le raisonner et d'en fournir une implémentation.

Il faut une certaine expérience pour utiliser correctement l'héritage et la composition, mais ce sont des outils entièrement différents et je ne pense pas que l'on puisse «se substituer» l'un à l'autre, bien que je convienne qu'un système pourrait être conçu entièrement en utilisant uniquement la composition.

edalorzo
la source
Vous faites valoir que c'est le modèle qui est important et non l'outil. En ce qui concerne le type redondant, l'héritage aide-t-il vraiment? Par exemple, si je veux mettre en évidence tous les pions ou vérifier si une pièce était un roi, n'aurais-je pas besoin d'un casting?
CanISleepYet
-1

Eh bien, je ne suggère ni l'un ni l'autre.

Bien que l'orientation objet soit un bel outil, et aide souvent à simplifier les choses, ce n'est pas le seul outil dans la boîte à outils.

Envisagez plutôt d'implémenter une classe board, contenant un simple bytearray.
L'interpréter et calculer les choses à ce sujet se fait dans les fonctions membres en utilisant le masquage de bits et la commutation.

Utilisez le MSB pour le propriétaire, en laissant 7 bits pour la pièce, mais réservez zéro pour vide.
Alternativement, zéro pour vide, signe pour le propriétaire, valeur absolue pour la pièce.

Si vous le souhaitez, vous pouvez utiliser des champs de bits pour éviter le masquage manuel, bien qu'ils ne soient pas transférables:

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};
Déduplicateur
la source
Intéressant ... et apte étant donné que c'est une question C ++. Mais, n'étant pas programmeur C ++, ce ne serait pas mon premier (ou deuxième ou troisième ) instinct! ... C'est peut-être la réponse la plus C ++ ici!
svidgen
7
Il s'agit d'une microoptimisation qu'aucune application réelle ne devrait suivre.
Lie Ryan
@LieRyan Si tout ce que vous avez est OOP, tout sent comme un objet. Et si c'est vraiment ce "micro", c'est aussi une question intéressante. Selon ce que vous en faites , cela peut être assez important.
Déduplicateur
Heh ... cela dit , C ++ est orienté objet. Je suppose qu'il ya une raison pour laquelle l'OP utilise C ++ au lieu de simplement C .
svidgen
3
Ici, je suis moins préoccupé par le manque de POO, mais plutôt en obscurcissant le code avec un peu de twiddling. Sauf si vous écrivez du code pour Nintendo 8 bits ou si vous écrivez des algorithmes qui nécessitent de traiter des millions de copies de l'état de la carte, il s'agit d'une microoptimisation prématurée et déconseillée. Dans les scénarios normaux, il ne sera probablement pas plus rapide de toute façon car l'accès aux bits non alignés nécessite des opérations de bits supplémentaires.
Lie Ryan