Zum Beispiel habe ich ein Spiel, das einige Werkzeuge hat, um die Fähigkeit des Spielers zu erhöhen:
Tool.h
class Tool{
public:
std::string name;
};
Und einige Tools:
Schwert.h
class Sword : public Tool{
public:
Sword(){
this->name="Sword";
}
int attack;
};
Shield.h
class Shield : public Tool{
public:
Shield(){
this->name="Shield";
}
int defense;
};
MagicCloth.h
class MagicCloth : public Tool{
public:
MagicCloth(){
this->name="MagicCloth";
}
int attack;
int defense;
};
Und dann kann ein Spieler einige Werkzeuge für den Angriff halten:
class Player{
public:
int attack;
int defense;
vector<Tool*> tools;
void attack(){
//original attack and defense
int currentAttack=this->attack;
int currentDefense=this->defense;
//calculate attack and defense affected by tools
for(Tool* tool : tools){
if(tool->name=="Sword"){
Sword* sword=(Sword*)tool;
currentAttack+=sword->attack;
}else if(tool->name=="Shield"){
Shield* shield=(Shield*)tool;
currentDefense+=shield->defense;
}else if(tool->name=="MagicCloth"){
MagicCloth* magicCloth=(MagicCloth*)tool;
currentAttack+=magicCloth->attack;
currentDefense+=magicCloth->shield;
}
}
//some other functions to start attack
}
};
Ich denke, es ist schwierig, es durch if-else
virtuelle Methoden in den Werkzeugen zu ersetzen , da jedes Werkzeug unterschiedliche Eigenschaften hat und sich auf den Angriff und die Verteidigung des Spielers auswirkt, wofür die Aktualisierung des Angriffs und der Verteidigung des Spielers im Player-Objekt durchgeführt werden muss.
Aber ich war mit diesem Entwurf nicht zufrieden, da er mit einer langen if-else
Aussage downcasting enthält . Muss dieses Design "korrigiert" werden? Wenn ja, was kann ich tun, um das Problem zu beheben?
design-patterns
c++
generics
ggrr
quelle
quelle
Antworten:
Ja, es ist ein Codegeruch (in vielen Fällen).
In Ihrem Beispiel ist es ganz einfach, das if / else durch virtuelle Methoden zu ersetzen:
Jetzt brauchen Sie Ihren
if
Block nicht mehr , der Anrufer kann ihn einfach so verwendenNatürlich ist eine solche Lösung für kompliziertere Situationen nicht immer so offensichtlich (aber dennoch fast jederzeit möglich). Wenn Sie jedoch nicht wissen, wie Sie den Fall mit virtuellen Methoden lösen können, können Sie hier unter "Programmierer" (oder, wenn es sprach- oder implementierungsspezifisch wird, unter Stackoverflow) erneut eine Frage stellen.
quelle
Sword
dieser Art nicht einmal in Ihrer Codebasis benötigen . Sie könnten einfachnew Tool("sword", swordAttack, swordDefense)
aus zB einer JSON-Datei.Tool
mit allen möglichen Modifikatorenvector<Tool*>
erstellen, einige mit Material füllen, das aus einer Datendatei gelesen wurde, dann einfach eine Schleife über sie machen und die Statistiken ändern, wie Sie es jetzt tun. Sie würden in Schwierigkeiten geraten, wenn Sie möchten, dass ein Gegenstand zB einen 10% igen Angriffsbonus erhält. Vielleicht ist a einetool->modify(playerStats)
andere Option.Das Hauptproblem bei Ihrem Code besteht darin, dass Sie bei jeder Einführung eines neuen Artikels nicht nur den Code des Artikels schreiben und aktualisieren müssen, sondern auch Ihren Player (oder den Ort, an dem der Artikel verwendet wird) modifizieren müssen, was das Ganze zu einem macht viel komplizierter.
Als allgemeine Faustregel halte ich es immer für faul, wenn man sich nicht auf normale Unterklassen / Vererbung verlassen kann und das Upcasting selbst durchführen muss.
Ich könnte mir zwei mögliche Ansätze vorstellen, um das Ganze flexibler zu machen:
Verschieben Sie, wie bereits erwähnt, die Elemente
attack
unddefense
in die Basisklasse und initialisieren Sie sie dort0
. Dies kann auch als Kontrolle dienen, ob Sie den Gegenstand tatsächlich für einen Angriff schwingen oder ihn zum Blockieren von Angriffen verwenden können.Erstellen Sie eine Art Rückruf- / Ereignissystem. Hierfür gibt es verschiedene mögliche Ansätze.
Wie wäre es einfach zu halten?
virtual void onEquip(Owner*) {}
und erstellenvirtual void onUnequip(Owner*) {}
.virtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }
undvirtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }
.Verglichen damit, nur die Angriffs- / Verteidigungswerte rechtzeitig anzufordern, wird das Ganze dadurch nicht nur dynamischer, es erspart Ihnen auch unnötige Anrufe und Sie können sogar Elemente erstellen, die Ihren Charakter dauerhaft beeinflussen.
Stellen Sie sich zum Beispiel einen verfluchten Ring vor, der nur einen versteckten Wert setzt, sobald er ausgerüstet ist, und Ihren Charakter dauerhaft als verflucht markiert.
quelle
Obwohl @DocBrown eine gute Antwort gegeben hat, geht es nicht weit genug, imho. Bevor Sie mit der Bewertung der Antworten beginnen, sollten Sie Ihre Anforderungen bewerten. Was brauchst du wirklich ?
Im Folgenden zeige ich zwei mögliche Lösungen, die unterschiedliche Vorteile für unterschiedliche Bedürfnisse bieten.
Das erste ist sehr simpel und speziell auf das zugeschnitten, was Sie gezeigt haben:
Dies ermöglicht eine sehr einfache Serialisierung / Deserialisierung von Tools (z. B. zum Speichern oder Vernetzen) und erfordert überhaupt keinen virtuellen Versand. Wenn Ihr Code alles ist, was Sie gezeigt haben, und Sie nicht erwarten, dass er sich wesentlich weiterentwickelt, außer dass es mehr verschiedene Tools mit unterschiedlichen Namen und Statistiken gibt, nur in unterschiedlichen Mengen, dann ist dies der richtige Weg.
@DocBrown hat eine Lösung angeboten, die sich immer noch auf den virtuellen Versand stützt. Dies kann von Vorteil sein, wenn Sie die Tools auf Teile Ihres Codes spezialisieren, die nicht angezeigt wurden. Wenn Sie jedoch wirklich ein anderes Verhalten benötigen oder ändern möchten, würde ich die folgende Lösung vorschlagen:
Zusammensetzung über Vererbung
Was ist, wenn Sie später ein Tool benötigen, das die Beweglichkeit ändert ? Oder Geschwindigkeit laufen ? Mir scheint, Sie machen ein Rollenspiel. Eine Sache, die für RPGs wichtig ist, ist die Möglichkeit zur Verlängerung . Die bisher gezeigten Lösungen bieten das nicht. Sie müssten die
Tool
Klasse jedes Mal ändern und neue virtuelle Methoden hinzufügen, wenn Sie ein neues Attribut benötigen.Die zweite Lösung, die ich zeige, ist die, auf die ich bereits in einem Kommentar hingewiesen habe. Sie verwendet Komposition anstelle von Vererbung und folgt dem Prinzip "Zur Änderung geschlossen, zur Erweiterung offen *" wird vertraut aussehen (ich stelle mir die Komposition gerne als den kleineren Bruder von ES vor).
Beachten Sie, dass das, was ich unten zeige, in Sprachen mit Laufzeitinformationen wie Java oder C # deutlich eleganter ist. Daher muss der C ++ - Code, den ich zeige, eine "Buchhaltung" enthalten, die einfach notwendig ist, damit die Komposition genau hier funktioniert. Vielleicht kann jemand mit mehr C ++ - Erfahrung einen noch besseren Ansatz vorschlagen.
Zunächst schauen wir uns noch einmal die Seite des Anrufers an . In Ihrem Beispiel
attack
interessieren Sie als Aufrufer in der Methode überhaupt nicht für Tools. Was Sie interessiert, sind zwei Eigenschaften - Angriffs- und Verteidigungspunkte. Es ist Ihnen eigentlich egal, woher diese kommen, und Sie interessieren sich nicht für andere Eigenschaften (z. B. Laufgeschwindigkeit, Beweglichkeit).Als erstes führen wir eine neue Klasse ein
Und dann erstellen wir unsere ersten beiden Komponenten
Anschließend lassen wir ein Tool eine Reihe von Eigenschaften enthalten und die Eigenschaften für andere abfragbar machen.
Beachten Sie, dass in diesem Beispiel nur eine Komponente für jeden Typ unterstützt wird. Dies erleichtert die Arbeit. Theoretisch könnte man auch mehrere Komponenten desselben Typs zulassen, aber das wird sehr schnell hässlich. Ein wichtiger Aspekt:
Tool
ist jetzt für Änderungen geschlossen - wir werden nie wieder die Quelle berührenTool
- aber offen für Erweiterungen - wir können das Verhalten eines Tools erweitern, indem wir andere Dinge modifizieren und einfach andere Komponenten in das Tool übertragen.Jetzt brauchen wir eine Möglichkeit, Werkzeuge nach Komponententypen abzurufen. Sie können immer noch einen Vektor für Werkzeuge verwenden, genau wie in Ihrem Codebeispiel:
Sie können dies auch in Ihre eigene
Inventory
Klasse umgestalten und Nachschlagetabellen speichern, die das Abrufen von Tools nach Komponententyp erheblich vereinfachen und das wiederholte Durchlaufen der gesamten Sammlung vermeiden.Welche Vorteile hat dieser Ansatz? In
attack
, Sie verarbeiten Tools , die zwei Komponenten haben - Sie kümmern sich nicht um irgendetwas anderes.Stellen wir uns vor, Sie haben eine
walkTo
Methode, und jetzt entscheiden Sie, dass es eine gute Idee ist, wenn ein Werkzeug die Fähigkeit erhält, Ihre Gehgeschwindigkeit zu ändern. Kein Problem!Erstellen Sie zuerst das neue
Component
:Anschließend fügen Sie dem Tool, mit dem Sie die Weckgeschwindigkeit erhöhen möchten, einfach eine Instanz dieser Komponente hinzu und ändern die
WalkTo
Methode, um die soeben erstellte Komponente zu verarbeiten:Beachten Sie, dass wir unseren Tools ein gewisses Verhalten hinzugefügt haben, ohne die Tools-Klasse zu ändern.
Sie können (und sollten) die Zeichenfolgen in ein Makro oder eine statische const-Variable verschieben, damit Sie sie nicht immer wieder eingeben müssen.
Wenn Sie diesen Ansatz weiter verfolgen, z. B. Komponenten erstellen, die dem Spieler hinzugefügt werden können, und eine
Combat
Komponente erstellen, die den Spieler als kampffähig kennzeichnet, können Sie auch dieattack
Methode loswerden und diese behandeln lassen durch die Komponente oder an anderer Stelle verarbeitet werden.Der Vorteil, dass der Spieler auch Komponenten erhalten kann, ist, dass Sie dann nicht einmal den Spieler wechseln müssen, um ihm ein anderes Verhalten zu verleihen. In meinem Beispiel könnten Sie eine
Movable
Komponente erstellen , so dass Sie diewalkTo
Methode nicht auf dem Player implementieren müssen , um ihn in Bewegung zu setzen. Sie erstellen einfach die Komponente, hängen sie an den Player an und lassen sie von einer anderen Person verarbeiten.Ein vollständiges Beispiel finden Sie in dieser Übersicht: https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20
Diese Lösung ist offensichtlich etwas komplexer als die anderen, die veröffentlicht wurden. Aber je nachdem, wie flexibel Sie sein möchten, wie weit Sie gehen möchten, kann dies ein sehr wirkungsvoller Ansatz sein.
Bearbeiten
Einige andere Antworten schlagen eine direkte Vererbung vor (Schwerter verlängern lassen, Schild verlängern lassen). Ich denke nicht, dass dies ein Szenario ist, in dem die Vererbung sehr gut funktioniert. Was ist, wenn Sie entscheiden, dass das Blockieren mit einem Schild auf eine bestimmte Weise auch den Angreifer schädigen kann? Mit meiner Lösung können Sie einfach eine Angriffskomponente zu einem Schild hinzufügen und dies ohne Änderungen an Ihrem Code feststellen. Mit der Vererbung hätten Sie ein Problem. Gegenstände / Werkzeuge in RPGs sind von Anfang an Hauptkandidaten für die Komposition oder sogar für die direkte Verwendung von Entitätssystemen.
quelle
Im Allgemeinen
if
ist es ein Zeichen dafür, dass etwas stinkendes vor sich geht , wenn Sie jemals die Notwendigkeit haben, in einer beliebigen OOP-Sprache (in Kombination mit der Anforderung des Typs einer Instanz) zu verwenden. Zumindest sollten Sie sich Ihre Modelle genauer ansehen.Ich würde Ihre Domain anders modellieren.
Für Ihren Anwendungsfall
Tool
hat a einAttackBonus
und einDefenseBonus
- was beides sein könnte,0
falls es für Kämpfe wie Federn oder ähnliches unbrauchbar ist.Für einen Angriff hast du dein
baserate
+bonus
von der Waffe benutzt. Das gleiche gilt für die Verteidigungbaserate
+bonus
.Infolgedessen
Tool
müssen Sie einevirtual
Methode zur Berechnung der Angriffs- / Verteidigungsboni haben.tl; dr
Mit einem besseren Design könnten Sie hacky
if
s vermeiden .quelle
if
weniger Programmierung. Meist in Kombinationen wie ifinstanceof
oder so. Aber es gibt eine Position, von der behauptetif
wird, sie sei ein Codesmell, und es gibt Möglichkeiten, sie zu umgehen. Und Sie haben Recht, das ist ein wesentlicher Operator, der sein eigenes Recht hat.Wie geschrieben "riecht" es, aber das könnten nur die Beispiele sein, die Sie gegeben haben. Das Speichern von Daten in generischen Objektcontainern und das anschließende Umwandeln, um Zugriff auf die Daten zu erhalten, ist kein automatischer Codegeruch. Sie werden sehen, dass es in vielen Situationen verwendet wird. Wenn Sie es jedoch verwenden, sollten Sie wissen, was Sie tun, wie Sie es tun und warum. Wenn ich mir das Beispiel ansehe, wird anhand von auf Zeichenfolgen basierenden Vergleichen festgestellt, welches Objekt das ist, was meinen persönlichen Geruchsmesser auslöst. Es deutet darauf hin, dass Sie nicht ganz sicher sind, was Sie hier tun (was in Ordnung ist, da Sie die Weisheit hatten, hierher zu kommen, um Programmierern zu helfen mich raus! ").
Das grundlegende Problem beim Umwandeln von Daten aus generischen Containern wie diesem ist, dass der Produzent der Daten und der Konsument der Daten zusammenarbeiten müssen, aber es ist möglicherweise nicht offensichtlich, dass dies auf den ersten Blick der Fall ist. In jedem Beispiel dieses Musters, ob es stinkt oder nicht, ist dies das grundlegende Problem. Es ist sehr wahrscheinlich, dass der nächste Entwickler nicht weiß, dass Sie dieses Muster ausführen, und es versehentlich abbricht. Wenn Sie dieses Muster verwenden, müssen Sie darauf achten, dem nächsten Entwickler zu helfen. Sie müssen es ihm leichter machen, den Code nicht ungewollt zu knacken, da er möglicherweise nicht genau weiß, dass er existiert.
Was wäre zum Beispiel, wenn ich einen Player kopieren wollte? Wenn ich mir nur den Inhalt des Player-Objekts ansehe, sieht es ziemlich einfach aus. Ich habe zu kopieren nur die
attack
,defense
undtools
Variablen. Einfach wie Torte! Nun, ich werde schnell herausfinden, dass Ihre Verwendung von Zeigern es etwas schwieriger macht (irgendwann lohnt es sich, sich intelligente Zeiger anzusehen, aber das ist ein anderes Thema). Das ist leicht zu lösen. Ich erstelle einfach neue Kopien von jedem Tool und füge diese in meine neuetools
Liste ein. ImmerhinTool
ist eine wirklich einfache Klasse mit nur einem Mitglied. Also erstelle ich ein paar Kopien, einschließlich einer Kopie derSword
, aber ich wusste nicht, dass es ein Schwert ist, also habe ich nur die kopiertname
. Später betrachtet dieattack()
Funktion den Namen, stellt fest, dass es sich um ein "Schwert" handelt, wirft es und es passieren schlimme Dinge!Wir können diesen Fall mit einem anderen Fall in der Socket-Programmierung vergleichen, der dasselbe Muster verwendet. Ich kann eine UNIX-Socket-Funktion wie folgt einrichten:
Warum ist das das gleiche Muster? Weil
bind
es kein akzeptiertsockaddr_in*
, akzeptiert es ein generischeressockaddr*
. Wenn Sie sich die Definitionen für diese Klassen ansehen, sehen wir, dasssockaddr
nur ein Mitglied der Familie, der wirsin_family
* zugewiesen haben, vorhanden ist . Die Familie sagt, zu welchem Subtyp du die Casts machen solltestsockaddr
.AF_INET
sagt dir, dass die Adressstruktur tatsächlich a istsockaddr_in
. In diesemAF_INET6
Fall wäre die Adresse eine Adressesockaddr_in6
mit größeren Feldern, um die größeren IPv6-Adressen zu unterstützen.Dies ist identisch mit Ihrem
Tool
Beispiel, außer dass eine Ganzzahl verwendet wird, um die Familie anzugeben, und nicht einstd::string
. Ich behaupte jedoch, dass es nicht riecht und versuche es aus anderen Gründen als "es ist eine Standardmethode, Sockets zu machen, damit es nicht" riecht ". Offensichtlich ist es dasselbe Muster, das ist Warum ich behaupte, dass das Speichern von Daten in generischen Objekten und das Umwandeln von Daten nicht automatisch nach Code riecht, aber es gibt einige Unterschiede in der Vorgehensweise, die die Sicherheit erhöhen.Bei Verwendung dieses Musters besteht die wichtigste Information darin, die Übermittlung von Informationen über die Unterklasse vom Produzenten zum Verbraucher zu erfassen. Dies tun Sie mit dem
name
Feld und UNIX-Sockets mit ihremsin_family
Feld. Dieses Feld ist die Information, die der Verbraucher benötigt, um zu verstehen, was der Produzent tatsächlich erstellt hat. In allen Fällen dieses Musters sollte es sich um eine Aufzählung handeln (oder zumindest um eine Ganzzahl, die wie eine Aufzählung wirkt). Warum? Überlegen Sie, was Ihr Verbraucher mit den Informationen tun wird. Sie müssen eine großeif
Erklärung oder eine Erklärung geschrieben habenswitch
Anweisung, wie Sie es getan haben, wo sie den richtigen Untertyp bestimmen, ihn umwandeln und die Daten verwenden. Per Definition kann es nur eine kleine Anzahl dieser Typen geben. Sie können es in einer Zeichenfolge speichern, wie Sie es getan haben, aber das hat zahlreiche Nachteile:std::string
muss normalerweise einen dynamischen Speicher ausführen, um die Zeichenfolge beizubehalten. Sie müssen auch einen Volltextvergleich durchführen, um den Namen jedes Mal abzugleichen, wenn Sie herausfinden möchten, welche Unterklasse Sie haben.MagicC1oth
. Im Ernst, bei solchen Fehlern kann es Tage dauern, bis Sie feststellen, was passiert ist.Eine Aufzählung funktioniert viel besser. Es ist schnell, günstig und weitaus weniger fehleranfällig:
Dieses Beispiel zeigt auch eine
switch
Aussage, die die Aufzählungen mit einbezieht, mit dem wichtigsten Teil dieses Musters: einemdefault
Fall, der wirft. Sie sollten niemals in diese Situation geraten, wenn Sie die Dinge perfekt machen. Wenn jedoch jemand einen neuen Tooltyp hinzufügt und Sie vergessen, den Code zu aktualisieren, um ihn zu unterstützen, möchten Sie, dass der Fehler behoben wird. Tatsächlich empfehle ich sie so sehr, dass Sie sie hinzufügen sollten, selbst wenn Sie sie nicht benötigen.Der andere große Vorteil von
enum
ist, dass dem nächsten Entwickler eine vollständige Liste der gültigen Werkzeugtypen zur Verfügung steht. Es ist nicht nötig, den Code zu durchforsten, um Bobs spezielle Flötenklasse zu finden, die er in seinem epischen Bosskampf verwendet.Ja, ich habe eine "leere" Standardanweisung eingefügt, um dem nächsten Entwickler klar zu machen, was zu erwarten ist, wenn ein neuer unerwarteter Typ auf mich zukommt.
Wenn Sie dies tun, riecht das Muster weniger. Um jedoch geruchsfrei zu sein, müssen Sie als letztes die anderen Optionen in Betracht ziehen. Diese Casts sind einige der mächtigsten und gefährlichsten Werkzeuge, die Sie im C ++ - Repertoire haben. Sie sollten sie nicht verwenden, es sei denn, Sie haben einen guten Grund.
Eine sehr beliebte Alternative ist das, was ich als "Gewerkschaftsstruktur" oder "Gewerkschaftsklasse" bezeichne. Für Ihr Beispiel wäre dies tatsächlich eine sehr gute Passform. Um eine davon zu erstellen, erstellen Sie eine
Tool
Klasse mit einer Aufzählung wie zuvor, aber statt einer UnterklasseTool
werden nur alle Felder von jedem Untertyp darauf platziert.Jetzt brauchen Sie überhaupt keine Unterklassen mehr. Sie müssen sich nur das
type
Feld ansehen, um zu sehen, welche anderen Felder tatsächlich gültig sind. Dies ist viel sicherer und leichter zu verstehen. Es hat jedoch Nachteile. Es gibt Zeiten, in denen Sie dies nicht verwenden möchten:Diese Lösung wird von UNIX-Sockets aufgrund des Unähnlichkeitsproblems, das durch die offene Endlichkeit der API verursacht wird, nicht verwendet. Mit UNIX-Sockets sollte etwas geschaffen werden, mit dem jede UNIX-Variante arbeiten kann. Jede Variante könnte die Liste der Familien definieren, die sie unterstützen
AF_INET
, und es würde für jede eine kurze Liste geben. Wenn jedoch ein neues ProtokollAF_INET6
hinzukommt, müssen Sie möglicherweise neue Felder hinzufügen. Wenn Sie dies mit einer Unionsstruktur tun würden, würden Sie effektiv eine neue Version der Struktur mit demselben Namen erstellen und endlose Inkompatibilitätsprobleme verursachen. Aus diesem Grund haben sich die UNIX-Sockets dafür entschieden, das Casting-Muster anstelle einer Union-Struktur zu verwenden. Ich bin sicher, sie haben darüber nachgedacht, und die Tatsache, dass sie darüber nachgedacht haben, ist ein Teil dessen, warum es nicht riecht, wenn sie es benutzen.Sie könnten auch eine Union für real verwenden. Gewerkschaften sparen Speicher, indem sie nur so groß sind wie das größte Mitglied, aber sie haben ihre eigenen Probleme. Dies ist wahrscheinlich keine Option für Ihren Code, aber es ist immer eine Option, die Sie in Betracht ziehen sollten.
Eine andere interessante Lösung ist
boost::variant
. Boost ist eine großartige Bibliothek mit wiederverwendbaren plattformübergreifenden Lösungen. Es ist wahrscheinlich einer der besten C ++ - Codes, die jemals geschrieben wurden. Boost.Variant ist im Grunde die C ++ - Version von Gewerkschaften. Es ist ein Container, der viele verschiedene Typen enthalten kann, aber jeweils nur einen. Sie könnten Ihre machenSword
,Shield
undMagicCloth
Klassen, dann Werkzeug sein , machtboost::variant<Sword, Shield, MagicCloth>
es einen dieser drei Typen enthält Sinn. Dies hat immer noch dasselbe Problem mit der zukünftigen Kompatibilität, das die Verwendung von UNIX-Sockets verhindert (ganz zu schweigen davon, dass UNIX-Sockets älter als C sind)boost
von ziemlich viel!), aber dieses Muster kann unglaublich nützlich sein. Variant wird beispielsweise häufig in Analysebäumen verwendet, die eine Zeichenfolge von Text verwenden und diese anhand einer Grammatik für Regeln auflösen.Die endgültige Lösung, die ich vor dem Eintauchen und Verwenden des generischen Objektguss-Ansatzes empfehlen würde, ist das Besucher- Entwurfsmuster. Visitor ist ein leistungsfähiges Entwurfsmuster, das die Beobachtung nutzt, dass das Aufrufen einer virtuellen Funktion das von Ihnen benötigte Casting effektiv und für Sie erledigt. Weil der Compiler es tut, kann es niemals falsch sein. Anstatt eine Aufzählung zu speichern, verwendet Visitor daher eine abstrakte Basisklasse mit einer vtable, die den Typ des Objekts kennt. Wir erstellen dann einen hübschen kleinen Aufruf mit doppelter Indirektion, der die Arbeit erledigt:
Also, wie sieht dieses gottesfürchtige Muster aus? Nun,
Tool
hat eine virtuelle Funktionaccept
. Wenn Sie es einem Besucher übergeben, wird erwartet, dass es sich umdreht und die richtigevisit
Funktion für diesen Besucher für den Typ aufruft. Dies ist, was dasvisitor.visit(*this);
für jeden Untertyp tut. Kompliziert, aber wir können dies mit Ihrem obigen Beispiel zeigen:Also, was passiert hier? Wir erschaffen einen Besucher, der für uns etwas Arbeit leistet, sobald er weiß, welche Art von Objekt er besucht. Anschließend durchlaufen wir die Liste der Tools. Nehmen wir an, das erste Objekt ist ein
Shield
, aber unser Code weiß das noch nicht. Es ruftt->accept(v)
eine virtuelle Funktion auf. Da das erste Objekt ein Schild ist,void Shield::accept(ToolVisitor& visitor)
ruft es am Ende , was ruftvisitor.visit(*this);
. Wenn wir uns nun ansehen, wasvisit
wir anrufen sollen, wissen wir bereits, dass wir einen Schild haben (weil diese Funktion aufgerufen wurde), also werden wir am Endevoid ToolVisitor::visit(Shield* shield)
unseren anrufenAttackVisitor
. Dies führt nun den richtigen Code aus, um unsere Verteidigung zu aktualisieren.Besucher ist sperrig. Es ist so klobig, dass ich fast denke, es hat einen eigenen Geruch. Es ist sehr einfach, schlechte Besuchermuster zu schreiben. Es hat jedoch einen großen Vorteil, den keiner der anderen hat. Wenn wir einen neuen Werkzeugtyp hinzufügen, müssen wir dafür eine neue
ToolVisitor::visit
Funktion hinzufügen . In dem Moment, in dem wir dies tun, wird sich jederToolVisitor
im Programm weigern, zu kompilieren, weil ihm eine virtuelle Funktion fehlt. Dies macht es sehr einfach, alle Fälle zu erfassen, in denen wir etwas verpasst haben. Es ist viel schwieriger zu garantieren , dass , wenn Sie verwendenif
oderswitch
Aussagen , die Arbeit zu tun. Diese Vorteile sind gut genug, dass Visitor in Generatoren für 3D-Grafikszenen eine nette kleine Nische gefunden hat. Sie brauchen genau die Art von Verhalten, die der Besucher anbietet, damit es großartig funktioniert!Denken Sie daran, dass diese Muster es dem nächsten Entwickler schwer machen. Nehmen Sie sich Zeit, um es ihnen leichter zu machen, und der Code riecht nicht!
* Technisch gesehen hat sockaddr ein Mitglied namens
sa_family
. Hier auf der C-Ebene gibt es einige knifflige Aufgaben, die für uns keine Rolle spielen. Sie können sich gerne die tatsächliche Implementierung ansehen , aber für diesesa_family
sin_family
und andere Antworten werde ich sie vollständig austauschen, wobei Sie die für die Prosa intuitivste verwenden und darauf vertrauen, dass dieser C-Trick die unwichtigen Details berücksichtigt.quelle
Im Allgemeinen vermeide ich es, mehrere Klassen zu implementieren / zu erben, wenn es nur um die Kommunikation von Daten geht. Sie können sich an eine einzelne Klasse halten und von dort aus alles implementieren. Für Ihr Beispiel ist das genug
Wahrscheinlich erwarten Sie, dass Ihr Spiel mehrere Arten von Schwertern usw. implementiert, aber Sie haben andere Möglichkeiten, dies zu implementieren. Klassenexplosion ist selten die beste Architektur. Halte es einfach.
quelle
Wie bereits erwähnt, ist dies ein schwerwiegender Codegeruch. Man könnte jedoch davon ausgehen, dass die Ursache für Ihr Problem in der Verwendung der Vererbung anstelle der Komposition in Ihrem Entwurf liegt.
Angenommen, Sie haben uns drei Konzepte gezeigt:
Beachten Sie, dass Ihre vierte Klasse nur eine Kombination der letzten beiden Konzepte ist. Deshalb würde ich vorschlagen, dafür Komposition zu verwenden.
Sie benötigen eine Datenstruktur, um die für einen Angriff erforderlichen Informationen darzustellen. Und Sie benötigen eine Datenstruktur, die die Informationen darstellt, die Sie für die Verteidigung benötigen. Zuletzt benötigen Sie eine Datenstruktur, um Dinge darzustellen, die eine oder beide der folgenden Eigenschaften aufweisen können:
quelle
Attack
undDefense
kompliziert werden, ohne die Schnittstelle zu ändernTool
.Tool
vollständig schließen, um sie zu ändern, während Sie sie weiterhin zur Erweiterung öffnen lassen.Tool
ohne es zu ändern . Und wenn ich das Recht habe, es zu ändern, sehe ich keine Notwendigkeit für beliebige Komponenten.Warum nicht abstrakte Methoden
modifyAttack
undmodifyDefense
in derTool
Klasse erstellen ? Dann würde jedes Kind seine eigene Implementierung haben, und Sie nennen das elegant:Wenn Sie Werte als Referenz übergeben, werden Ressourcen gespart, wenn Sie:
quelle
Wenn man Polymorphismus verwendet, ist es immer am besten, wenn sich der gesamte Code, der sich um die verwendete Klasse kümmert, in der Klasse selbst befindet. So würde ich es codieren:
Dies hat folgende Vorteile:
quelle
Ich denke, eine Möglichkeit, die Fehler in diesem Ansatz zu erkennen, besteht darin, Ihre Idee zu ihrer logischen Schlussfolgerung zu entwickeln.
Das sieht aus wie ein Spiel, also werden Sie sich wahrscheinlich irgendwann Sorgen um die Leistung machen und diese Zeichenfolgenvergleiche gegen ein
int
oder austauschenenum
. Wenn die Liste der Elemente länger wird, wird diesif-else
ziemlich unhandlich. Sie können daher in Erwägung ziehen, sie in eine neue Version zu konvertierenswitch-case
. Sie haben an dieser Stelle auch eine ziemliche Textwand, sodass Sie die Aktion in jedercase
Funktion in eine separate Funktion umwandeln können.Sobald Sie diesen Punkt erreicht haben, wird die Struktur Ihres Codes vertraut - sie sieht aus wie eine handgerollte Homebrew-Tabelle * - die Grundstruktur, auf der virtuelle Methoden normalerweise implementiert werden. Dies ist jedoch eine Tabelle, die Sie jedes Mal manuell aktualisieren und pflegen müssen, wenn Sie einen Elementtyp hinzufügen oder ändern.
Indem Sie sich an "reale" virtuelle Funktionen halten, können Sie die Implementierung des Verhaltens jedes Elements innerhalb des Elements selbst beibehalten. Sie können zusätzliche Elemente auf eigenständige und konsistente Weise hinzufügen. Und während Sie dies alles tun, ist es der Compiler, der sich um die Implementierung Ihres dynamischen Versands kümmert, und nicht Sie.
Um Ihr spezifisches Problem zu lösen: Sie haben Mühe, ein einfaches Paar virtueller Funktionen zum Aktualisieren von Angriff und Verteidigung zu schreiben, da einige Elemente nur den Angriff und einige Elemente nur die Verteidigung betreffen. Der Trick in einem einfachen Fall wie diesem, beide Verhaltensweisen trotzdem zu implementieren, aber in bestimmten Fällen ohne Wirkung.
GetDefenseBonus()
könnte zurückkehren0
oderApplyDefenseBonus(int& defence)
einfach gehendefence
unverändert bleiben. Wie Sie vorgehen, hängt davon ab, wie Sie mit den anderen Aktionen umgehen möchten, die Auswirkungen haben. In komplexeren Fällen, in denen das Verhalten abwechslungsreicher ist, können Sie die Aktivität einfach zu einer einzigen Methode kombinieren.* (Wird jedoch in Bezug auf die typische Implementierung umgesetzt.)
quelle
Einen Codeblock zu haben, der alle möglichen "Werkzeuge" kennt, ist kein großartiges Design (zumal Sie am Ende viele solcher Blöcke in Ihrem Code haben werden). Aber es gibt auch kein Basic
Tool
mit Stubs für alle möglichen Werkzeugeigenschaften: JetztTool
muss die Klasse über alle möglichen Verwendungen Bescheid wissen.Was jedes Werkzeug weiß, ist, was es zu dem Charakter beitragen kann, der es benutzt. Stellen Sie also eine Methode für alle Werkzeuge bereit
giveto(*Character owner)
. Es passt die Statistiken des Spielers an, ohne zu wissen, was andere Werkzeuge können, und was am besten ist, es muss auch nicht über irrelevante Eigenschaften des Charakters informiert sein. Zum Beispiel hat ein Schild nicht einmal über die Attribute muß wissenattack
,invisibility
,health
usw. Alles , was ein Werkzeug anzuwenden benötigt werden ist für das Zeichen der Attribute zu unterstützen , dass das Objekt erfordert. Wenn Sie versuchen, einem Esel ein Schwert zu geben und der Esel keineattack
Statistik hat, wird eine Fehlermeldung angezeigt.Die Werkzeuge sollten auch eine
remove()
Methode haben, die ihre Wirkung auf den Eigentümer umkehrt. Dies ist etwas schwierig (es kann vorkommen, dass Werkzeuge vorhanden sind, die einen Effekt ungleich Null hinterlassen, wenn sie ausgegeben und dann entfernt werden), aber zumindest für jedes Werkzeug lokalisiert.quelle
Es gibt keine Antwort, die besagt, dass es nicht riecht. Ich werde derjenige sein, der für diese Meinung steht. Dieser Code ist völlig in Ordnung! Meine Meinung basiert auf der Tatsache, dass es manchmal einfacher ist, weiterzumachen und Ihre Fähigkeiten schrittweise zu verbessern, wenn Sie mehr neue Inhalte erstellen. Man kann tagelang nicht weiterkommen, um eine perfekte Architektur zu erstellen, aber wahrscheinlich wird es niemand jemals in Aktion sehen, weil man das Projekt nie abgeschlossen hat. Prost!
quelle