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.
À 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: chaque 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 un 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 enfant de MovementComponent
également. Mais cette conception entraînerait très probablement une énorme instruction "switch(type)
" dans l'implémentation de GetValidMoveSquares
. Et c'est certainement une forte indication de l'héritage étant le meilleur choix ici .
Remarque dans la conception "héritage", il est assez facile de fournir le champ "type" de manière non redondante: ajoutez une méthode virtuelle GetType()
à BasePiece
et implémentez-la en conséquence dans chaque classe de base.
Concernant la "promotion": je suis ici avec @svidgen, je trouve les arguments présentés dans @ La réponse de TheCatWhisperer discutable.
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.
Je préférerais probablement préfère l'option la 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 de 1 à 1 entre les pièces et leur comportement/mouvement en plusieurs classes, vous êtes probablement uniquement ajouter 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 , IMO, consiste à créer une interface Piece
que vos pièces individuelles implémentent . Dans la plupart des langues, ceci est en fait 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.
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 besoins. 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 telles que 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 passer au bord du chemin).
Je commencerais par faire quelques observations sur le domaine du problème (c'est-à-dire les règles des échecs):
Ceux-ci sont difficiles à modéliser comme une 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;
}
MovementRule
est une interface simple mais flexible qui permet aux implémentations de mettre à jour l'état de la carte de toute manière nécessaire 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 instance Game
pour prendre en charge différentes variantes d'échecs.
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.
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, elle est sans spin, il faut comprendre le jeu pour comprendre le code.
Veuillez ne pas utiliser l'héritage ici. Bien que les autres réponses contiennent certainement de nombreux joyaux de sagesse, l'utilisation de l'héritage ici serait certainement une erreur. L'utilisation de composition vous aide non seulement à gérer 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 présente des 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 classe Piece
(comme vous l'avez déjà), ainsi qu'une classe PieceType
. La classe PieceType
définit toutes les méthodes qu'un pion, reine, ect doit définir, telles que CanMoveTo
, GetPieceTypeName
, ect. Ensuite, les classes Pawn
et Queen
, ect hériteraient virtuellement de la classe PieceType
.
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 modélisé 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 conception, le fait que votre classe Piece
ait un champ type
, révèle clairement qu'il y a un problème de conception 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.