it-swarm.com.de

Erstellen Aufzählungen spröde Schnittstellen?

Betrachten Sie das folgende Beispiel. Jede Änderung an der ColorChoice-Enumeration wirkt sich auf alle IWindowColor-Unterklassen aus.

Neigen Enums dazu, spröde Grenzflächen zu verursachen? Gibt es etwas Besseres als eine Aufzählung, um mehr polymorphe Flexibilität zu ermöglichen?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

Bearbeiten: Entschuldigung für die Verwendung von Farbe als Beispiel, darum geht es bei der Frage nicht. Hier ist ein anderes Beispiel, das den roten Hering vermeidet und mehr Informationen darüber liefert, was ich unter Flexibilität verstehe.

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

Stellen Sie sich außerdem vor, dass Handles für die entsprechende Unterklasse ISomethingThatNeedsToKnowWhatTypeOfCharacter durch ein werkseitiges Entwurfsmuster ausgegeben werden. Jetzt habe ich eine API, die in Zukunft nicht mehr für eine andere Anwendung erweitert werden kann, bei der die zulässigen Zeichentypen {Mensch, Zwerg} sind.

Bearbeiten: Nur um konkreter zu sein, woran ich arbeite. Ich entwerfe eine starke Bindung dieser ( MusicXML ) Spezifikation und verwende Enum-Klassen, um die Typen in der Spezifikation darzustellen, die mit xs: enumeration deklariert sind. Ich versuche darüber nachzudenken, was passiert, wenn die nächste Version (4.0) herauskommt. Könnte meine Klassenbibliothek in einem 3.0-Modus und in einem 4.0-Modus funktionieren? Wenn die nächste Version 100% abwärtskompatibel ist, dann vielleicht. Aber wenn Aufzählungswerte aus der Spezifikation entfernt wurden, bin ich tot im Wasser.

18

Bei richtiger Verwendung sind Aufzählungen weitaus lesbarer und robuster als die "magischen Zahlen", die sie ersetzen. Normalerweise sehe ich nicht, dass sie Code spröder machen. Zum Beispiel:

  • setColor () muss keine Zeit damit verschwenden, zu überprüfen, ob value ein gültiger Farbwert ist oder nicht. Der Compiler hat das bereits getan.
  • Sie können setColor (Color :: Red) anstelle von setColor (0) schreiben. Ich glaube, mit der Funktion enum class In modernem C++ können Sie sogar Leute dazu zwingen, immer die erstere anstelle der letzteren zu schreiben.
  • Normalerweise nicht wichtig, aber die meisten Aufzählungen können mit jedem Größenintegraltyp implementiert werden, sodass der Compiler die Größe auswählen kann, die am bequemsten ist, ohne dass Sie gezwungen sind, über solche Dinge nachzudenken.

Die Verwendung einer Aufzählung für Farben ist jedoch fraglich , da es in vielen (den meisten?) Situationen keinen Grund gibt, den Benutzer auf einen so kleinen Satz von Farben zu beschränken. Sie können sie auch beliebige RGB-Werte übergeben. Bei den Projekten, mit denen ich arbeite, wird eine kleine Liste solcher Farben immer nur als Teil einer Reihe von "Themen" oder "Stilen" angezeigt, die als dünne Abstraktion über konkrete Farben fungieren sollen.

Ich bin mir nicht sicher, worum es bei Ihrer Frage nach "polymorpher Flexibilität" geht. Aufzählungen haben keinen ausführbaren Code, daher gibt es nichts, was polymorph gemacht werden könnte. Vielleicht suchen Sie nach dem Befehlsmuster ?

Bearbeiten: Nach dem Bearbeiten ist mir immer noch nicht klar, nach welcher Art von Erweiterbarkeit Sie suchen, aber ich denke immer noch, dass das Befehlsmuster einer "polymorphen Aufzählung" am nächsten kommt.

25
Ixrec

