Contexte
Voici le problème réel sur lequel je travaille: je veux un moyen de représenter les cartes dans le jeu de cartes Magic: The Gathering . La plupart des cartes du jeu sont des cartes d'apparence normale, mais certaines d'entre elles sont divisées en deux parties, chacune avec son propre nom. Chaque moitié de ces cartes en deux parties est traitée comme une carte elle-même. Donc, pour plus de clarté, je vais utiliser Card
uniquement pour faire référence à quelque chose qui est soit une carte ordinaire, soit la moitié d'une carte en deux parties (en d'autres termes, quelque chose avec un seul nom).
Nous avons donc un type de base, Card. Le but de ces objets est vraiment juste de conserver les propriétés de la carte. Ils ne font vraiment rien par eux-mêmes.
interface Card {
String name();
String text();
// etc
}
Il y a deux sous-classes de Card
, que j'appelle PartialCard
(la moitié d'une carte en deux parties) et WholeCard
(une carte ordinaire). PartialCard
a deux méthodes supplémentaires: PartialCard otherPart()
et boolean isFirstPart()
.
Représentants
Si j'ai un deck, il devrait être composé de WholeCard
s, pas de Card
s, comme Card
pourrait être un PartialCard
, et cela n'aurait aucun sens. Je veux donc un objet qui représente une "carte physique", c'est-à-dire quelque chose qui puisse représenter un WholeCard
ou deux PartialCard
s. J'appelle provisoirement ce type Representative
et Card
j'aurais la méthode getRepresentative()
. A Representative
ne fournirait presque aucune information directe sur la ou les cartes qu'il représente, il ne ferait que les pointer. Maintenant, mon idée brillante / folle / stupide (vous décidez) est que WholeCard hérite des deux Card
et Representative
. Après tout, ce sont des cartes qui se représentent! WholeCards pourrait implémenter en getRepresentative
tant que return this;
.
Quant à PartialCards
, ils ne se représentent pas eux-mêmes, mais ils ont un externe Representative
qui n'est pas un Card
, mais fournit des méthodes pour accéder aux deux PartialCard
s.
Je pense que cette hiérarchie de types est logique, mais c'est compliqué. Si nous considérons Card
s comme des "cartes conceptuelles" et Representative
s comme des "cartes physiques", eh bien, la plupart des cartes sont les deux! Je pense que vous pourriez faire valoir que les cartes physiques contiennent en fait des cartes conceptuelles et qu'elles ne sont pas la même chose , mais je dirais qu'elles le sont.
Nécessité de coulée de caractères
Parce que PartialCard
s et WholeCards
sont tous les deux Card
s, et qu'il n'y a généralement aucune bonne raison de les séparer, je travaillerais normalement avec Collection<Card>
. Donc, parfois, je devais lancer des PartialCard
s pour accéder à leurs méthodes supplémentaires. En ce moment, j'utilise le système décrit ici parce que je n'aime vraiment pas les transtypages explicites. Et comme Card
, Representative
aurait besoin d'être converti en un WholeCard
ou Composite
pour accéder aux Card
s réels qu'ils représentent.
Donc, juste pour le résumé:
- Type de base
Representative
- Type de base
Card
- Type
WholeCard extends Card, Representative
(aucun accès requis, il se représente lui-même) - Type
PartialCard extends Card
(donne accès à une autre partie) - Type
Composite extends Representative
(donne accès aux deux parties)
Est-ce fou? Je pense que cela a beaucoup de sens, mais je ne suis honnêtement pas sûr.
la source
Réponses:
Il me semble que vous devriez avoir une classe comme
Le code qui concerne la carte physique peut traiter la classe de carte physique, et le code qui concerne la carte logique peut gérer cela.
Peu importe que vous pensiez ou non que la carte physique et la carte logique sont la même chose. Ne présumez pas que, simplement parce qu'ils sont le même objet physique, ils devraient être le même objet dans votre code. Ce qui importe, c'est de savoir si l'adoption de ce modèle facilite la lecture et l'écriture du codeur. Le fait est que l'adoption d'un modèle plus simple où chaque carte physique est traitée comme une collection de cartes logiques de manière cohérente, 100% du temps, se traduira par un code plus simple.
la source
Pour être franc, je pense que la solution proposée est trop restrictive et trop déformée et décousue de la réalité physique est des modèles, avec peu d'avantages.
Je suggérerais l'une des deux alternatives:
Option 1. Traitez-la comme une seule carte, identifiée comme Half A // Half B , comme le site MTG répertorie Wear // Tear . Mais, permettez à votre
Card
entité de contenir N de chaque attribut: nom jouable, coût de mana, type, rareté, texte, effets, etc.Option 2. Pas très différent de l'option 1, modélisez-la d'après la réalité physique. Vous avez une
Card
entité qui représente une carte physique . Et, son but est alors de contenir NPlayable
choses. CeuxPlayable
-ci peuvent chacun avoir un nom distinct, un coût de mana, une liste d'effets, une liste de capacités, etc. Et votre "physique"Card
peut avoir son propre identifiant (ou nom) qui est un composé duPlayable
nom de chacun , un peu comme la base de données MTG semble faire.Je pense que l'une ou l'autre de ces options est assez proche de la réalité physique. Et, je pense que cela sera bénéfique pour quiconque regarde votre code. (Comme vous-même en 6 mois.)
la source
Cette phrase est un signe qu'il y a quelque chose de mal dans votre conception: dans la POO, chaque classe doit avoir exactement un rôle, et le manque de comportement révèle une classe de données potentielle , ce qui est une mauvaise odeur dans le code.
À mon humble avis, cela semble un peu étrange, et même un peu bizarre. Un objet de type "Carte" doit représenter une carte. Période.
Je ne sais rien de Magic: le rassemblement , mais je suppose que vous voulez utiliser vos cartes de la même manière, quelle que soit leur structure réelle: vous voulez afficher une représentation sous forme de chaîne, vous voulez calculer une valeur d'attaque, etc.
Pour le problème que vous décrivez, je recommanderais un modèle de conception composite , malgré le fait que ce DP est généralement présenté pour résoudre un problème plus général:
Card
interface, comme vous l'avez déjà fait.ConcreteCard
, qui implémenteCard
et définit une simple carte faciale. N'hésitez pas à mettre le comportement d'un normal carte dans cette classe.CompositeCard
, qui implémenteCard
et a deux s supplémentaires (et a priori privés)Card
. Appelons-lesleftCard
etrightCard
.L'élégance de l'approche est que a
CompositeCard
contient deux cartes, qui peuvent elles-mêmes être ConcreteCard ou CompositeCard. Dans votre jeu,leftCard
etrightCard
sera probablement systématiquementConcreteCard
s, mais le Design Pattern vous permet de concevoir des compositions de niveau supérieur gratuitement si vous le souhaitez. Votre manipulation de carte ne prendra pas en compte le type réel de vos cartes, et donc vous n'avez pas besoin de choses telles que le casting en sous-classe.CompositeCard
doit implémenter les méthodes spécifiées dansCard
, bien sûr, et le fera en tenant compte du fait qu'une telle carte est composée de 2 cartes (plus, si vous le souhaitez, quelque chose de spécifique à laCompositeCard
carte elle-même. Par exemple, vous pouvez souhaiter que le mise en œuvre suivante:Ce faisant, vous pouvez utiliser un
CompositeCard
exactement comme vous le faites pour toutCard
, et le comportement spécifique est masqué grâce au polymorphisme.Si vous êtes sûr qu'un
CompositeCard
contenu contiendra toujours deuxCard
s normaux , vous pouvez conserver l'idée et simplement l'utiliserConcreateCard
comme type pourleftCard
etrightCard
.la source
Card
enCompositeCard
vous implémentez le motif décorateur . Je recommande également à l'OP d'utiliser cette solution, le décorateur est le chemin à parcourir!CompositeCard
n'expose pas de méthodes supplémentaires,CompositeCard
n'est qu'un décorateur.Peut-être que tout est une carte quand elle est dans le deck ou le cimetière, et quand vous la jouez, vous construisez une créature, un terrain, un enchantement, etc. à partir d'un ou plusieurs objets carte, qui implémentent ou étendent tous jouables. Ensuite, un composite devient un jouable unique dont le constructeur prend deux cartes partielles, et une carte avec un kicker devient un jouable dont le constructeur prend un argument de mana. Le type reflète ce que vous pouvez en faire (dessiner, bloquer, dissiper, taper) et ce qui peut l'affecter. Ou un jouable est juste une carte qui doit être soigneusement restaurée (perdre les bonus et les compteurs, se séparer) lorsqu'elle est retirée du jeu, s'il est vraiment utile d'utiliser la même interface pour invoquer une carte et prédire ce qu'elle fera.
Peut-être que Card et Playable ont un effet.
la source
Le modèle de visiteur est une technique classique pour récupérer des informations de type caché. Nous pouvons l'utiliser (une légère variation ici) ici pour discerner entre les deux types même lorsqu'ils sont stockés dans des variables d'abstraction supérieure.
Commençons par cette abstraction plus élevée, une
Card
interface:Il peut y avoir un peu plus de comportement sur l'
Card
interface, mais la plupart des getters de propriété se déplacent vers une nouvelle classeCardProperties
:Maintenant, nous pouvons avoir un
SimpleCard
représentant une carte entière avec un seul ensemble de propriétés:Nous voyons comment le
CardProperties
et le reste à écrireCardVisitor
commencent à s’intégrer. Faisons unCompoundCard
pour représenter une carte à deux faces:Les
CardVisitor
débuts émergent. Essayons d'écrire cette interface maintenant:(Il s'agit d'une première version de l'interface pour l'instant. Nous pouvons apporter des améliorations, qui seront discutées plus tard.)
Nous avons maintenant étoffé toutes les parties. Il nous suffit maintenant de les rassembler:
Le runtime gérera la répartition vers la version correcte de la
#visit
méthode par polymorphisme plutôt que d'essayer de la casser.Plutôt que d'utiliser une classe anonyme, vous pouvez même promouvoir l'
CardVisitor
élément en classe interne ou même en classe complète si le comportement est réutilisable ou si vous souhaitez pouvoir échanger le comportement lors de l'exécution.Nous pouvons utiliser les classes telles qu'elles sont actuellement, mais nous pouvons apporter quelques améliorations à l'
CardVisitor
interface. Par exemple, il peut arriver un moment oùCard
s peut avoir trois, quatre ou cinq faces. Plutôt que d'ajouter de nouvelles méthodes à implémenter, nous pourrions simplement avoir la deuxième méthode take et array au lieu de deux paramètres. Cela a du sens si les cartes à faces multiples sont traitées différemment, mais le nombre de faces au-dessus d'une est traité de la même manière.Nous pourrions également convertir
CardVisitor
en une classe abstraite au lieu d'une interface, et avoir des implémentations vides pour toutes les méthodes. Cela nous permet de mettre en œuvre uniquement les comportements qui nous intéressent (peut-être que nous ne sommes intéressés que par les faces simplesCard
). Nous pouvons également ajouter de nouvelles méthodes sans forcer chaque classe existante à implémenter ces méthodes ou à ne pas compiler.la source