Wie man einen Datentyp für etwas erstellt, das entweder sich selbst oder zwei andere Dinge repräsentiert

16

Hintergrund

Hier ist das eigentliche Problem, an dem ich arbeite: Ich möchte eine Möglichkeit, Karten im Kartenspiel Magic: The Gathering darzustellen . Die meisten Karten im Spiel sind normal aussehende Karten, aber einige von ihnen sind in zwei Teile unterteilt, von denen jeder seinen eigenen Namen hat. Jede Hälfte dieser zweiteiligen Karten wird als Karte behandelt. Aus Gründen der Klarheit Cardbeziehe ich mich nur auf eine Karte, die entweder eine normale Karte oder eine Hälfte einer zweiteiligen Karte ist (mit anderen Worten, etwas mit nur einem Namen).

Karten

Wir haben also einen Basistyp, Card. Der Zweck dieser Objekte besteht eigentlich nur darin, die Eigenschaften der Karte zu speichern. Sie machen nichts wirklich alleine.

interface Card {
    String name();
    String text();
    // etc
}

Es gibt zwei Unterklassen Card, die ich nenne PartialCard(eine Hälfte einer zweiteiligen Karte) und WholeCard(eine reguläre Karte). PartialCardhat zwei zusätzliche Methoden: PartialCard otherPart()und boolean isFirstPart().

Vertreter

Wenn ich ein Deck habe, sollte es aus WholeCards bestehen, nicht aus Cards, wie es sein Cardkönnte PartialCard, und das würde keinen Sinn ergeben. Ich möchte also ein Objekt, das eine "physische Karte" darstellt, also etwas, das ein WholeCardoder zwei PartialCards darstellen kann. Ich rufe diesen Typ vorläufig auf Representativeund Cardhätte die Methode getRepresentative(). A Representativewürde fast keine direkten Informationen über die Karte (n) liefern, die es darstellt, es würde nur auf sie zeigen. Nun, meine geniale / verrückte / dumme Idee (Sie entscheiden) ist, dass WholeCard von beiden Card und erbt Representative. Immerhin sind sie Karten, die sich selbst darstellen! WholeCards könnten getRepresentativeals implementieren return this;.

Was PartialCards, Sie repräsentieren sich nicht, aber sie haben eine externe Representative, auf dem eine nicht Card, sondern stellt Methoden für den Zugriff auf die beiden PartialCards.

Ich denke, diese Typhierarchie ist sinnvoll, aber kompliziert. Wenn wir uns Cards als "konzeptuelle Karten" und Representatives als "physische Karten" vorstellen, dann sind die meisten Karten beides! Ich denke, Sie könnten argumentieren, dass physische Karten tatsächlich konzeptionelle Karten enthalten und dass sie nicht dasselbe sind , aber ich würde argumentieren, dass sie es sind.

Notwendigkeit des Schriftgusses

Weil PartialCards und WholeCardsbeide Cards sind und es normalerweise keinen guten Grund gibt, sie zu trennen, würde ich normalerweise nur damit arbeiten Collection<Card>. Manchmal musste ich also PartialCards besetzen, um auf ihre zusätzlichen Methoden zugreifen zu können. Im Moment benutze ich das hier beschriebene System, weil ich explizite Casts nicht mag. Und wie Card, Representativemüsste entweder a WholeCardoder gegossen werden Composite, um auf die tatsächlichen Cards zuzugreifen, die sie darstellen.

Also nur zur Zusammenfassung:

  • Basistyp Representative
  • Basistyp Card
  • Typ WholeCard extends Card, Representative(kein Zugriff erforderlich, er repräsentiert sich selbst)
  • Typ PartialCard extends Card(ermöglicht den Zugriff auf andere Teile)
  • Typ Composite extends Representative(ermöglicht den Zugriff auf beide Teile)

Ist das verrückt? Ich denke, es macht tatsächlich viel Sinn, aber ich bin mir ehrlich gesagt nicht sicher.

