Se viene Java 8... ¿conocés a fondo los enums que se agregaron hace tiempo en Java 5?

Es una lástima que aún hoy en día se subestiman los enums en Java, ya que son más poderosos de lo que parecen, y no sólo pueden usarse como constantes enumeradas!

Cyrille Martraire comparte una serie de interesantes ejemplos para aprender más sobre los enum en Java.

Los enum de Java son polimórficos

Los enums de Java son clases reales que pueden contener comportamiento y datos.

Representemos el clásico juego Piedra, Papel o Tijera usando los enum con un único método. Primero los tests para definir el comportamiento:

@Test
public void papel_ganaContra_roca() {
    assertThat(PAPEL.ganaContra(ROCA)).isTrue();
    assertThat(ROCA.ganaContra(PAPEL)).isFalse();
}
@Test
public void tijeras_ganaContra_papel() {
    assertThat(TIJERAS.ganaContra(PAPEL)).isTrue();
    assertThat(PAPEL.ganaContra(TIJERAS)).isFalse();
}
@Test
public void roca_ganaContra_tijeras() {
    assertThat(ROCA.ganaContra(TIJERAS)).isTrue();
    assertThat(TIJERAS.ganaContra(ROCA)).isFalse();
}
 

Y a continuación la implementación del enum, que utiliza el ordinal entero de cada constante del enum (como ser que el elemento N+1 le gana al elemento N). Muchas veces resulta útil esta relación entre la constante enum y su ordinal entero.

/** Enums have behavior! */
public enum Gesto {
    ROCA() {
      // Los Enums son polimorficos, y es muy útil!
        @Override
        public boolean ganaContra(Gesto other) {
            return other == TIJERAS;
        }
    },
    PAPEL, TIJERAS;
 
    // podemos implementarlo con la reprensetación a enteros
    public boolean ganaContra(Gesto other) {
        return ordinal() - other.ordinal() == 1;
    }
}
 

Noten que no hay ningún IF en el código, toda la lógica de negocio está manejada por el entero ordinal y por polimorfismo, en donde sobreescribimos el método para el caso ROCA. Si el ordenamiento entre los elementos no fuera cíclico podríamos implementarlo usando el órden natural de cada enum, aquí el polimorfismo ayuda con el ciclo.

Este enum de Java es también un ejemplo perfecto de que podemos tener la torta (ofrecer un buen API orientado a objetos con nombres claros), y también comerla (implementarlo con una lógica simple y con enteros, como en los viejos tiempos).

En mis últimos proyectos utilicé muchos enums para substituir clases: son singleton garantizados, tienen órden, hashcode, equals, serialización desde y hacia texto; todo esto ya heredado sin ensuciar el código.

Si están buscando implementar Value Objects y si parte del dominio puede representarse con un conjunto limitado de instancias, es posible un enum sea lo que necesitan! Son un poco parecidos a las Sealed Case Class en Scala, excepto que los enums están totalmente restringidos a un conjunto de instancias definidas en tiempo de compilación. Esto último es una limitación real, pero si usan despligues continuos probablemente pueden esperar a la siguiente entrega para agregar ese caso extra.

Los enum son una buena opción para el patrón Strategy

Vayamos ahora a un ejemplo con sobre el canal de televisión Eurovisión y su concurso de bandas: queremos configurar el comportamiento sobre notificar (o no) al usuario de cualquier evento de Eurovision. Es importante. Y lo podemos hacer con un enum:

/** La regla sobre como notificar a un usuario de cualquier evento de Eurovision */
public enum EurovisionNotification {
 
 /** Me encanta Eurovision, no me lo quiero perder nunca! */
 SIEMPRE() {
    @Override
    public boolean debeNotificar(String ciudadDelEvento, String ciudadDelUsuario) {
        return true;
    }
 },
 
 /**
  * Sólo quiero enterarme sobre Eurovision si el evento es en mi ciudad, así puedo irme de
  * vacaciones a cualquier otro lugar durante ese momento.
  */
 SOLO_SI_ES_MI_CIUDAD() {
    @Override
    public boolean debeNotificar(String ciudadDelEvento, String ciudadDelUsuario) {
        return ciudadDelEvento.equalsIgnoreCase(ciudadDelUsuario);
    }
 },
 
