Wofür verwendet ihr enums?

#1

In verschiedenen Zusammenhängen habe ich beobachtet, dass Leute enums verwenden - für Dinge, für die sie nach meinem Dafürhalten absolut nicht geeignet oder gedacht sind.

Die Auswüchse, die das teilweise annimmt, und die Planlosigkeit, mit der “einfach mal eine enum in den Code geworfen” wird, ist schon fast verstörend. Ein (vom Original zu Anonymized umbenanntes) Beispiel:

public enum  AnonymizedEnum {
    ONE,TWO,THREE,FOUR,FIVE,SIX,SEVEN
}

Dass komplexe, sich ständig ändernde, hierarchische Strukturen mit hierarchisch verschachtelten enum-Instanzen abgebildet werden, war ein weiteres Highlight. Den Code kann ich aus “NDA-Gründen” nicht posten.

Ein Anlass für diesen Thread ist, dass kürzlich auf stackoverflow jemand gefragt hat: Is there a way to have a bigger enum than 2746 values?.

Wie dort in meinem Kommentar auch erwähnt: Selbst im offiziellen enum-Tutorial-Beispiel wird etwas gemacht, was meiner Ansicht nach “falsch” ist: Dort wird folgende enum definiert:

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);
}

Und aus Neugier, und um meinen Punkt zu unterstreichen, bin ich mal auf Spurensuche gegangen, und habe in der Wayback-Machine eine ältere Version des Tutorials ausgegraben. Dort war die enum noch definiert als

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7),
    PLUTO   (1.27e+22,  1.137e6);
}

Nun. Das ist schon eine source- und binär-inkompatible Änderung.