Codeknacker
quelle
1
Sind PartialCards andere Karten als zwei Karten pro physischer Karte? Ist die Reihenfolge, in der sie gespielt werden, wichtig? Könnten Sie nicht einfach eine "Deck Slot" - und "WholeCard" -Klasse erstellen und DeckSlots erlauben, mehrere WholeCards zu haben, und dann einfach so etwas wie DeckSlot.WholeCards.Play ausführen und wenn es 1 oder 2 gibt, beide spielen, als wären sie getrennt Karten?
Angesichts
Ich habe wirklich nicht das Gefühl, dass ich Polymorphismus zwischen ganzen Karten und Teilkarten verwenden kann, um dieses Problem zu lösen. Ja, wenn ich eine "Spiel" -Funktion wollte, dann könnte ich das leicht implementieren, aber das Problem ist, dass Teilkarten und ganze Karten unterschiedliche Sätze von Attributen haben. Ich muss wirklich in der Lage sein, diese Objekte als das zu betrachten, was sie sind, und nicht nur als ihre Basisklasse. Es sei denn, es gibt eine bessere Lösung für dieses Problem. Aber ich habe festgestellt, dass sich Polymorphismus nicht gut mit Typen verträgt, die eine gemeinsame Basisklasse haben, aber unterschiedliche Methoden verwenden, wenn dies sinnvoll ist.
Codebreaker
1
Ehrlich gesagt finde ich das lächerlich kompliziert. Sie scheinen nur "is a" -Beziehungen zu modellieren, was zu einem sehr starren Design mit enger Kopplung führt. Interessanter wäre, was diese Karten tatsächlich tun?
Daniel,
2
Wenn sie "nur Datenobjekte" sind, würde mein Instinkt darin bestehen, eine Klasse zu haben, die ein Array von Objekten einer zweiten Klasse enthält, und es dabei zu belassen; keine Vererbung oder andere sinnlose Komplikationen. Ob es sich bei diesen beiden Klassen um PlayableCard und DrawableCard oder WholeCard und CardPart handeln soll, weiß ich nicht, da ich nicht annähernd genug über die Funktionsweise Ihres Spiels weiß.
Ixrec
1
Welche Art von Immobilien halten sie? Können Sie Beispiele für Aktionen nennen, die diese Eigenschaften verwenden?
Daniel Jour

Antworten:

14

Es scheint mir, dass Sie eine Klasse wie haben sollten

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

Code, der sich mit der physischen Karte befasst, kann sich mit der physischen Kartenklasse befassen, und Code, der sich mit der logischen Karte befasst, kann sich damit befassen.

Ich denke, Sie könnten argumentieren, dass physische Karten tatsächlich konzeptionelle Karten enthalten und dass sie nicht dasselbe sind, aber ich würde argumentieren, dass sie es sind.

Es spielt keine Rolle, ob Sie der Meinung sind, dass die physische und die logische Karte dasselbe sind. Gehen Sie nicht davon aus, dass sie dasselbe Objekt in Ihrem Code sein sollten, nur weil sie dasselbe physische Objekt sind. Entscheidend ist, ob die Übernahme dieses Modells das Lesen und Schreiben des Codierers erleichtert. Tatsache ist, dass die Annahme eines einfacheren Modells, bei dem jede physische Karte zu 100% als Sammlung logischer Karten behandelt wird, zu einem einfacheren Code führt.

Winston Ewert
quelle
2
Aufgestimmt, weil dies die beste Antwort ist, wenn auch nicht aus dem angegebenen Grund. Dies ist die beste Antwort, nicht weil es einfach ist, sondern weil physische Karten und konzeptuelle Karten nicht dasselbe sind und diese Lösung ihre Beziehung korrekt modelliert. Es ist wahr, dass ein gutes OOP-Modell nicht immer die physische Realität widerspiegelt, sondern immer die konzeptionelle Realität widerspiegeln sollte. Einfachheit ist natürlich gut, aber konzeptionelle Korrektheit tritt in den Hintergrund.
Kevin Krumwiede
2
@ KevinKrumwiede, wie ich es sehe, gibt es keine einzige konzeptionelle Realität. Unterschiedliche Menschen denken auf unterschiedliche Weise über das Gleiche, und die Menschen können ihre Meinung ändern. Sie können sich physische und logische Karten als separate Einheiten vorstellen. Oder Sie können sich die geteilten Karten als eine Art Ausnahme vom allgemeinen Begriff einer Karte vorstellen. Beides ist an sich nicht falsch, aber das eine bietet sich für eine einfachere Modellierung an.
Winston Ewert
8

