Pregunta ¿Cómo asegurar la integridad en un conmutador de enumeración en tiempo de compilación?


Tengo varias declaraciones de cambio que prueban una enum. Todas enum los valores deben ser manejados en switch declaraciones de un case declaración. Durante la refabricación de código, puede ocurrir que enum se encoge y crece Cuando el enum se encoge el compilador arroja un error. Pero no se produce ningún error, si el enum crece El estado de coincidencia se olvida y produce un error de tiempo de ejecución. Me gustaría mover este error desde el tiempo de ejecución hasta el tiempo de compilación. Teóricamente debería ser posible detectar la falta enum casos en tiempo de compilación. ¿Hay alguna forma de lograr esto?

La pregunta ya existe "Cómo detectar un nuevo valor se agregó a una enumeración y no se maneja en un conmutador"pero no contiene una respuesta solo a un trabajo relacionado con Eclipse".


32
2018-05-28 16:57


origen


Respuestas:


Otra solución utiliza el enfoque funcional. Solo necesita declarar la clase enum de acuerdo con la siguiente plantilla:

public enum Direction {

    UNKNOWN,
    FORWARD,
    BACKWARD;

    public interface SwitchResult {
        public void UNKNOWN();
        public void FORWARD();
        public void BACKWARD();
    }

    public void switchValue(SwitchResult result) {
        switch (this) {
            case UNKNOWN:
                result.UNKNOWN();
                break;
            case FORWARD:
                result.FORWARD();
                break;
            case BACKWARD:
                result.BACKWARD();
                break;
        }
    }
}

Si intentas usar esto sin una constante de enumeración al menos, obtendrás el error de compilación:

getDirection().switchValue(new Direction.SwitchResult() {
    public void UNKNOWN() { /* */ }
    public void FORWARD() { /* */ }
    // public void BACKWARD() { /* */ } // <- Compilation error if missing
});

4
2017-12-12 17:16



En Java efectivo, Joshua Bloch recomienda crear un método abstracto que se implementaría para cada constante. Por ejemplo:

enum Color {
    RED   { public String getName() {return "Red";} },
    GREEN { public String getName() {return "Green";} },
    BLUE  { public String getName() {return "Blue";} };
    public abstract String getName();
}

Esto funcionaría como un cambio seguro, lo que le obligaría a implementar el método si agrega una nueva constante.

EDITAR: Para aclarar un poco de confusión, aquí está el equivalente usando un regular switch:

enum Color {
    RED, GREEN, BLUE;
    public String getName() {
        switch(this) {
            case RED:   return "Red";
            case GREEN: return "Green";
            case BLUE:  return "Blue";
            default: return null;
        }
    }
}

18
2018-05-28 17:56



No sé sobre el compilador estándar de Java, pero el compilador de Eclipse ciertamente puede configurarse para advertir sobre esto. Vaya a Ventana-> Preferencias-> Java-> Compilador-> Errores / Advertencias / Constante de tipo Enum no cubierto en el interruptor.


10
2018-05-28 17:02



En mi opinión, y si el código que va a ejecutar está fuera del dominio de su enumeración, una forma de hacerlo es crear un caso de prueba unitaria que recorra sus elementos en la enumeración y ejecute el fragmento de código que contiene el interruptor. Si algo sale mal o no como se espera, puede verificar el valor de retorno o el estado del objeto con una aserción.

Podría ejecutar las pruebas como parte de algún proceso de construcción y verá anomalías en este punto.

De todos modos, las pruebas unitarias son casi obligatorias y beneficiosas en muchos proyectos.

Si el código dentro del interruptor pertenece a la enumeración, inclúyalo dentro de lo propuesto en otras respuestas.


3
2018-05-28 18:23



Probablemente una herramienta como FindBugs marcará tales conmutadores.

La respuesta difícil sería refactorizar:

Posibilidad 1: puede ir orientado a objetos


2
2018-05-28 17:16



También puede usar una adaptación del patrón Visitor a enumeraciones, que evitan poner todo tipo de estado no relacionado en la clase enum.

La falla del tiempo de compilación ocurrirá si el que modifica la enumeración es lo suficientemente cuidadoso, pero no está garantizado.

Aún tendrá una falla anterior a la RTE en una declaración predeterminada: fallará cuando se cargue una de las clases de visitantes, lo que puede suceder al inicio de la aplicación.