 /** No me importa, no quiero saber.  */
 NUNCA() {
    @Override
    public boolean debeNotificar(String ciudadDelEvento, String ciudadDelUsuario) {
        return false;
    }
 };
 
 // no hay comportamiento predeterminado.
 public abstract boolean debeNotificar(String ciudadDelEvento, String ciudadDelUsuario);
}
 

Y la prueba unitaria para el caso de SOLO_SI_ES_MI_CIUDAD:

@Test
public void notify_users_in_Baku_only() {
    assertThat(SOLO_SI_ES_MI_CIUDAD.debeNotificar("Baku", "BAKU")).isTrue();
    assertThat(SOLO_SI_ES_MI_CIUDAD.debeNotificar("Baku", Paris")).isFalse();
}

Aqui definimos un método abstracto, para implementarlo en cada caso. Una alternativa sería implementar el comportamiento predeterminado y sólo sobreescribirlo para los casos donde tenga sentido, al igual que hicimos con el ejemplo de Piedra-Papel-Tijera.

Nuevamente, no necesitamos un switch para elegir el comportamiento del iterador, en cambio usamos el polimorfismo. Lo más probable es que nunca necesitemos un switch con los enum, excepto por motivos de dependencias. Por ejemplo cuando el enum es parte de un mensaje hacia el mundo exterior en un Data Transfer Object (DTO), no queremos que ninguna dependencia en el código interno del enum o su firma.

Funciona perfecto con el patrón State

Al igual que con el patrón Strategy, los enum de Java funcionan muy bien con máquinas de estado finito, donde por definición el conjunto de posibles estados es finito.

Hagamos un ejemplo usando un bebé con estados finitos:

/**
 * Los estados principales de un bebé (simplificado)
 */
public enum BabyState {
 
    CAGAR(null), DORMIR(CAGAR), COMER(DORMIR), LLORAR(COMER);
 
    private final BabyState siguiente;
 
    private BabyState(BabyState siguiente) {
        this.siguiente = siguiente;
    }
 
    public BabyState siguiente(boolean descontento) {
        if (descontento) {
            return LLORAR;
        }
        return siguiente == null ? COMER : siguiente;
    }
}
 

Y por supuesto las pruebas unitarias del comportamiento:

@Test
public void comer_luego_dormir_luego_cagar_y_repetir() {
    assertThat(COMER.siguiente(NO_DESCONTENTO)).isEqualTo(DORMIR);
    assertThat(DORMIR.siguiente(NO_DESCONTENTO)).isEqualTo(CAGAR);
    assertThat(CAGAR.siguiente(NO_DESCONTENTO)).isEqualTo(COMER);
}
 
@Test
public void si_descontento_entonces_LLORAR_luego_COMER() {
    assertThat(DORMIR.siguiente(DESCONTENTO)).isEqualTo(LLORAR);
    assertThat(LLORAR.siguiente(NO_DESCONTENTO)).isEqualTo(COMER);
}
 

Si, podemos referenciar a las constantes del enum entre ellas, con la restricción que sólo las constantes definidas antes pueden ser referenciadas. Aqui tenemos un ciclo entre los estados COMER -> DORMIR -> CAGAR -> COMER, por lo que necesitamos abrir el ciclo y cerrarlo con un arreglo en tiempo de ejecución.

De hecho tenemos un grafo con el estado LLORAR que puede accederse desde cualquier otro estado.

Utilicé enums para representar árboles simples de categorías simplemente referenciando en cada nodo a sus elementos, todo con constantes enum.

Colecciones optimizadas con enums

Los enum también se benefician de tener implementaciones dedicadas de Map y Set: EnumMap y EnumSet.

Estas colecciones tienen la misma interfaz y se comportan igual que las colecciones normales, pero internamente aprovecha la naturaleza entera de los enums, como optimizaciones. En breve, tenemos estructuras al estilo de C (bit masking y demás) ocultas detrás de una interfaz elegante. ¡Otra demostración de que no es necesario comprometer el API en pos del rendimiento!

Para ilustrar el uso de estas colecciones dedicadas, representemos "Delegation Poker", un juego de 7 cargas de Jurgen Appelo.

public enum AuthorityLevel {
 
 /** tomar decisiones como un gerente */
 ORDENAR, 
 
 /** convencer a las personas sobre la decisión */
 VENDER,
 
 /** obtener feedback del equipo sobre la decisión */
 CONSULTAR,
 
 /** tomar la decisión en conjunto con el equipo */
 ACORDAR,
 
 /** influenciar la decisión tomada por el equipo */
 ACONSEJAR,
 
 /** pedir feedback luego de la decisión del equipo */
 PREGUNTAR,
 
 /** no influenciar, dejar que el equipo lo resuelva */
 DELEGAR;
 

Hay 7 cartas, las primeras 3 son orientas al control, la del medio es balanceada, y las últimas 3 cartas son orientadas a la delegación (me inventé esta interpretación, lean el libro para más explicaciones). En Delegation Poker, cada jugador elige una carta para una situación dada, y gana tantos puntos como la carta (de 1 a 7), excepto el jugador en la "minoría más alta".

Resulta trivial calcular la cantidad de puntos usando el valor ordinal + 1. También es simple seleccionar las cartas orientadas al control usando su valor ordinal, o podemos usar un Set construido con el método range para seleccionar las cartas orientadas a la delegación, como vemos a continuación:

public int cantidadDePuntos() {
    return ordinal() + 1;
 }
 
 // Está bien usar la representación entera para la implementación
 public boolean isControlOriented() {
    return ordinal() < ACORDAR.ordinal();
 }
 
 // EnumSet es una implementación de Set que se beneficia 
 // de la naturaleza de enteros de los enums
 public static Set NIVELES_DE_DELEGACION = EnumSet.range(ACONSEJAR, DELEGAR);
 
 // los enums son comparables, por lo cual tenemos los beneficios habituales
 public static AuthorityLevel mayor(List niveles) {
    return Collections.max(niveles);
 }
}
 

EnumSet ofrece métodos estáticos de creación como el range(desde, hasta) para crear un conjunto que incluya cada constante enum entre ACONSEJAR y DELEGAR, en órden de declaración.

Para calcular la minoría más alta comenzamos por la carta más alta, que significa encontrar el más alto, algo que resulta trivial ya que los enum son siempre comparables.

Cuando necesitemos usar este enum como un Map, debemos usar la clase EnumMap, como vemos en el siguiente ejemplo:

 
// Uso de un EnumMap para representar los vatos de varios niveles
@Test
public void votos_con_mayoria_clara() {
  final Map<AuthorityLevel, Integer> votos = new EnumMap(AuthorityLevel.class);
  votos.put(VENDER, 1);
  votos.put(ACONSEJAR, 3);
  votos.put(PREGUNTAR, 2);
  assertThat(votos.get(ACONSEJAR)).isEqualTo(3);
}
 

¡Los enums en Java son buenos!

Me encantan los enums en Java: son perfectos para Value Objects en un diseño orientado al dominio en donde un cojunto de valores está acotado. En un proyecto reciente conseguí tener una gran parte de tipos de datos expresado como enums. Logré algo increíble gratis, y especialmente sin casi ningún ruido técnico. Esto ayuda a mejorar el ratio señal/ruido entre las palabras del dominio y el lenguaje técnico.

Por supuesto que me aseguro que cada constante del enum es también inmutable, y logro obtener equals correctos, hashcode, toString, serialización con String o entero, singletons y colecciones muy eficientes, y todo esto gratis y con poquito código.

El polimorfismo en los enum es muy práctico, y nunca use instaceof en los enums y casi nunca necesito un switch tampoco.

Me encantaría que el enum en Java se completara con un constructor como un case class en Scala, para cuando el conjunto de valores posibles no pueda estar acotado. Y también sería bueno alguna forma de garantizar la inmutabilidad de cualquier clase. ¿Estoy pidiendo mucho?

También ni intenten comparar los enum en Java con los enums de C#...

Traducido de Java Enums: You have grace, elegance and power and this is what I Love!, por Cyrille Martraire.

Seguinos en Facebook.

Publicá tus artículos.

Publicar Convertite en redactor para Dos Ideas y compartí tus conocimientos a una comunidad que sigue creciendo!
Quiero publicar

Inspiración.

"Si tú tienes una manzana y yo tengo una manzana e intercambiamos las manzanas, entonces tanto tú como yo seguiremos teniendo una manzana cada uno. Pero si tú tienes una idea y yo tengo una idea, e intercambiamos las ideas, entonces ambos tendremos dos ideas"

Bernard Shaw