Um ehrlich zu sein, denke ich, dass die vorgeschlagene Lösung zu restriktiv und zu verzerrt und von der physischen Realität abgesetzt ist, sind Modelle mit wenig Vorteil.

Ich würde eine von zwei Alternativen vorschlagen:

Option 1. Behandeln Sie die Karte als Einzelkarte, gekennzeichnet als Half A // Half B , wie auf der MTG-Site Wear // Tear aufgelistet . Erlaube deiner CardEntität jedoch, N von jedem Attribut zu enthalten : Spielbarer Name, Manakosten, Typ, Seltenheit, Text, Effekte usw.

interface Card {
  List<String> Names();
  List<ManaCost> Costs();
  List<CardTypes> Types();
  /* etc. */
}

Option 2. Nicht ganz anders als Option 1, modellieren Sie es nach der physischen Realität. Sie haben eine CardEntität, die eine physische Karte darstellt . Und es ist dann der Zweck, N Playable Dinge zu halten . Diese Playablekönnen jeweils einen eigenen Namen, Manakosten, eine Liste von Effekten, eine Liste von Fähigkeiten usw. haben. Und Ihre "physischen" Cardkönnen eine eigene Kennung (oder einen eigenen Namen) haben, die in etwa der Zusammensetzung des jeweiligen PlayableNamens entspricht Die MTG-Datenbank scheint zu funktionieren.

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

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

Ich denke, dass eine dieser Optionen der physischen Realität ziemlich nahe kommt. Und ich denke, das ist für jeden von Vorteil, der sich Ihren Code ansieht. (Wie Sie selbst in 6 Monaten.)

Svidgen
quelle
5

Der Zweck dieser Objekte besteht eigentlich nur darin, die Eigenschaften der Karte zu speichern. Sie machen nichts wirklich alleine.

Dieser Satz ist ein Zeichen dafür, dass in Ihrem Entwurf etwas nicht stimmt: In OOP sollte jede Klasse genau eine Rolle haben, und das fehlende Verhalten zeigt eine potenzielle Datenklasse , die im Code schlecht riecht.

Immerhin sind sie Karten, die sich selbst darstellen!

IMHO, es klingt ein bisschen seltsam und sogar ein bisschen komisch. Ein Objekt vom Typ "Karte" sollte eine Karte darstellen. Zeitraum.

Ich weiß nichts über Magie: Das Sammeln , aber ich denke, Sie möchten Ihre Karten auf ähnliche Weise verwenden, unabhängig von ihrer tatsächlichen Struktur: Sie möchten eine Zeichenfolgendarstellung anzeigen, Sie möchten einen Angriffswert berechnen usw.

Für das von Ihnen beschriebene Problem würde ich ein Composit-Entwurfsmuster empfehlen , obwohl dieses DP normalerweise zur Lösung eines allgemeineren Problems dargestellt wird:

  1. Erstellen Sie eine CardSchnittstelle, wie Sie es bereits getan haben.
  2. Erstellen Sie eine ConcreteCard, Carddie eine einfache Bildkarte implementiert und definiert. Zögern Sie nicht, das Verhalten einer normalen Karte in diese Klasse einzustufen.
  3. Erstellen Sie ein CompositeCard, das Cardzwei zusätzliche (und a priori private) implementiert und hat Card. Nennen wir sie leftCardund rightCard.

Die Eleganz des Ansatzes ist, dass a CompositeCardzwei Karten enthält, die selbst entweder ConcreteCard oder CompositeCard sein können. In Ihrem Spiel leftCardund rightCardwird wahrscheinlich systematisch ConcreteCards sein, aber das Design Pattern ermöglicht es Ihnen, kostenlos Kompositionen höherer Ebenen zu entwerfen, wenn Sie möchten. Bei Ihrer Kartenmanipulation wird der tatsächliche Kartentyp nicht berücksichtigt, weshalb Sie keine Funktionen wie das Casting für die Unterklasse benötigen.

