Comment créer un type de données pour quelque chose qui se représente lui-même ou deux autres choses

16

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 Carduniquement 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).

cartes

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). PartialCarda deux méthodes supplémentaires: PartialCard otherPart()et boolean isFirstPart().

Représentants

Si j'ai un deck, il devrait être composé de WholeCards, pas de Cards, comme Cardpourrait ê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 WholeCardou deux PartialCards. J'appelle provisoirement ce type Representativeet Cardj'aurais la méthode getRepresentative(). A Representativene 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 getRepresentativetant que return this;.

Quant à PartialCards, ils ne se représentent pas eux-mêmes, mais ils ont un externe Representativequi n'est pas un Card, mais fournit des méthodes pour accéder aux deux PartialCards.

Je pense que cette hiérarchie de types est logique, mais c'est compliqué. Si nous considérons Cards comme des "cartes conceptuelles" et Representatives 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 PartialCards et WholeCardssont tous les deux Cards, 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 PartialCards 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, Representativeaurait besoin d'être converti en un WholeCardou Compositepour accéder aux Cards 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.

décrypteur
la source
1
Les cartes partielles sont-elles effectivement des cartes autres que deux par carte physique? L'ordre de lecture est-il important? Pourriez-vous non seulement créer une classe "Deck Slot" et "WholeCard" et autoriser DeckSlots à avoir plusieurs WholeCards, puis faire quelque chose comme DeckSlot.WholeCards.Play et s'il y a 1 ou 2, jouez les deux comme s'ils étaient séparés cartes? Il semble, étant donné votre conception beaucoup plus compliquée, qu'il doit y avoir quelque chose qui me manque :)
enderland
Je n'ai vraiment pas l'impression de pouvoir utiliser le polymorphisme entre des cartes entières et des cartes partielles pour résoudre ce problème. Oui, si je voulais une fonction "jouer", je pourrais l'implémenter facilement, mais le problème est que les cartes partielles et les cartes entières ont des ensembles d'attributs différents. J'ai vraiment besoin de pouvoir voir ces objets comme ce qu'ils sont, pas seulement comme leur classe de base. À moins qu'il n'y ait une meilleure solution à ce problème. Mais j'ai trouvé que le polymorphisme ne se mélange pas bien avec les types qui partagent une classe de base commune, mais ont des méthodes différentes entre eux, si cela a du sens.
codebreaker
1
Honnêtement, je pense que c'est ridiculement compliqué. Vous semblez ne modéliser que les relations "est un", ce qui conduit à une conception très rigide avec un couplage serré. Plus intéressant serait ce que ces cartes font réellement?
Daniel Jour
2
S'ils ne sont "que des objets de données", alors mon instinct serait d'avoir une classe qui contient un tableau d'objets d'une deuxième classe, et de s'en tenir à cela; aucun héritage ou autres complications inutiles. Je ne sais pas si ces deux classes devraient être PlayableCard et DrawableCard ou WholeCard et CardPart, car je ne sais pas assez comment fonctionne votre jeu, mais je suis sûr que vous pouvez penser à quelque chose pour les appeler.
Ixrec
1
Quels types de propriétés détiennent-ils? Pouvez-vous donner des exemples d'actions utilisant ces propriétés?
Daniel Jour

Réponses:

14

Il me semble que vous devriez avoir une classe comme

class PhysicalCard {
    List<LogicalCard> getLogicalCards();
}

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.

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.

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.

