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
und tools
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 neue tools
Liste ein. Immerhin Tool
ist eine wirklich einfache Klasse mit nur einem Mitglied. Also erstelle ich ein paar Kopien, einschließlich einer Kopie der Sword
, aber ich wusste nicht, dass es ein Schwert ist, also habe ich nur die kopiert name
. Später betrachtet die attack()
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:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
Warum ist das das gleiche Muster? Weil bind
es kein akzeptiert sockaddr_in*
, akzeptiert es ein generischeres sockaddr*
. Wenn Sie sich die Definitionen für diese Klassen ansehen, sehen wir, dass sockaddr
nur ein Mitglied der Familie, der wir sin_family
* zugewiesen haben, vorhanden ist . Die Familie sagt, zu welchem Subtyp du die Casts machen solltest sockaddr
. AF_INET
sagt dir, dass die Adressstruktur tatsächlich a ist sockaddr_in
. In diesem AF_INET6
Fall wäre die Adresse eine Adresse sockaddr_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 ein std::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 ihrem sin_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ße if
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:
- Langsam -
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.
- Zu vielseitig - Es gibt etwas zu sagen, um sich selbst Einschränkungen aufzuerlegen, wenn Sie etwas äußerst Gefährliches tun. Ich hatte Systeme wie dieses, die nach einem Teilstring suchten, um festzustellen, auf welche Art von Objekt sie schauten. Dies funktionierte hervorragend, bis der Name eines Objekts versehentlich diese Teilzeichenfolge enthielt und einen fürchterlich kryptischen Fehler verursachte. Da wir, wie oben erwähnt, nur eine kleine Anzahl von Fällen benötigen, gibt es keinen Grund, ein stark überlastetes Werkzeug wie Strings zu verwenden. Dies führt zu...
- Fehleranfällig - Sagen wir einfach, Sie wollen auf mörderische Weise versuchen, zu debuggen, warum Dinge nicht funktionieren, wenn ein Verbraucher versehentlich den Namen eines Zaubertuchs festlegt
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:
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
std::string typeName() const {
switch(type) {
case kSword: return "Sword";
case kSheild: return "Sheild";
case kMagicCloth: return "Magic Cloth";
default:
throw std::runtime_error("Invalid enum!");
}
}
};
Dieses Beispiel zeigt auch eine switch
Aussage, die die Aufzählungen mit einbezieht, mit dem wichtigsten Teil dieses Musters: einem default
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.
void damageWargear(Tool* tool)
{
switch(tool->type)
{
case Tool::kSword:
static_cast<Sword*>(tool)->damageSword();
break;
case Tool::kShield:
static_cast<Sword*>(tool)->damageShield();
break;
default:
break; // Ignore all other objects
}
}
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 Unterklasse Tool
werden nur alle Felder von jedem Untertyp darauf platziert.
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
int attack;
int defense;
};
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:
- Wenn die Objekte zu unterschiedlich sind - Sie erhalten möglicherweise eine Wäscheliste mit Feldern, und es ist unklar, welche für die einzelnen Objekttypen gelten.
- Wenn Sie in einer speicherkritischen Situation arbeiten - Wenn Sie 10 Werkzeuge herstellen müssen, können Sie mit dem Speicher faul sein. Wenn Sie 500 Millionen Werkzeuge herstellen müssen, kümmern Sie sich um Bits und Bytes. Unionsstrukturen sind immer größer als sie sein müssen.
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 Protokoll AF_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 machen Sword
, Shield
und MagicCloth
Klassen, dann Werkzeug sein , macht boost::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:
class Tool;
class Sword;
class Shield;
class MagicCloth;
class ToolVisitor {
public:
virtual void visit(Sword* sword) = 0;
virtual void visit(Shield* shield) = 0;
virtual void visit(MagicCloth* cloth) = 0;
};
class Tool {
public:
virtual void accept(ToolVisitor& visitor) = 0;
};
lass Sword : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
};
class Shield : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int defense;
};
class MagicCloth : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
int defense;
};
Also, wie sieht dieses gottesfürchtige Muster aus? Nun, Tool
hat eine virtuelle Funktion accept
. Wenn Sie es einem Besucher übergeben, wird erwartet, dass es sich umdreht und die richtige visit
Funktion für diesen Besucher für den Typ aufruft. Dies ist, was das visitor.visit(*this);
für jeden Untertyp tut. Kompliziert, aber wir können dies mit Ihrem obigen Beispiel zeigen:
class AttackVisitor : public ToolVisitor
{
public:
int& currentAttack;
int& currentDefense;
AttackVisitor(int& currentAttack_, int& currentDefense_)
: currentAttack(currentAttack_)
, currentDefense(currentDefense_)
{ }
virtual void visit(Sword* sword)
{
currentAttack += sword->attack;
}
virtual void visit(Shield* shield)
{
currentDefense += shield->defense;
}
virtual void visit(MagicCloth* cloth)
{
currentAttack += cloth->attack;
currentDefense += cloth->defense;
}
};
void Player::attack()
{
int currentAttack = this->attack;
int currentDefense = this->defense;
AttackVisitor v(currentAttack, currentDefense);
for (Tool* t: tools) {
t->accept(v);
}
//some other functions to start attack
}
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 ruft t->accept(v)
eine virtuelle Funktion auf. Da das erste Objekt ein Schild ist, void Shield::accept(ToolVisitor& visitor)
ruft es am Ende , was ruft visitor.visit(*this);
. Wenn wir uns nun ansehen, was visit
wir anrufen sollen, wissen wir bereits, dass wir einen Schild haben (weil diese Funktion aufgerufen wurde), also werden wir am Ende void ToolVisitor::visit(Shield* shield)
unseren anrufen AttackVisitor
. 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 jeder ToolVisitor
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 verwenden if
oder switch
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 diese sa_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.