Aquí hay un código:

Comienzas desde una enumeración que se ve así:

public enum Status {
    PENDING, PROGRESSING, DONE
}

Aquí es cómo lo transforma para usar el patrón de visitante:

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();
    }
}

Cuando agrega una nueva constante a la enumeración, si no olvida agregar el método visitXXX a la clase abstracta StatusVisitor, tendrá directamente el error de compilación que espera en todos los lugares donde utilizó un visitante (que debería reemplazar cada uno de los conmutadores que realizó) en la enumeración):

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

debe convertirse :

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());

Y ahora la parte fea, la clase EnumVisitor. Es la clase superior de la jerarquía de Visitantes, implementando el método de visita y haciendo que el código falle al inicio (de prueba o aplicación) si olvidó actualizar el visitante de absract:

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);
        }
    }
}

Hay varias formas de implementar / mejorar este código de pegamento. Elijo subir la jerarquía de clases, parar cuando la superclase es EnumVisitor, y leer el tipo parametrizado desde allí. También podrías hacerlo con un parámetro de constructor que sea la clase enum.

Podría usar una estrategia de nombres más inteligente para tener nombres menos feos, y así sucesivamente ...

El inconveniente es que es un poco más detallado. Los beneficios son

  • error de tiempo de compilación [en la mayoría de los casos de todos modos]
  • funciona incluso si no posee el código enum
  • sin código muerto (la declaración por defecto de activar todos los valores enum)
  • sonar / pmd / ... no se queja de que tiene una instrucción switch sin declaración predeterminada

2
2017-10-02 12:47



los Proyecto Enum Mapper proporciona un procesador de anotación que se asegurará en tiempo de compilación de que se manejan todas las constantes enum.
Además, admite búsquedas inversas y mapeos parciales.

Ejemplo de uso:

@EnumMapper
public enum Seasons {
  SPRING, SUMMER, FALL, WINTER
}

El procesador de anotaciones generará una clase java Seasons_MapperFull, que se puede usar para asignar todas las constantes enum a valores arbitrarios.

Aquí hay un ejemplo de uso donde asignamos cada constante enum a una cadena.

EnumMapperFull<Seasons, String> germanSeasons = Seasons_MapperFull
     .setSPRING("Fruehling")
     .setSUMMER("Sommer")
     .setFALL("Herbst")
     .setWINTER("Winter");

Ahora puede usar el asignador para obtener los valores o realizar búsquedas inversas

String germanSummer = germanSeasons.getValue(Seasons.SUMMER); // returns "Sommer"
ExtremeSeasons.getEnumOrNull("Sommer");                 // returns the enum-constant SUMMER
ExtremeSeasons.getEnumOrRaise("Fruehling");             // throws an IllegalArgumentException 

2
2018-05-18 09:11



Esta es una variante del enfoque Visitor que le brinda ayuda en tiempo de compilación cuando agrega constantes:

interface Status {
    enum Pending implements Status {
        INSTANCE;

        @Override
        public <T> T accept(Visitor<T> v) {
            return v.visit(this);
        }
    }
    enum Progressing implements Status {
        INSTANCE;

        @Override
        public <T> T accept(Visitor<T> v) {
            return v.visit(this);
        }
    }
    enum Done implements Status {
        INSTANCE;

        @Override
        public <T> T accept(Visitor<T> v) {
            return v.visit(this);
        }
    }

    <T> T accept(Visitor<T> v);
    interface Visitor<T> {
        T visit(Done done);
        T visit(Progressing progressing);
        T visit(Pending pending);
    }
}

void usage() {
    Status s = getRandomStatus();
    String userMessage = s.accept(new Status.Visitor<String>() {
        @Override
        public String visit(Status.Done done) {
            return "completed";
        }

        @Override
        public String visit(Status.Progressing progressing) {
            return "in progress";
        }

        @Override
        public String visit(Status.Pending pending) {
            return "in queue";
        }
    });
}

Hermoso, ¿eh? Lo llamo la "Solución de Arquitectura Rube Goldberg".

Normalmente, simplemente usaría un método abstracto, pero si realmente no desea agregar métodos a su enumeración (tal vez porque introduce dependencias cíclicas), esta es una forma.


0
2018-01-28 08:06