Mit der Einführung von Enums in Java 5 wurde endlich eine leistungsfähige (und leicht magische) Möglichkeit geschaffen, feste Mengen zu modellieren. Man kann jedoch auch zu viel des Guten haben, und in manchen Situationen ist eine Enum ein wenig zu starr, um praktisch zu sein.
Hier möchte ich ein Muster diskutieren, das ich - in Ermangelung eines besseren Namens - die dynamische Enum nenne, die dieses Problem angeht.
"Dynamische Enums"? Ist das nicht ein bisschen wie 'typsichere Skriptsprache' oder 'vollständig spezifizierte Anforderungen'?" Nun, lassen Sie mich versuchen, den Zweck einer dynamischen Enum zu umreißen:
CountryRepository.get(countryIsoCode) != null
check könnte verwendet werden, aber das ist eine völlig anwendungsspezifische Logik.
Ein anderes Szenario: Nehmen wir an, Sie bauen eine HR-Anwendung für den Geheimdienst Ihrer Majestät. Eines der Schlüsselelemente des Domänenmodells wird Ihr
BureaucracyService.recordDamageCaused(int agentCodeNumber, int damageInMillionGBP)
was sicherlich weniger selbstdokumentierend ist als
BureaucracyService.recordDamageCaused(Agent agent, int damageInMillionGBP)
Hier ist Agent ein "enum-ähnliches" Objekt, das wie folgt aussehen könnte
public class Agent {
public final int codeNumber;
public Agent(int codeNumber) {
this.codeNumber = codeNumber;
}
}
oder Sie könnten einen Getter hinzufügen, wenn Ihnen die Idee eines öffentlichen endgültigen Feldes nicht gefällt. "Aber warten Sie einen Moment! Was soll Sie daran hindern, einen Agenten mit einer nicht existierenden Codenummer zu erstellen? Schlimmer noch, was ist, wenn Ihre Geschäftslogik irgendwo new Country("Sowjetunion") verwendet? Wenn Sie Countries.SOVIET_UNION verwenden und dann die Enum-Konstante entfernen, würden Sie einen Kompilierungsfehler erhalten. Aber jetzt haben Sie einfach eine Zeitbombe in Ihrem Code!" Sehr wahr. Das scheint einfach der Preis zu sein, den Sie für Ihr Mittagessen zahlen müssen. Mögliche Ansätze zur Abschwächung dieses Risikos bestehen darin, die Verwendung bestimmter Enum-Konstanten auf ein Minimum zu beschränken (was nicht immer machbar ist) oder zu entscheiden, dass Elemente nur zu Ihrer dynamischen Enum hinzugefügt, nicht aber entfernt werden dürfen. In jedem Fall scheinen dynamische Enums für Systeme, in denen Enum-Konstanten nicht "dynamisch", d.h. auf der Grundlage von Benutzereingaben, ausgewählt werden, weniger relevant zu sein. Vergleichen Sie
Klasse TrafficLight {
private static enum Status { ROT, GRÜN }
privaten Staat Staat;
void changeState(boolean goGreen) {
state = (goGreen ? State.GREEN : State.RED);
}
}
ein Beispiel für eine "statische Auswahl", mit
Klasse TrafficLight {
...
void changeState(String targetState) {
state = State.valueOf(targetState);
}
}
was verdeutlicht, was ich mit "dynamischer" Auswahl meine. In diesem letzteren Fall besteht immer die Möglichkeit, dass der Wert ungültig ist, wenn targetState nicht vertrauenswürdig ist (z.B. aus einer HTTP-Anfrage stammt, die von einer Benutzeroberfläche übermittelt wurde). Die Verwendung einer "echten" Enum ändert daran nichts!
Wenn State nun ein dynamisches und kein echtes Enum wäre (was es mir z.B. erlauben würde, AMBER hinzuzufügen, ohne neu kompilieren zu müssen), wäre es dann nicht praktisch, wenn ich valueOf, values und all die anderen Methoden, die wir von Enums gewohnt sind, aufrufen könnte und sie sich auf die gleiche Weise verhalten würden?
Das scheint mir auf jeden Fall besser zu sein, als den Code für den Aufruf von stateRepository.find(targetState) oder einer anderen, nicht standardisierten Methode umschreiben zu müssen.
An dieser Stelle kommen DynamicEnum und DynamicEnumerable ins Spiel. Ein schwergewichtiges Objekt wie AgentRecord würde
public interface DynamicEnumerable {
String name();
E enumValue();
}
public AgentRecord implements DynamicEnumerable {
int codeNumber;
String-Name;
boolean licenceToKill;
int numKills;
long totalDamagedPropertyValue; // in GBP * 100
public String name() {
return "00" + Integer.toString(codeNumber);
}
public Agent enumValue() {
return new Agent(codeNumber);
}
}
Die DynamicEnum selbst ist im Wesentlichen eine Fabrik für die dynamischen Enum-Konstanten, die eine bestimmte DynamicEnumerable darstellen. Sie bietet alle Funktionen, die über die statischen Methoden einer regulären Enum verfügbar sind, wie z.B. Werte.
public interface DynamicEnum<e, D erweitert DynamicEnumerable> erweitert Comparator {
boolean exists(E enumValue);
E valueOf(String name);
Liste Werte();
int ordinal(E enumValue);
Bereich festlegen(E von, E bis);
D backingValueOf(E enumValue);
}
Darüber hinaus bietet die backingValueOf-Methode eine Möglichkeit, auf das schwergewichtige "Backing"-Objekt zuzugreifen, das einer dynamischen Enum-Konstante entspricht. Dies könnte z.B. erforderlich sein, um einen Bildschirm "Agentendetails" zu füllen.
public interface DynamicEnumerableRepository {
Liste loadAll();
}
public class StaticResourceBackedDynamicEnum<e, D erweitert DynamicEnumerable>
implementiert DynamicEnum {
private final List orderedDynamicEnumValues;
private final Map dynamicEnumValues;
private final Map dynamicEnumValueNames;
private class DynamicEnumValueDescriptor {
private int ordinal;
private D backingObject;
private DynamicEnumValueDescriptor(int ordinal, D backingObject) {
this.ordinal = ordinal;
this.backingObject = backingObject;
}
}
public StaticResourceBackedDynamicEnum(
DynamicEnumerableRepository dynamicEnumerableRepository) {
List dynamicEnumerables = dynamicEnumerableRepository.loadAll();
int numDynamicEnumerables = dynamicEnumerables.size();
orderedDynamicEnumValues = new ArrayList(numDynamicEnumerables);
dynamicEnumValues = new HashMap(numDynamicEnumerables);
dynamicEnumValueNames = new HashMap(numDynamicEnumerables);
for (int i = 0; i < numDynamicEnumerbles; i++) {
D dynamicEnumerable = dynamicEnumerables.get(i);
E dynamicEnumValue = dynamicEnumerable.enumValue();
// ...
orderedDynamicEnumValues.add(dynamicEnumValue);
dynamicEnumValues.put(dynamicEnumValue,
new DynamicEnumValueDescriptor(i, dynamicEnumerable));
dynamicEnumValueNames.put(dynamicEnumerable.name(), dynamicEnumValue);
}
}
public boolean exists(E enumValue) {
return dynamicEnumValues.containsKey(enumValue);
}
public E valueOf(String name) {
// ...
return dynamicEnumValueNames.get(name);
}
// ...
public Liste Werte() {
return new ArrayList(orderedDynamicEnumValues);
}
public int ordinal(E enumValue) {
// ...
return dynamicEnumValues.get(enumValue).ordinal;
}
public int compare(E enumValue1, E enumValue2) {
// ...
return (ordinal(enumValue1) - ordinal(enumValue2));
}
public Set Bereich(E von, E bis) {
// ...
// subList behandelt den Index "to" als *exclusive*, aber *inclusive* ist hier erforderlich
return new HashSet(orderedDynamicEnumValues.subList(
orderedDynamicEnumValues.indexOf(from), orderedDynamicEnumValues.indexOf(to) + 1));
}
public D backingValueOf(E enumValue) {
// ...
return dynamicEnumValues.get(enumValue).backingObject;
}
}
Der größte Teil der Argumentprüfung wurde in dem Codeausschnitt weggelassen. Die Implementierung ist auch nicht besonders effizient - es gibt jede Menge Duplikation in den drei wichtigsten internen Datenstrukturen orderedDynamicEnumValues, dynamicEnumValues und dynamicEnumValueNames, die zweifellos optimiert werden könnten. Der Code erwartet auch, dass das zugrunde liegende Repository im Konstruktor verwendbar ist. Wenn das Repository eine Transaktion benötigt, um auf eine Datenbank zuzugreifen, ist dies möglicherweise nicht der Fall, z.B. wenn die deklarative Transaktionsverwaltung von Spring verwendet wird. Hier könnte ein Initialization on Demand Holder oder ein anderes geeignetes Singleton-Initialisierungsmuster verwendet werden. In jedem Fall ist die Erstellung eines dynamischen AgentsEnum so einfach wie die Erstellung eines dynamischen AgentsEnumValues, vorausgesetzt, es gibt ein AgentRecordRepository, das alle AgentRecords aus Moneypennys altem Aktenschrank lesen kann.
public class Agents extends StaticResourceBackedDynamicEnum {
public Agents(DynamicEnumerableRepository agentRecordRepository) {
super(agentRecordRepository);
}
}
Dies ermöglicht Ihnen, Code zu schreiben wie:
Agents agents = new Agents(agentRecordRepository);
assert agents.valueOf("007").equals(new Agent(7)) // unter der Annahme, dass Agent "gleich" für die Codenummer definiert
assert agents.values().contains(new Agent(8)); // 008 ('Bill') erscheint in Goldfinger
assert (agents.exists(new Agent(-73)) == false);
assert (agents.range(new Agent(1), new Agent(7)).size() == 5); // 001, 004, 005, 006 und 007
assert agents.backingValueOf(new Agent(7)).hasLicenseToKill();
Den Quellcode und ein Maven-Projekt finden Sie hier.
Verfasst von
Andrew Phillips
Unsere Ideen
Weitere Blogs
Contact




