How to ensure completeness in an enum switch at compile time?

后端 未结 12 1000
悲&欢浪女
悲&欢浪女 2020-11-27 06:25

I have several switch statements which test an enum. All enum values must be handled in the switch statements by a case s

12条回答
  •  一向
    一向 (楼主)
    2020-11-27 06:52

    You could also use an adaptation of the Visitor pattern to enums, which avoid putting all kind of unrelated state in the enum class.

    The compile time failure will happen if the one modifying the enum is careful enough, but it is not garanteed.

    You'll still have a failure earlier than the RTE in a default statement : it will fail when one of the visitor class is loaded, which you can make happen at application startup.

    Here is some code :

    You start from an enum that look like that :

    public enum Status {
        PENDING, PROGRESSING, DONE
    }
    

    Here is how you transform it to use the visitor pattern :

    public enum Status {
        PENDING,
        PROGRESSING,
        DONE;
    
        public static abstract class StatusVisitor extends EnumVisitor {
            public abstract R visitPENDING();
            public abstract R visitPROGRESSING();
            public abstract R visitDONE();
        }
    }
    

    When you add a new constant to the enum, if you don't forget to add the method visitXXX to the abstract StatusVisitor class, you'll have directly the compilation error you expect everywhere you used a visitor (which should replace every switch you did on the enum) :

    switch(anObject.getStatus()) {
    case PENDING :
        [code1]
        break;
    case PROGRESSING :
        [code2]
        break;
    case DONE :
        [code3]
        break;
    }
    

    should become :

    StatusVisitor v = new StatusVisitor() {
        @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. It is the top class of the Visitor hierarchy, implementing the visit method and making the code fail at startup (of test or application) if you forgot to update the absract visitor :

    public abstract class EnumVisitor, R> {
    
        public EnumVisitor() {
            Class currentClass = getClass();
            while(currentClass != null && !currentClass.getSuperclass().getName().equals("xxx.xxx.EnumVisitor")) {
                currentClass = currentClass.getSuperclass();
            }
    
            Class e = (Class) ((ParameterizedType) currentClass.getGenericSuperclass()).getActualTypeArguments()[0];
            Enum[] enumConstants = e.getEnumConstants();
            if (enumConstants == null) {
                throw new RuntimeException("Seems like " + e.getName() + " is not an enum.");
            }
            Class actualClass = this.getClass();
            Set 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 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 you could implement / improve this glue code. I choose to walk up the class hierarchy, stop when the superclass is the EnumVisitor, and read the parameterized type from there. You could also do it with a constructor param being the enum class.

    You could use a smarter naming strategy to have less ugly names, and so on...

    The drawback is that it is a bit more verbose. The benefits are

    • compile time error [in most cases anyway]
    • works even if you don't own the enum code
    • no dead code (the default statement of switch on all enum values)
    • sonar/pmd/... not complaining that you have a switch statement without default statement

提交回复
热议问题