Jede Änderung an der ColorChoice-Enumeration wirkt sich auf alle IWindowColor-Unterklassen aus.

Nein, tut es nicht. Es gibt zwei Fälle: Implementierer werden entweder

  • speichern, Zurückgeben und Weiterleiten von Aufzählungswerten, ohne diese zu bearbeiten. In diesem Fall sind sie von Änderungen in der Aufzählung nicht betroffen

  • arbeiten mit einzelnen Aufzählungswerten. In diesem Fall muss natürlich jede Änderung der Aufzählung unweigerlich notwendigerweise mit einer entsprechenden Änderung in der Logik des Implementierers berücksichtigt werden.

Wenn Sie "Sphere", "Rectangle" und "Pyramid" in eine "Shape" -Aufzählung setzen und eine solche Aufzählung an eine drawSolid() - Funktion übergeben, die Sie geschrieben haben, um den entsprechenden Volumenkörper zu zeichnen, und dann an eine Wenn Sie sich entscheiden, der Aufzählung einen "Ikositetraeder" -Wert hinzuzufügen, können Sie nicht erwarten, dass die Funktion drawSolid() davon unberührt bleibt. Wenn Sie erwartet haben, dass es irgendwie Ikositetraeder zeichnet, ohne dass Sie zuerst den eigentlichen Code schreiben müssen, um Ikositetraeder zu zeichnen, ist das Ihre Schuld, nicht die Schuld der Aufzählung. Damit:

Neigen Enums dazu, spröde Grenzflächen zu verursachen?

Nein, das tun sie nicht. Was zu spröden Schnittstellen führt, sind Programmierer, die sich selbst als Ninjas betrachten und versuchen, ihren Code zu kompilieren, ohne dass ausreichende Warnungen aktiviert sind. Dann warnt der Compiler sie nicht, dass ihre Funktion drawSolid() eine Anweisung switch enthält, der eine Klausel case für den neu hinzugefügten Aufzählungswert "Ikositetrahedron" fehlt.

Die Art und Weise, wie es funktionieren soll, ist analog zum Hinzufügen einer neuen reinen virtuellen Methode zu einer Basisklasse: Sie müssen diese Methode dann auf jedem einzelnen Erben implementieren, sonst wird und sollte das Projekt nicht erstellt.


Um ehrlich zu sein, sind Aufzählungen kein objektorientiertes Konstrukt. Sie sind eher ein pragmatischer Kompromiss zwischen dem objektorientierten Paradigma und dem strukturierten Programmierparadigma.

Die reine objektorientierte Art, Dinge zu tun, besteht darin, überhaupt keine Aufzählungen zu haben und stattdessen Objekte auf dem ganzen Weg zu haben.

Die rein objektorientierte Art, das Beispiel mit den Volumenkörpern zu implementieren, besteht natürlich darin, Polymorphismus zu verwenden: Anstatt eine einzige monströse zentralisierte Methode zu haben, die weiß, wie man alles zeichnet, und zu wissen, welcher Körper zu zeichnen ist, deklarieren Sie ein " Solide "Klasse mit einer abstrakten (rein virtuellen) draw() Methode, und dann fügen Sie die Unterklassen" Sphere "," Rectangle "und" Pyramid "hinzu, die jeweils ihre eigene Implementierung von draw() haben weiß, wie man sich selbst zeichnet.

Auf diese Weise müssen Sie beim Einführen der Unterklasse "Ikositetrahedron" lediglich eine draw() - Funktion dafür bereitstellen, und der Compiler erinnert Sie daran, indem Sie "Icositetrahedron" nicht anderweitig instanziieren lassen.

15
Mike Nakis

Aufzählungen erzeugen keine spröden Schnittstellen. Missbrauch von Aufzählungen tut.

Wofür sind Aufzählungen?

