You can also use the Visitor template adaptation to enumerations that prevent all types of unrelated states from being placed in the enum class.
Compilation time will fail if one of them changes the enumeration carefully enough, but it is not guaranteed.
You will still have a crash earlier than the RTE in the default statement: it will not succeed if one of the visitor classes loads, which may happen when the application starts.
Here is the code:
You start with an enumeration that looks like this:
public enum Status { PENDING, PROGRESSING, DONE }
Here's how you convert it to use a visitor template:
public enum Status { PENDING, PROGRESSING, DONE; public static abstract class StatusVisitor<R> extends EnumVisitor<Status, R> { public abstract R visitPENDING(); public abstract R visitPROGRESSING(); public abstract R visitDONE(); } }
When you add a new constant to the enumeration, if you do not forget to add the visitXXX method to the abstract class StatusVisitor, you will immediately have the compilation error that you would expect wherever you used the visitor (which should replace every switch you made on the enumeration):
switch(anObject.getStatus()) { case PENDING : [code1] break; case PROGRESSING : [code2] break; case DONE : [code3] break; }
should become:
StatusVisitor<String> v = new StatusVisitor<String>() { @Override public String visitPENDING() { [code1] return null; } @Override public String visitPROGRESSING() { [code2] return null; } @Override public String visitDONE() { [code3] return null; } }; v.visit(anObject.getStatus());
And now the ugly part, the EnumVisitor class. This is the highest class of the visitors hierarchy that implements the visit method and provides code at startup (test or application) if you forget to update the abstract visitor:
public abstract class EnumVisitor<E extends Enum<E>, R> { public EnumVisitor() { Class<?> currentClass = getClass(); while(currentClass != null && !currentClass.getSuperclass().getName().equals("xxx.xxx.EnumVisitor")) { currentClass = currentClass.getSuperclass(); } Class<E> e = (Class<E>) ((ParameterizedType) currentClass.getGenericSuperclass()).getActualTypeArguments()[0]; Enum[] enumConstants = e.getEnumConstants(); if (enumConstants == null) { throw new RuntimeException("Seems like " + e.getName() + " is not an enum."); } Class<? extends EnumVisitor> actualClass = this.getClass(); Set<String> missingMethods = new HashSet<>(); for(Enum c : enumConstants) { try { actualClass.getMethod("visit" + c.name(), null); } catch (NoSuchMethodException e2) { missingMethods.add("visit" + c.name()); } catch (Exception e1) { throw new RuntimeException(e1); } } if (!missingMethods.isEmpty()) { throw new RuntimeException(currentClass.getName() + " visitor is missing the following methods : " + String.join(",", missingMethods)); } } public final R visit(E value) { Class<? extends EnumVisitor> actualClass = this.getClass(); try { Method method = actualClass.getMethod("visit" + value.name()); return (R) method.invoke(this); } catch (Exception e) { throw new RuntimeException(e); } } }
There are several ways to implement / improve this code. I choose to go up the class hierarchy, stop when the superclass is EnumVisitor and read the parameterized type from there. You can also do this with a constructor parameter, which is an enum class.
You could use a smarter naming strategy to have less ugly names, etc.
The disadvantage is that it is a bit more verbose. Benefits
- compile-time error [in most cases, anyway]
- works even if you don’t own the listing code
- no dead code (default instruction for all enumeration values)
- sonar / pmd / ... not complaining that you have a switch statement without a default statement