Winston Ewert
la source
2
A voté parce que c'est la meilleure réponse, mais pas pour la raison donnée. C'est la meilleure réponse non pas parce qu'elle est simple, mais parce que les cartes physiques et les cartes conceptuelles ne sont pas la même chose, et cette solution modélise correctement leur relation. Il est vrai qu'un bon modèle de POO ne reflète pas toujours la réalité physique, mais il devrait toujours refléter la réalité conceptuelle. La simplicité est bonne, bien sûr, mais elle prend un siège arrière à l'exactitude conceptuelle.
Kevin Krumwiede
2
@KevinKrumwiede, selon moi, il n'y a pas une seule réalité conceptuelle. Différentes personnes pensent à la même chose de différentes manières, et les gens peuvent changer leur façon de penser. Vous pouvez considérer les cartes physiques et logiques comme des entités distinctes. Ou vous pouvez considérer les cartes partagées comme une sorte d'exception à la notion générale de carte. Ni l'un ni l'autre n'est intrinsèquement incorrect, mais celui-ci se prête à une modélisation plus simple.
Winston Ewert du
8

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 Cardentité de contenir N de chaque attribut: nom jouable, coût de mana, type, rareté, texte, effets, etc.

interface Card {
  List<String> Names();
  List<ManaCost> Costs();
  List<CardTypes> Types();
  /* 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 Cardentité qui représente une carte physique . Et, son but est alors de contenir N Playable choses. Ceux Playable-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" Cardpeut avoir son propre identifiant (ou nom) qui est un composé du Playablenom de chacun , un peu comme la base de données MTG semble faire.

interface Card {
  String Name();
  List<Playable> Playables();
}

interface Playable {
  String Name();
  ManaCost Cost();
  CardType Type();
  /* etc. */
}

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.)

svidgen
la source
5

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.

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.

Après tout, ce sont des cartes qui se représentent!

À 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:

  1. Créer un Card interface, comme vous l'avez déjà fait.
  2. Créez un ConcreteCard, qui implémente Cardet définit une simple carte faciale. N'hésitez pas à mettre le comportement d'un normal carte dans cette classe.
  3. Créez un CompositeCard, qui implémente Cardet a deux s supplémentaires (et a priori privés) Card. Appelons-les leftCardet rightCard.

L'élégance de l'approche est que a CompositeCardcontient deux cartes, qui peuvent elles-mêmes être ConcreteCard ou CompositeCard. Dans votre jeu, leftCardet rightCardsera probablement systématiquement ConcreteCards, 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.

CompositeCarddoit implémenter les méthodes spécifiées dans Card, 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 à la CompositeCardcarte elle-même. Par exemple, vous pouvez souhaiter que le mise en œuvre suivante:

public class CompositeCard implements Card
{ 
   private final Card leftCard, rightCard;
   private final double factor;

   @Override // Defined in Card
   public double attack(Player p){
      return factor * (leftCard.attack(p) + rightCard.attack(p));
   }

   @Override // idem
   public String name()
   {
       return leftCard.name() + " combined with " + rightCard.name();
   }

   ...
}

Ce faisant, vous pouvez utiliser un CompositeCard exactement comme vous le faites pour tout Card, et le comportement spécifique est masqué grâce au polymorphisme.

Si vous êtes sûr qu'un CompositeCardcontenu contiendra toujours deux Cards normaux , vous pouvez conserver l'idée et simplement l'utiliser ConcreateCardcomme type pour leftCardet rightCard.

mgoeminne
la source
Vous parlez du motif composite , mais en fait , puisque vous tenez deux références Carden CompositeCardvous implémentez le motif décorateur . Je recommande également à l'OP d'utiliser cette solution, le décorateur est le chemin à parcourir!
Repéré le
Je ne vois pas pourquoi avoir deux instances de carte fait de ma classe un décorateur. Selon votre propre lien, un décorateur ajoute des fonctionnalités à un seul objet et est lui-même une instance de la même classe / interface que cet objet. Alors que, selon votre autre lien, un composite permet la possession de plusieurs objets de la même classe / interface. Mais finalement les mots n'ont pas d'importance, et seule l'idée est géniale.
mgoeminne
@Spotted Ce n'est certainement pas le motif de décoration car le terme est utilisé en Java. Le modèle de décorateur est implémenté en remplaçant les méthodes sur un objet individuel, créant une classe anonyme qui est unique à cet objet.
Kevin Krumwiede
@KevinKrumwiede tant qu'il CompositeCardn'expose pas de méthodes supplémentaires, CompositeCardn'est qu'un décorateur.
Repéré le
"... Design Pattern vous permet de concevoir des compositions de niveau supérieur gratuitement" - non, il ne vient pas gratuitement , bien au contraire - il vient pour le prix d'avoir une solution qui est plus compliquée que nécessaire pour les exigences.
Doc Brown
3

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.