CompositeCardmuss Cardnatürlich die in angegebenen Methoden implementieren und wird dies tun, indem berücksichtigt wird, dass eine solche Karte aus 2 Karten besteht (plus, wenn Sie möchten, etwas Spezifisches für die CompositeCardKarte selbst. Beispielsweise möchten Sie möglicherweise die folgende Implementierung:

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();
   }

   ...
}

Auf diese Weise können Sie a CompositeCardgenauso verwenden wie für jedes andere Card, und das spezifische Verhalten wird dank Polymorphismus ausgeblendet.

Wenn Sie sicher sind, dass a CompositeCardimmer zwei normale Cards enthält, können Sie die Idee beibehalten und einfach ConcreateCardals Typ für leftCardund verwenden rightCard.

mgoeminne
quelle
Sie sprechen über das zusammengesetzte Muster , aber tatsächlich implementieren Sie das Dekorationsmuster , da Sie über zwei Referenzen von Cardin verfügen . Ich empfehle auch dem OP, diese Lösung zu verwenden, Dekorateur ist der Weg zu gehen! CompositeCard
Gepunktet
Ich verstehe nicht, warum zwei Karteninstanzen meine Klasse zum Dekorateur machen. Gemäß Ihrem eigenen Link fügt ein Dekorateur einem einzelnen Objekt Features hinzu und ist selbst eine Instanz derselben Klasse / Schnittstelle wie dieses Objekt. Laut Ihrem anderen Link erlaubt ein Composite den Besitz mehrerer Objekte derselben Klasse / Schnittstelle. Aber letztendlich spielen die Worte keine Rolle und nur die Idee ist großartig.
mgoeminne
@Spotted Dies ist definitiv nicht das Dekorationsmuster, da der Begriff in Java verwendet wird. Das Dekorationsmuster wird durch Überschreiben von Methoden für ein einzelnes Objekt implementiert, wodurch eine anonyme Klasse erstellt wird, die für dieses Objekt eindeutig ist.
Kevin Krumwiede
@ KevinKrumwiede, solange CompositeCardkeine zusätzlichen Methoden verfügbar sind, CompositeCardist nur ein Dekorateur.
Gepunktet
"Mit ... Design Pattern können Sie kostenlos Kompositionen auf höherer Ebene entwerfen" - nein, es ist nicht kostenlos , im Gegenteil - es kostet eine Lösung, die komplizierter ist als für die Anforderungen erforderlich.
Doc Brown
3

Vielleicht ist alles eine Karte, wenn sie sich im Deck oder auf dem Friedhof befindet, und wenn du sie spielst, baust du eine Kreatur, ein Land, eine Verzauberung usw. aus einem oder mehreren Kartenobjekten, die alle spielbar sind oder diese erweitern. Dann wird eine Komposition zu einer einzelnen Spielbar, deren Konstruktor zwei Teilkarten erhält, und eine Karte mit einem Kicker wird zu einer Spielbar, deren Konstruktor ein Mana-Argument erhält. Der Typ gibt an, was Sie damit tun können (zeichnen, blockieren, zerstreuen, tippen) und was sich darauf auswirken kann. Oder eine spielbare Karte ist nur eine Karte, die sorgfältig zurückgesetzt werden muss (Boni und Zählmarken verlieren, aufgeteilt werden), wenn sie nicht mehr im Spiel ist, wenn es wirklich nützlich ist, dieselbe Schnittstelle zu verwenden, um eine Karte aufzurufen und vorherzusagen, was sie tut.

Möglicherweise haben Karte und spielbares einen Effekt.

Davislor
quelle
Leider spiele ich diese Karten nicht. Es sind wirklich nur Datenobjekte, die abgefragt werden können. Wenn Sie eine Deck-Datenstruktur möchten, möchten Sie eine Liste <WholeCard> oder so etwas. Wenn Sie nach grünen Sofortkarten suchen möchten, benötigen Sie eine Liste <Karte>.
Codebreaker
Ah, okay. Für Ihr Problem also nicht sehr relevant. Soll ich löschen?
Davislor
3

Das Besuchermuster ist eine klassische Technik zum Wiederherstellen verborgener Typinformationen. Wir können es hier verwenden (eine kleine Variation davon), um zwischen den beiden Typen zu unterscheiden, selbst wenn sie in Variablen mit höherer Abstraktion gespeichert sind.

