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 Card
beziehe 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).
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). PartialCard
hat zwei zusätzliche Methoden: PartialCard otherPart()
und boolean isFirstPart()
.
Vertreter
Wenn ich ein Deck habe, sollte es aus WholeCard
s bestehen, nicht aus Card
s, wie es sein Card
könnte PartialCard
, und das würde keinen Sinn ergeben. Ich möchte also ein Objekt, das eine "physische Karte" darstellt, also etwas, das ein WholeCard
oder zwei PartialCard
s darstellen kann. Ich rufe diesen Typ vorläufig auf Representative
und Card
hätte die Methode getRepresentative()
. A Representative
wü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 getRepresentative
als 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 PartialCard
s.
Ich denke, diese Typhierarchie ist sinnvoll, aber kompliziert. Wenn wir uns Card
s als "konzeptuelle Karten" und Representative
s 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 PartialCard
s und WholeCards
beide Card
s sind und es normalerweise keinen guten Grund gibt, sie zu trennen, würde ich normalerweise nur damit arbeiten Collection<Card>
. Manchmal musste ich also PartialCard
s 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
, Representative
müsste entweder a WholeCard
oder gegossen werden Composite
, um auf die tatsächlichen Card
s 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.
quelle
Antworten:
Es scheint mir, dass Sie eine Klasse wie haben sollten
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.
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.
quelle
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
Card
Entität jedoch, N von jedem Attribut zu enthalten : Spielbarer Name, Manakosten, Typ, Seltenheit, Text, Effekte usw.Option 2. Nicht ganz anders als Option 1, modellieren Sie es nach der physischen Realität. Sie haben eine
Card
Entität, die eine physische Karte darstellt . Und es ist dann der Zweck, NPlayable
Dinge zu halten . DiesePlayable
können jeweils einen eigenen Namen, Manakosten, eine Liste von Effekten, eine Liste von Fähigkeiten usw. haben. Und Ihre "physischen"Card
können eine eigene Kennung (oder einen eigenen Namen) haben, die in etwa der Zusammensetzung des jeweiligenPlayable
Namens entspricht Die MTG-Datenbank scheint zu funktionieren.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.)
quelle
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.
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:
Card
Schnittstelle, wie Sie es bereits getan haben.ConcreteCard
,Card
die eine einfache Bildkarte implementiert und definiert. Zögern Sie nicht, das Verhalten einer normalen Karte in diese Klasse einzustufen.CompositeCard
, dasCard
zwei zusätzliche (und a priori private) implementiert und hatCard
. Nennen wir sieleftCard
undrightCard
.Die Eleganz des Ansatzes ist, dass a
CompositeCard
zwei Karten enthält, die selbst entweder ConcreteCard oder CompositeCard sein können. In Ihrem SpielleftCard
undrightCard
wird wahrscheinlich systematischConcreteCard
s 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.CompositeCard
mussCard
natü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 dieCompositeCard
Karte selbst. Beispielsweise möchten Sie möglicherweise die folgende Implementierung:Auf diese Weise können Sie a
CompositeCard
genauso verwenden wie für jedes andereCard
, und das spezifische Verhalten wird dank Polymorphismus ausgeblendet.Wenn Sie sicher sind, dass a
CompositeCard
immer zwei normaleCard
s enthält, können Sie die Idee beibehalten und einfachConcreateCard
als Typ fürleftCard
und verwendenrightCard
.quelle
Card
in verfügen . Ich empfehle auch dem OP, diese Lösung zu verwenden, Dekorateur ist der Weg zu gehen!CompositeCard
CompositeCard
keine zusätzlichen Methoden verfügbar sind,CompositeCard
ist nur ein Dekorateur.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.
quelle
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
Card
Schnittstelle:Möglicherweise gibt es ein wenig mehr Verhalten auf der
Card
Schnittstelle, aber die meisten Eigenschaften werden in eine neue Klasse verschobenCardProperties
:Jetzt können wir
SimpleCard
eine ganze Karte mit einem einzigen Satz von Eigenschaften darstellen lassen:Wir sehen, wie das
CardProperties
und das noch zu schreibende ineinanderCardVisitor
passen. Lassen Sie uns a tunCompoundCard
, um eine Karte mit zwei Gesichtern darzustellen:Das
CardVisitor
beginnt aufzutauchen. Versuchen wir, diese Schnittstelle jetzt zu schreiben:(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:
Die Laufzeit verwaltet den Versand an die richtige Version der
#visit
Methode durch Polymorphismus, anstatt zu versuchen, diese zu unterbrechen.Anstatt eine anonyme Klasse zu verwenden, können Sie die sogar
CardVisitor
zu 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
CardVisitor
Schnittstelle vornehmen . Es kann zum Beispiel vorkommen, dassCard
s 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
CardVisitor
in 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 einseitigenCard
s 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.quelle