Davislor
la source
Malheureusement, je ne joue pas ces cartes. Ce ne sont vraiment que des objets de données qui peuvent être interrogés. Si vous voulez une structure de données de deck, vous voulez une List <WholeCard> ou quelque chose. Si vous souhaitez effectuer une recherche de cartes instantanées vertes, vous souhaitez une liste <Card>.
codebreaker
Ah ok. Pas très pertinent pour votre problème, alors. Dois-je supprimer?
Davislor
3

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 Cardinterface:

public interface Card {
    public void accept(CardVisitor visitor);
}

Il peut y avoir un peu plus de comportement sur l' Cardinterface, mais la plupart des getters de propriété se déplacent vers une nouvelle classe CardProperties:

public class CardProperties {
    // property methods, constructors, etc.

    String name();
    String text();
    // ...
}

Maintenant, nous pouvons avoir un SimpleCardreprésentant une carte entière avec un seul ensemble de propriétés:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

    @Override
    public void accept(CardVisitor visitor) {
        visitor.visit(properties);
    }
}

Nous voyons comment le CardPropertieset le reste à écrire CardVisitorcommencent à s’intégrer. Faisons un CompoundCardpour représenter une carte à deux faces:

public class CompoundCard implements Card {
    private CardProperties firstFaceProperties;
    private CardProperties secondFaceProperties;

    // Constructors, ...

    public void accept(CardVisitor visitor) {
        visitor.visit(firstFaceProperties, secondFaceProperties);
    }
}

Les CardVisitordébuts émergent. Essayons d'écrire cette interface maintenant:

public interface CardVisitor {
    public void visit(CardProperties properties);
    public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties);
}

(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:

List<Card> cards = new LinkedList<>();
cards.add(new SimpleCard(new CardProperties(/* ... */)));
cards.add(new CompoundCard(new CardProperties(/* ... */), new CardProperties(/* ... */)));

 for(Card card : cards) {
     card.accept(new CardVisitor() {
         @Override
         public void visit(CardProperties properties) {
             // Do something for simple cards with a single face
         }

         public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties) {
             // Do something else for compound cards with two faces
         }
     });
 }

Le runtime gérera la répartition vers la version correcte de la #visitmé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' CardVisitorinterface. 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 CardVisitoren 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 simples Card). Nous pouvons également ajouter de nouvelles méthodes sans forcer chaque classe existante à implémenter ces méthodes ou à ne pas compiler.

cbojar
la source
1
Je pense que cela pourrait être facilement étendu à l'autre type de carte à deux faces (recto et verso au lieu de côte à côte). ++
RubberDuck
Pour une exploration plus approfondie, l'utilisation d'un Visitor pour exploiter des méthodes spécifiques à une sous-classe est parfois appelée Multiple Dispatch. Double Dispatch pourrait être une solution intéressante au problème.
mgoeminne
1
Je vote contre parce que je pense que le modèle de visiteur ajoute une complexité et un couplage inutiles au code. Puisqu'une alternative est disponible (voir la réponse de mgoeminne), je ne l'utiliserais pas.
Repéré le
@Spotted Le visiteur est un motif complexe, et j'ai également pris en compte le motif composite lors de la rédaction de la réponse. La raison pour laquelle je suis allé avec le visiteur est que le PO souhaite traiter des choses similaires différemment, plutôt que des choses différentes de manière similaire. Si les cartes avaient plus de comportement et étaient utilisées pour le gameplay, le modèle composite permet de combiner des données pour produire une statistique unifiée. Ce ne sont que des sacs de données, cependant, éventuellement utilisés pour rendre un affichage de carte, auquel cas obtenir des informations séparées semblait plus utile qu'un simple agrégateur composite.
cbojar