Aufzählungen sind so konzipiert, dass sie als Mengen von Konstanten mit aussagekräftigen Namen verwendet werden. Sie sind zu verwenden, wenn:

  • Sie wissen, dass keine Werte entfernt werden.
  • (Und) Sie wissen, dass es höchst unwahrscheinlich ist, dass ein neuer Wert benötigt wird.
  • (Oder) Sie akzeptieren, dass ein neuer Wert benötigt wird, aber selten genug, um die Korrektur des gesamten Codes zu rechtfertigen, der aufgrund dessen beschädigt wird.

Gute Verwendung von Aufzählungen:

  • Wochentage: (gemäß .Net's System.DayOfWeek) Wenn Sie es nicht mit einem unglaublich dunklen Kalender zu tun haben, wird es immer nur einen geben 7 Tage die Woche.
  • Nicht erweiterbare Farben: (gemäß .Nets System.ConsoleColor) Einige mögen damit nicht einverstanden sein, aber .Net hat dies für a Grund. Im Konsolensystem von .Net stehen der Konsole nur 16 Farben zur Verfügung. Diese 16 Farben entsprechen der alten Farbpalette CGA oder 'Color Graphics Adapter' . Es werden niemals neue Werte hinzugefügt, was bedeutet, dass dies tatsächlich eine vernünftige Anwendung einer Aufzählung ist.
  • Aufzählungen, die feste Zustände darstellen: (gemäß Java Thread.State) Die Designer von Java entschieden, dass in Im Java Threading-Modell wird es immer nur eine feste Menge von Zuständen geben, in denen sich ein Thread befinden kann. Um die Sache zu vereinfachen, werden diese verschiedenen Zustände als Aufzählungen dargestellt bedeutet, dass viele zustandsbasierte Überprüfungen einfache Ifs und Switches sind, die in der Praxis mit ganzzahligen Werten arbeiten, ohne dass sich der Programmierer Gedanken darüber machen muss, was die Werte tatsächlich sind.
  • Bitflags, die sich nicht gegenseitig ausschließende Optionen darstellen: (gemäß System.Text.RegularExpressions.RegexOptions Von .Net) Bitflags werden häufig verwendet. In der Tat so häufig, dass in .Net alle Aufzählungen eine HasFlag(Enum flag) -Methode eingebaut haben. Sie unterstützen auch die bitweisen Operatoren und es gibt ein FlagsAttribute, um eine Aufzählung als zu kennzeichnend zu markieren eine Reihe von Bitflags. Wenn Sie eine Aufzählung als Satz von Flags verwenden, können Sie eine Gruppe von Booleschen Werten in einem einzelnen Wert darstellen und die Flags der Einfachheit halber eindeutig benennen. Dies wäre äußerst vorteilhaft für die Darstellung der Flags eines Statusregisters in einem Emulator oder für die Darstellung der Berechtigungen einer Datei (Lesen, Schreiben, Ausführen) oder für nahezu jede Situation, in der sich eine Reihe verwandter Optionen nicht gegenseitig ausschließen.

Schlechte Verwendung von Aufzählungen:

  • Charakterklassen/-typen in einem Spiel: Sofern es sich bei dem Spiel nicht um eine One-Shot-Demo handelt, die Sie wahrscheinlich nicht mehr verwenden werden, sollten Aufzählungen nicht für Charaktere verwendet werden Klassen, weil Sie wahrscheinlich viel mehr Klassen hinzufügen möchten. Es ist besser, eine einzelne Klasse zu haben, die einen Charakter darstellt, und einen Charakter im Spiel zu haben, der anders dargestellt wird. Eine Möglichkeit, dies zu handhaben, ist das Muster TypeObject . Andere Lösungen umfassen das Registrieren von Zeichentypen in einem Wörterbuch/einer Typregistrierung oder einer Mischung aus beiden.
  • Erweiterbare Farben: Wenn Sie eine Aufzählung für Farben verwenden, die später hinzugefügt werden können, ist es keine gute Idee, diese in Aufzählungsform darzustellen Sie werden für immer Farben hinzufügen. Dies ähnelt dem obigen Problem, daher sollte eine ähnliche Lösung verwendet werden (d. H. Eine Variation von TypeObject).
  • Erweiterbarer Status: Wenn Sie eine Zustandsmaschine haben, die möglicherweise viel mehr Zustände einführt, ist es keine gute Idee, diese Zustände über Aufzählungen darzustellen . Die bevorzugte Methode besteht darin, eine Schnittstelle für den Status der Maschine zu definieren, eine Implementierung oder Klasse bereitzustellen, die die Schnittstelle umschließt und ihre Methodenaufrufe delegiert (ähnlich dem Muster Strategie ), und dann ihren Status zu ändern, indem sie diese ändert vorausgesetzt, die Implementierung ist derzeit aktiv.
  • Bitflags, die sich gegenseitig ausschließende Optionen darstellen: Wenn Sie Aufzählungen verwenden, um Flags darzustellen, und zwei dieser Flags niemals zusammen auftreten sollten, haben Sie sich in den Fuß geschossen . Alles, was so programmiert ist, dass es auf eines der Flags reagiert, reagiert plötzlich auf das Flag, auf das es zuerst reagiert - oder schlimmer noch, es reagiert möglicherweise auf beide. Diese Art von Situation verlangt nur nach Ärger. Der beste Ansatz besteht darin, das Fehlen eines Flags nach Möglichkeit als alternative Bedingung zu behandeln (d. H. Das Fehlen des Flags True impliziert False). Dieses Verhalten kann durch die Verwendung spezialisierter Funktionen (d. H. IsTrue(flags) und IsFalse(flags)) weiter unterstützt werden.
14
Pharap

Aufzählungen sind eine große Verbesserung gegenüber magischen Identifikationsnummern für geschlossene Wertesätze, denen nicht viele Funktionen zugeordnet sind. Normalerweise ist es Ihnen egal, welche Nummer tatsächlich mit der Aufzählung verknüpft ist. In diesem Fall ist es einfach zu erweitern, indem am Ende neue Einträge hinzugefügt werden. Es sollte keine Sprödigkeit auftreten.

Das Problem liegt vor, wenn mit der Aufzählung erhebliche Funktionen verknüpft sind. Das heißt, Sie haben diese Art von Code herumliegen:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

Ein oder zwei davon sind in Ordnung, aber sobald Sie diese Art von aussagekräftiger Aufzählung haben, neigen diese switch Anweisungen dazu, sich wie Tribbles zu vermehren. Jedes Mal, wenn Sie die Aufzählung erweitern, müssen Sie alle zugehörigen switches suchen, um sicherzustellen, dass Sie alles abgedeckt haben. Dies ist spröde. Quellen wie Clean Code schlagen vor, dass Sie höchstens ein switch pro Aufzählung haben sollten.

In diesem Fall sollten Sie stattdessen OO Prinzipien) verwenden und eine Schnittstelle für diesen Typ erstellen Behalten Sie die Aufzählung für die Kommunikation dieses Typs bei, aber sobald Sie etwas damit tun müssen, erstellen Sie ein Objekt, das der Aufzählung zugeordnet ist, möglicherweise mithilfe einer Factory. Dies ist viel einfacher zu verwalten, da Sie nur nach einem suchen müssen Ort zum Aktualisieren: Ihre Factory und durch Hinzufügen einer neuen Klasse.

