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.
la source
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.Réponses:
À 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:
(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
Piece
contient 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
Piece
devrait fournir une "source unique de vérité", pas deux sources.Bien sûr, vous pouvez essayer de stocker les informations de type uniquement dans
type
et ne créer aucune classe enfantMovementComponent
. Mais cette conception conduirait très probablement à une énorme "switch(type)
" déclaration dans la mise en oeuvre deGetValidMoveSquares
. 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()
pourBasePiece
et 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.
la source
movementComponent
. Voir ma modification comment fournir un champ de type de manière sûre dans la conception "héritage".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.la source
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.
la source
Je commencerais par faire quelques observations sur le domaine du problème (c'est-à-dire les règles des échecs):
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:
MovementRule
est 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 uneGame
instance pour prendre en charge différentes variantes d'échecs.la source
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.
la source
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.
la source
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
Piece
classe (comme vous l'avez déjà), ainsi qu'unePieceType
classe. LesPieceType
définit la classe toutes les méthodes qui un pion, reine, ect doit définir, commeCanMoveTo
,GetPieceTypeName
, ect. Ensuite , les classesPawn
andQueen
, ect hériteraient virtuellement de laPieceType
classe.la source
Piece
n'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 àPiece
et non à l'estPieceType
sont son identité et potentiellement son histoire de mouvement.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.
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
Piece
classe ait untype
champ 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.
la source
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:
la source