Findet ihr (auch), dass enums nur verwendet werden sollten, wenn das “Domänenmodell” die Menge der Konstanten für alle Zeit unveränderlich festlegt? (Damit meine ich sowas wie DayOfWeek oder TrafficLightColor: Wenn etwas nicht aus Sonntag…Freitag besteht, dann ist das einfach keine Woche. Und wenn etwas nicht aus RED, YELLOW, GREEN besteht, dann ist das einfach keine Ampel. Sooo viele Beispiele gibt es da IMHO nicht…

#2

was wäre denn die Alternative bei den Planeten, über Versionen hinweg, mit Vermeidung des Problems, welches aus geänderten Zahlen besteht?

ich bin pro Enum für vieles, Sammlung von immer neuen Einträgen mit der Zeit, neuen Methoden, was es so braucht,
saubere Verwaltung als Arrays, einzelne statische Konstanten oder sonstige unschöne Umsetzungen

manche Enums eben anders eingesetzt als andere, das ist alles bewußt und kein Problem, jedenfalls nicht mehr Problem als sowieso mit der Sache, nicht durch Enum-Verwendung schlimmer

#3

Das Problem sind nicht veränderte Zahlen, sondern dass eine enum-Konstante weggefallen ist. Client-Code, der irgendwo Planet.PLUTO verwendet, geht damit kaputt.

Eine konkrete Alternative für das Planetenbeispiel zu nennen ist schwierig (bzw. aufwändig, mit ungünstigem Aufwand-Nutzen-Verhältnis). Man könnte sich was mit Planets.get("PLUTO") vorstellen, mit null als Rückgabewert oder IllegalArgumentException oder oder oder. Aber das ist ja nur ein Dummy-Beispiel, das die allgemeine Frage illustrieren (bzw. die Relevanz der Frage verdeutlichen) soll.

Ich finde, dass enum-Konstanten (als Teil der public API) eine sehr starke Semantik haben sollten, und Veränderungen vermieden werden müssen. Das ONE, TWO, THREE...-Beispiel oder eine enum mit >2000 Konstanten klingt für mich jedenfalls nicht wirklich durchdacht und sinnvoll.

Oder nochmal so: Ich denke, dass enums oft als “(vermeintlich) typsicherer String-Ersatz” o.ä. verwendet werden - als praktisches Hilfsmittel für Programmierer, die in der IDE nach MyEnum. eine Liste haben wollen, in der sie die Konstante auswählen können - obwohl sie eigentlich eher verwendet werden sollten, um etwas zu modellieren. Etwas abstrakt: Sie sollten etwas modelleren, was in der realen Welt existiert, und nicht eine Sammlung von Werten sein, die bei der Modellierung (als Code) irgendwo rausfällt, und wovon man sonst nicht weiß, wo hin damit.

(Ausnahmen und Grenzfälle gibt es sicher immer. Z.B. könnte man daraufhin jetzt streiten, ob die “Zustände eines Zustandsautomaten” denn nun ein Anwendungsfall für Enums sein könnten. Und ich würde sagen: Jooaaa, da schon, auch wenn nicht wirklich etwas “reales” modelliert wird).

#4

Ein Code der irgendwo Planet.PLUTO verwendet, ist meist auch strukturell genau so aufgebaut, dass er von der Existenz eines Planeten Pluto ausgeht. Das enum wäre also sicherlich nicht das einzige Problem beim entfernen. Genau so verhält es sich wenn Pluto als Planet wieder auftaucht.
In einer dynamischen Programmstruktur wo das keinen Unterschied macht, sind die Planeten sowieso alle in einem Array und nicht als enum.

Aber ich schließe mich der Kernaussage an, ich finde heute selten einen Grund für enums. Höchstens als abstrakte unspezifische Übergabeparameter bei denen boolean nicht ausreicht.

#5

Was nicht unter den Tisch fallen sollte, man kann Enums in Annotationen verwenden.
Falls man also (warum auch immer) eine annotation @interface Solarsystem machen will,
kann man dort seine Planeten Enums verwenden bzw. als Koonfiguration ermöglichen

#6

Ich bilde vielleicht das andere Extrem… Ich weiß, was Enums sind und kann sie auch “anwenden” (also “einsetzen”) - aber ich verwende/benutze sie nie. So gesehen, nehme ich dieselbe Position wie Marco ein… Und jetzt eine Begründung: Alle konstanten/variablen Daten sollten für mich “in Dateien bei der Anwendung” (also extern vorhanden) sein.

#7

Genau dann machen doch Enums Sinn. Was ist besser? Code der nicht kompiliert oder Code der zur Laufzeit plötzlich eine RuntimeException wirft? Irgendwas muss doch der Clientcode nach einer Änderungen machen, die Frage ist nur wann er es merkt? Ich bin der Meinung, je früher desto besser und der frühst mögliche Zeitpunkt ist beim Kompilieren.

1 Like
#8

starke Semantik haben sollten, und Veränderungen vermieden werden müssen

Kannst du mir ein praktisches Beispiel für ein Enum nennen, das sich niemals ändert (und auch nicht ändern kann)? Für mich wäre “Planet” das beste Beispiel für etwas “mit einer starken Semantik”, und doch gab es selbst hier eine Änderung. Wochentag? Es wäre nicht das erste Mal, dass man einen neuen Kalender einführt. Eignet sich ein CustomerType im Bankumfeld gut für ein Enum?

Ich kann das Problem ehrlich gesagt nicht ganz erkennen. Wenn ein Planet zum Zwergplaneten degradiert wird, ist es ein Breaking Change und alle Clients müssen damit klarkommen. Ich bekomme sowohl mit einem Enum als auch einem Array Probleme. Bei einem Enum merke ich es gleich, bei einem Array vermutlich erst deutlich später.

Client-Code, der irgendwo Planet.PLUTO verwendet, geht damit kaputt.

Wenn ein Enum mit verschiedenen Clients geteilt wird, kennen wir doch alle Verwender und können/müssen diese mit aktualisieren. Wenn es eine public API ist, dann sollte ich meinen Code sowieso gegen nicht existierende Felder absichern (was, wenn jemand Planet.KLENDATHU anfrargt?). Ich finde es auch gut, wenn ein Client “kaputt” geht, der versucht Informationen über einen Planeten anzuzeigen, der keiner (mehr) ist (vielleicht sollte sich auch der Client gegen eine leere Response vom Server absichern).

#9

Ist doch Jacke wie Hose. Das Resultat ist wie immer nicht funktionierender Code.

Die Nachteile überwiegen den Vorteilen.

#10

Planets.get(“PLUTO”) statt Enum ist allgemein eine Unmöglichkeit, die mit genau zur Einführung von Enums führte, selbst ohne das Beispiel des Wegfalls hier als ein Plus-Argument für Enum-Lösung wie schon genannt, sofortige Rückmeldung im Code statt versteckte Runtime-Bomben,

Planets.get(“PLUTO”) ist unsäglich zu handeln, unschöner Code mit vermeidbaren Strings,
anfällig für simple Tippfehler ohne Compiler-Rückmeldung, gleiche Bomben wie Wegfall Pluto,

schwierig Vorkommen im Code zu finden, Suche nach dem String? was wenn String-Variable, wer weiß wo wie befüllt, wobei es auch für Enums Planets.valueOf(stringEingabe) gibt…

wenn ich direkte Verwendungen einer Enum-Konstante suche setze ich einen Unterstrich rein und gehe die Fehlermeldungen durch, automatische Liste, super komfortabel :wink:


Planets.get(“PLUTO”) ist gar nichts, ein aktives Quellcode-Problem, wenn für wichtige Dinge eingesetzt,
oft genug leider notwendig, wenn eben Menge Keys zu groß, wenn zur Laufzeit dynamisch neue Einträge dazukommen usw.

statische Konstanten Planets.PLUTO usw. sind der erste Schritt zu Besserung, saubere Verwendunge von mehreren Stellen aus, mit noch manchen Problemen,
je nach Typ etwa bei Serialisierung zwei unterschiedliche gleiche Objekte möglich,
Vergleich mit Benutzereingaben besser equals() statt == usw.

Enums ist die Spitze der möglichen Entwicklung hier, beste direkte Verwendung, frühe Prüfung bei Benutzereingaben mit valueOf(), ==-Vergleich, Serialisierung + switch gut unterstützt, direkt sinnvoller F3-Quellcode-Sprung und und und was mir gerade vielleicht alles nicht einfällt,

ob als mächtige Möglichkeit genutzt oder puristisch vermieden, mit welchen Alternativen auch immer, muss jeder selber wissen

#11

Ich find das sehr diskriminierend! Pluto ist doch nach wie vor ein Planet - wenn auch ein Zwergplanet. Warum willst du denn jetzt auf einmal Minderheiten ausschließen? Hätte ich nicht von dir erwartet oO. Gib dem Enum doch einfach ein 3tes boolean-Feld (isGnarf). Und wenn das wahr ist, dann wird halt “Pluto (Zwergplanet)” anstatt “Pluto (Planet)” geschrieben … tsts.

Nein ist es nicht. Du willst Fehler so früh wie möglich finden. Compile-Fehler verlassen deine Softwarebude nicht. Fehler die zur Laufzeit fliegen können aber schon und wenn du Pech hast, dann tauchen die erst beim Kunden auf.

Dito. Nichts was du hast ist 100% stabil. Nimm doch nur mal den einfachen Fall hier:

public enum Gender {
    Male, Female;
}

Dann kommt von irgendeim non-binary (den Begriff hab ich vor kurzem gelernt und muss mit angeben) keine 5min später folgender PR:

- Male, Female;
+ // TODO: Get rid of this enum at all, cause wanna know the gender 
+ // is evil! 
+ Male, Female, NonBinary;

Könnte man jetzt ignorieren. Aber man sieht ja schon heute bei jeder Stellenausschreibung m/w/d oder m/w/x. Hätte man damit vor 10 Jahren gerechnet? Oder heute - obwohl man es überall sieht?

#12

und beim US-Militär werden diese Enums dann aktuell wieder zusammengestrichen :wink:

#13

Dieses Argument hatte ich schon erwartet. Es gibt verschiedene Kinds of Compatibility

  1. Source: Source compatibility concerns translating Java source code into class files.
  2. Binary: Binary compatibility is defined in The Java Language Specification as preserving the ability to link without error.
  3. Behavioral: Behavioral compatibility includes the semantics of the code that is executed at runtime.

Und wir reden hier von unterschiedlichen Arten. Wenn eine enum-Konstante wegfällt, ist das nicht mehr source compatible und nicht mehr binary compatible. Wenn eine enum-Konstante dazu kommt, ist es noch source-compatible, aber wohl nicht mehr binary compatible, und je nachdem, wie die Konstanten verwendet werden…

switch (e) { 
    case A: run(); break;
    case B: print(); break;
    default: throw SomethingWrongException();
}

… ist es auch nicht mehr behavioral compatible, und es können genauso schwer zu erkennender Fehler auftreten, wie bei dem Planets.get("PLUTO"). Das Beispiel mit Planets.get("PLUTO") hatte ich übrigens extra klein geschrieben und relativiert, aber schon damit gerechnet, dass trotzdem darauf “rumgehackt” wird :neutral_face: Das war kein echter “Gegenvorschlag”, und schon gar kein Statement der Art “Man sollte nie enums verwenden, sondern es immer genau SO machen”. Aber obwohl es nur suggestiv war: Es ist, auch wenn der Eintrag für "PLUTO" wegfällt, source+binary compatible. Je nachdem, worum es geht, kann eine solche/ähnliche Lösung darum IMHO schon sinnvoll sein.

(Die Frage, wie viel von sowas (durch Unit-Tests o.ä. abgedeckt) werden kann, würde wohl zu weit führen).

Ein Punkt, der IMHO wichtig sein kann, ist auch die Frage, ob die Menge der “Konstanten” durch Clients erweitert werden kann. Wenn es z.B. um irgendeine Art “registry” geht. (Ich könnte ein Suggestivbeispiel konstruieren, aber denke, dass jeder sich da etwas vorstellen kann).

Auch damit hatte ich schon gerechnet, und überlegt, ob ich die enum ganz suggestiv DayOfWeekInGregorianCalendar nennen sollte. Es gibt einfach Dinge, die, wenn sie sich ändern, nicht mehr das sind, was sie vorher waren. Wenn man einen LightSwitchState { ON, OFF } hat, und jemand sagt: “Das passt nicht, es gibt auch Dimmer”, dann ist die Antwort eben klar: “Ja, aber dann ist es eben kein LightSwitch mehr”. Genauso mit den Wochentagen.


Tatsächlich finde ich es schwierig, Fälle zu finden, in denen eine enum wirklich angebracht ist. Das ist ja genau der Punkt. Es gibt natürlich Fälle, wo man einfach technisch einen Zustandsraum definiert, wie der schon angedeutete endliche Automat, oder sowas wie MOUSE_PRESSED und MOUSE_RELEASED (Die sind in Swing zwar ints, aber aus Kompatibilitätsgründen - heute würden einige da vielleicht enums verwenden). Aber ich habe enums eben zu oft als Krücke für Dinge gesehen, die schlicht keine Enumeration (Aufzählung) von Elementen einer Menge sind, sondern etwas, wo man das Sprachmittel der enums für fragwürdige Designs verwendet hat.


Die Gender-Sache brauchen wir nicht auszubreiten: boolean hasYchromosome; F… your feelings.

#14

Ich verwende relativ oft Enums, insbesondere für alle Arten von Zuständen. Es macht natürlich einen Unterschied, wer sie dann benutzt, in einer Bibliothek wäre ich vorsichtiger. Meist ist es nicht so schlimm, wenn ein Enum-Wert hinzukommt, Probleme gibt es hauptsächlich beim Entfernen oder Ändern.

Wichtig ist, dass man versucht, die Abarbeitung von switch u.s.w. über Enums nicht über das ganze System zu verteilen, sondern möglichst zu zentralisieren.

Wenn ich mir nicht wirklich sicher bin, dass Enums in einem Fall für alle Zeiten ausreichen - oder einfach nur zum Prototyping - hat sich bewährt, ein Interface zu schreiben, und das Enum davon ableiten zu lassen. Damit kann man Code unabhängig von der Implementierung auf dem Interface werkeln lassen, und später zu einer anderen Implementierung wechseln. Dass würde ich z.B. ganz intuitiv bei dem Planeten-Beispiel so machen.

#15

Ich seh das so, enums machen dann Sinn, wenn ich eine API anbiete und genau weiß welche Instanzen ich von einer Klasse erwarte und verhindern möchte, dass (aus meiner Sicht) unglütige Objekte erzeugt werden können. Naiv könnte man das ja dadurch erreichen, dass man den Konstruktor auf private setzt und eine Factory anbietet, also so:

public class Planet {

  private double a, b;

  private Planet(double a, double b) {
    this.a = a;
    this.b = b;
  }
  
  public static Planet of(String name) {
    if(name.equalsIgnoreCase("PLUTO")) {
      return new Planet(1.27e+22,  1.137e6);
    }
    //...
    else {
      throw new IllegalArgumentException();
    }
  }  
}

Da es ja doof ist, jedes mal ein neues Objekt zu erzeugen, könnte man PLUTO als Konstante rausziehen.

public class Planet {

  private static Planet PLUTO = new Planet(1.27e+22,  1.137e6);
 
  public static Planet of(String name) {
    if(name.equalsIgnoreCase("PLUTO")) {
      return PLUTO;
    }
    //...
    else {
      throw new IllegalArgumentException();
    }
  }
  
}

Jetzt ist die Frage, ob ich möchte, dass mein Client über die Factory geht, oder halt gleich direkt die Konstante verwendet. Ich würde mich dafür entscheiden, dass der Client direkt die Konstante verwendet. Damit hat er zur Compilezeit die Sicherheit, dass er einen vorhandenen Wert verwendet. Für diesen Usecase finde ich, dass enums einfach nur syntaktischer Zucker sind. Mir ist klar, dass enums technisch etwas anderes sind, aber semantisch kommt es für mich auf das gleiche raus.

#16

wenn das kein Gegenvorschlag war, hast du dann einen anderen?
es ist hier nunmal ein konkretes Beispiel, auch wenn Verwendung fehlt, aber anschaulich für alles

welche Variante käme alternativ in Frage?


wiederum die Frage, wie es alternativ ohne Enum aussieht?
wenn <if, else if, else> statt switch, dann eben genauso problematisch, nur noch unschöner anzusehen,

der Satz suggeriert halb, dass speziell dieser Fall NUR bei Enum auftritt, ‘genauso’ ein Problem wie das andere mit fehlenden Erkennen im Quellcode, was doch reichlich fraglich ist

sichlich hat die beste Lösung™ :wink: Enum eine Grundmenge X an Problemen, alle Alternativen diese aber auch + zusätzliche Probleme

es hilft ja auch wenig, etwa ‘ein Enum kann (auch) zu einer NullPointerException führen’ zu erwähnen, wenn bei jeder Alternative genauso…

#17

Das stimmt auch wiederum. Code, der ggf. mit Enums funktioniert, aber nicht umgekehrt, wäre damit ggf. “gültig”, und Probleme könnten damit vielleicht erst/nur bei der Erweiterbarkeit auftreten. Jedenfalls verstehe ich dich. :slight_smile:

Dennoch bieten Enums vielleicht mehr Sicherheiten, aber sie sind vielleicht auch mit Einschränkungen verbunden. :wink: (Welche hier jetzt gerade diskutiert werden)

#18

Sicher, definierte Zustände in einem definierten Zustandsraum hatte ich ja als Beispiel genannt. Und wie wichtig die Frage ist, ob das public ist und clients das sehen, sieht man ja schon an den verschiedenen Formen der Kompatibilität.

Unter der Haube sind enums in Java ziemlich genau das. Mit ein bißchen Schmuck für switch & Co.

Nun, das Beispiel ist zwar anschaulich, macht aber nur bedingt Sinn, solange es keinen “Kontext” gibt, in dem man über das Design nachdenkt. “Planeten sind eine Aufzählung” würde man ja so nicht sagen - das Beispiel dient dort ja nur dazu, zu illustrieren, dass enums auch “komplex” sein können (und nicht nur verkappte ints, wie in C++). Der Ansatz, bei dem man nicht viel falsch machen kann, ist eben

interface Planet { ... }
interface SolarSystem { Planet get(String planetName); }
SolarSystem ourSolarSystem = SolarSystems.createOurs(); 

Ein “verstecktes Anti-Pattern” bei enums kann noch sein, dass man mit switch modelliert, was eigentlich über Polymorphie abgebildet werden sollte (aber die Frage sollte man sich IMHO bei jedem switch (und sogar bei den meisten ifs) stellen…)


Aber nochmal: Es geht nicht direkt um Vor- oder Nachteile der einen oder anderen Lösung, sondern wirklich ganz abstrakt um den Punkt, dass ich finde, dass enums nur und genau dann verwendet werden sollten, wenn man eine “Domänenspezifische Auflistung von Elementen” hat (die sich selten bis nie ändert), und eben nicht als “Strings ohne Anführungszeichen”.

#19

vorstellbar, falls nicht zu abstoßend, ist nebenher noch die Variante einer Enum nur für die platte Unterscheidung, Basisdaten (Name), Nummerierung,
und trotzdem noch komplexe Klassen dazu, evtl. sehr umfangreich erst geladen/ aufgebaut usw.

enum PlanetKey …
Planet get(PlanetKey key)

#20

Meistens nur für Types, also Dinge die man anno dazumal mit ints definiert hat. Seit kurzer Zeit erwische ich mich auch dabei, Factorys, die Konstanten bieten (z.B. Eineiten-Umrechner als Enums zu implementieren, welche dann natürlich ein Interface implementieren. Dann kann man zumindest switch auf die Konstanten anwenden, bei produzierten Objekten durch die Factory-Methode funktioniert das leider nicht.