Beginnen wir mit dieser höheren Abstraktion, einer CardSchnittstelle:

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

Möglicherweise gibt es ein wenig mehr Verhalten auf der CardSchnittstelle, aber die meisten Eigenschaften werden in eine neue Klasse verschoben CardProperties:

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

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

Jetzt können wir SimpleCardeine ganze Karte mit einem einzigen Satz von Eigenschaften darstellen lassen:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

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

Wir sehen, wie das CardPropertiesund das noch zu schreibende ineinander CardVisitorpassen. Lassen Sie uns a tun CompoundCard, um eine Karte mit zwei Gesichtern darzustellen:

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

    // Constructors, ...

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

Das CardVisitorbeginnt aufzutauchen. Versuchen wir, diese Schnittstelle jetzt zu schreiben:

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

(Dies ist eine erste Version der Benutzeroberfläche. Wir können Verbesserungen vornehmen, die später erläutert werden.)

Wir haben jetzt alle Teile ausgearbeitet. Jetzt müssen wir sie nur noch zusammenfügen:

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
         }
     });
 }

Die Laufzeit verwaltet den Versand an die richtige Version der #visitMethode durch Polymorphismus, anstatt zu versuchen, diese zu unterbrechen.

Anstatt eine anonyme Klasse zu verwenden, können Sie die sogar CardVisitorzu einer inneren Klasse oder sogar zu einer vollständigen Klasse heraufstufen, wenn das Verhalten wiederverwendbar ist oder wenn Sie das Verhalten zur Laufzeit austauschen möchten.


Wir können die Klassen so verwenden, wie sie jetzt sind, aber wir können einige Verbesserungen an der CardVisitorSchnittstelle vornehmen . Es kann zum Beispiel vorkommen, dass Cards drei, vier oder fünf Gesichter haben kann. Anstatt neue Methoden zur Implementierung hinzuzufügen, können wir statt zwei Parametern auch nur die zweite Methode take und array verwenden. Dies ist sinnvoll, wenn Karten mit mehreren Gesichtern unterschiedlich behandelt werden, die Anzahl der darüber liegenden Gesichter jedoch alle gleich behandelt wird.

Wir könnten auch CardVisitorin eine abstrakte Klasse anstelle einer Schnittstelle konvertieren und leere Implementierungen für alle Methoden haben. Dadurch können wir nur die Verhaltensweisen implementieren, an denen wir interessiert sind (vielleicht sind wir nur an einseitigen Cards interessiert ). Wir können auch neue Methoden hinzufügen, ohne dass jede vorhandene Klasse gezwungen wird, diese Methoden zu implementieren, oder die Kompilierung fehlschlägt.

cbojar
quelle
1
Ich denke, dies könnte leicht auf die andere Art von doppelseitiger Karte ausgeweitet werden (Vorderseite und Rückseite statt Seite an Seite). ++
RubberDuck
Zur weiteren Untersuchung wird die Verwendung eines Besuchers zum Ausnutzen von Methoden, die für eine Unterklasse spezifisch sind, manchmal als Mehrfachversand bezeichnet. Double Dispatch könnte eine interessante Lösung für das Problem sein.
Mgoeminne
1
Ich stimme ab, weil ich denke, dass das Besuchermuster dem Code unnötige Komplexität und Kopplung hinzufügt. Da es eine Alternative gibt (siehe Antwort von mgoeminne), würde ich sie nicht verwenden.
Gesichtet
@Spotted Der Besucher ist ein komplexes Muster, und ich habe beim Schreiben der Antwort auch das zusammengesetzte Muster berücksichtigt. Der Grund, warum ich mit dem Besucher gegangen bin, ist, dass das OP ähnliche Dinge anders behandeln möchte als unterschiedliche Dinge. Wenn die Karten mehr Verhalten aufweisen und für das Spiel verwendet werden, können die Daten mithilfe des zusammengesetzten Musters kombiniert werden, um eine einheitliche Statistik zu erstellen. Es handelt sich jedoch nur um Datenmengen, die möglicherweise zur Darstellung einer Kartenanzeige verwendet werden. In diesem Fall schien es sinnvoller, getrennte Informationen zu erhalten als ein einfacher zusammengesetzter Aggregator.
Cbojar