4
congusbongus

Wenn Sie bei der Verwendung vorsichtig sind, würde ich Aufzählungen nicht als schädlich betrachten. Es gibt jedoch einige Dinge zu beachten, wenn Sie sie in einem Bibliothekscode verwenden möchten, im Gegensatz zu einer einzelnen Anwendung.

  1. Entfernen oder ordnen Sie niemals Werte neu. Wenn Sie irgendwann einen Aufzählungswert in Ihrer Liste haben, sollte dieser Wert für alle Ewigkeit mit diesem Namen verknüpft sein. Wenn Sie möchten, können Sie die Werte irgendwann in deprecated_orc Oder was auch immer umbenennen. Wenn Sie sie jedoch nicht entfernen, können Sie die Kompatibilität mit altem Code, der mit den alten Aufzählungen kompiliert wurde, einfacher aufrechterhalten. Wenn ein neuer Code nicht mit alten Enum-Konstanten umgehen kann, generieren Sie dort entweder einen geeigneten Fehler oder stellen Sie sicher, dass kein solcher Wert diesen Code erreicht.

  2. Rechne nicht mit ihnen. Führen Sie insbesondere keine Auftragsvergleiche durch. Warum? Denn dann kann es vorkommen, dass Sie nicht in der Lage sind, vorhandene Werte beizubehalten und gleichzeitig eine vernünftige Reihenfolge beizubehalten. Nehmen Sie zum Beispiel eine Aufzählung für Kompassrichtungen: N = 0, NE = 1, E = 2, SE = 3, ... Wenn Sie nun nach einer Aktualisierung NNE usw. zwischen den vorhandenen Richtungen einfügen, können Sie diese entweder am Ende hinzufügen der Liste und brechen so die Reihenfolge, oder Sie verschachteln sie mit vorhandenen Schlüsseln und brechen so etablierte Zuordnungen, die in Legacy-Code verwendet werden. Oder Sie verwerfen alle alten Schlüssel und verfügen über einen völlig neuen Schlüsselsatz sowie einen Kompatibilitätscode, der aus Gründen des Legacy-Codes zwischen alten und neuen Schlüsseln übersetzt wird.

  3. Wählen Sie eine ausreichende Größe. Standardmäßig verwendet der Compiler die kleinste Ganzzahl, die alle Aufzählungswerte enthalten kann. Das heißt, wenn bei einem Update Ihr Satz möglicher Aufzählungen von 254 auf 259 erweitert wird, benötigen Sie plötzlich 2 Bytes für jeden Aufzählungswert anstelle von einem. Dies könnte überall zu Struktur- und Klassenlayouts führen. Versuchen Sie daher, dies zu vermeiden, indem Sie im ersten Entwurf eine ausreichende Größe verwenden. C++ 11 gibt Ihnen hier viel Kontrolle, aber ansonsten sollte es auch hilfreich sein, einen Eintrag LAST_SPECIES_VALUE=65535 Anzugeben.

  4. Haben Sie eine zentrale Registrierung. Da Sie die Zuordnung zwischen Namen und Werten korrigieren möchten, ist es schlecht, wenn Sie Drittbenutzer Ihres Codes neue Konstanten hinzufügen lassen. Kein Projekt, das Ihren Code verwendet, sollte diesen Header ändern dürfen, um neue Zuordnungen hinzuzufügen. Stattdessen sollten sie dich nerven, um sie hinzuzufügen. Dies bedeutet, dass für das von Ihnen erwähnte Beispiel von Menschen und Zwergen Aufzählungen in der Tat schlecht passen. Dort wäre es besser, zur Laufzeit eine Art Registrierung zu haben, in der Benutzer Ihres Codes eine Zeichenfolge einfügen und eine eindeutige Nummer zurückerhalten können, die schön in einen undurchsichtigen Typ eingewickelt ist. Die "Zahl" könnte sogar ein Zeiger auf die betreffende Zeichenfolge sein, es spielt keine Rolle.

Trotz der Tatsache, dass mein letzter Punkt oben Aufzählungen schlecht für Ihre hypothetische Situation geeignet macht, scheint Ihre tatsächliche Situation, in der sich einige Spezifikationen ändern könnten und Sie Code aktualisieren müssten, recht gut in eine zentrale Registrierung für Ihre Bibliothek zu passen. Daher sollten Aufzählungen dort angebracht sein, wenn Sie sich meine anderen Vorschläge zu Herzen nehmen.

1
MvG