Grails, enumerados, combos, i18n y otras historias

Los tipos enumerados aparecieron en Java 5 para aliviar todo el uso (y abuso) de constantes que usábamos en nuestros proyectos. Y en Grails podemos usarlos como atributos en nuestras clases de dominio de una manera muy sencilla.

Antes de ver los enumerados, vamos a ver como manejábamos antes la situación sin ellos. Supongamos que tenemos una tabla y clase de dominio Usuario, y tenemos un campo estado en el que queremos guardar el estado de ese usuario. Podríamos tener algo así:

class Usuario {
    String nombre, password, email
    String estado

    static constraints = {
        estado(inList: estados)
    }

    static String estado_valido = "valido"
    static String estado_eliminado = "eliminado"
    static String estado_bloqueado = "bloqueado"
    static List estados = [estado_valido, estado_eliminado, estado_bloqueado]
}

El valor que le demos a las constantes valido, eliminado y bloqueado no importa demasiado, podrían haber sido los valores 0, 1 y 2 perfectamente. Pero es que ese valor es el que vamos a ver cuando consultemos la base de datos con SQL, así que es interesante que sea descriptivo, por lo que elegimos una cadena de texto.

¿Cuál es el problema? Que los estados no son más que valores, y no tienen información adicional. Además, mezclamos la definición de los estados en la propia clase. Puede que no parezca muy grave si solo tenemos tres estados, pero imagina que tuviéramos 20 estados. O imagina que, además, tuviéramos otros campos como “tipoDeUsuario”, “sexo”, “situaciónLaboral” o lo que sea, que también fueran susceptibles de tener varios valores como el campo estado. El resultado sería una clase llena de constantes, colecciones de constantes y atributos que usan esas constantes.

Con un enumerado todo esto es mucho más sencillo. Un enumerado es igual que una clase, pero define una serie de instancias únicas con un nombre concreto. Para crear el enumerado, podemos hacerlo en un fichero aparte, como si fuera una clase más, o podemos hacerlo dentro de nuestra clase de dominio:

class Usuario {
    String nombre, password, email
    Estado estado
}
enum Estado {
    valido, eliminado, bloqueado
}

Muy bien, y ahora ¿cómo accedemos a estos datos? Simplemente usamos cada enumerado como si fuera un valor estático de la clase Estado:

Usuario u = new Usuario(estado: Estado.valido) // creación
u.estado = Estado.eliminado // asignación
if (u.estado == Estado.bloqueado) println "Oh oh!" // lectura y comparación

Gorm se encargará de leer y persistir este dato en nuestra tabla, usando el propio nombre del enumerado “valido”, “eliminado” y “bloqueado”.

Métodos values() y name()

Los enumerados tienen dos métodos muy interesantes. Uno de ellos es values(), que se usa a nivel de clase y devuelve una colección con todos los enumerados posibles. Y el otro es name(), que se usa a nivel de instancia del enumerado y devuelve un String con el nombre de la instancia.

def u = new Usuario(estado: Estado.valido)
String nombreEstado = u.estado.name()
println nombreEstado // pintará "valido"

Estado.values().each { println it.name() } // pintará valido eliminado bloqueado

Para acceder a un enumerado usando su nombre, podemos hacerlo de varias maneras:

String nombreEstado = "valido"
println Estado[nombreEstado]
println Estado."$nombreEstado"

¿Para qué querríamos hacer una cosa así? Pues por ejemplo cuando el valor viene en un fichero de texto, o a través de un formulario como parámetro de una petición http. En este último caso no sería realmente necesario, pues el binding de Grails transforma automáticamente los campos de texto en su enumerado correspondiente, pero siempre es bueno saber como hacerlo a mano.

def u = new Usuario(params) // Esto funciona si hay un parámetro "estado"
u.estado = Estado."${params.otroEstado}" // Y esto también funciona

Añadiendo información a cada enumerado

Una de las ventajas que tiene el usar enumerados, es que podemos ampliar la información que tiene cada valor. Por ejemplo, en el caso de los estados, podemos añadir un texto con el nombre y un color, que usaremos después para mostrar el estado junto con los datos del usuario.

enum Estado {
    valido("Valido", "blue"),
    eliminado("Eliminado", "gray"),
    bloqueado("Bloqueado", "red")

    String text, color
    Estado(text, color) {
        this.text = text
        this.text = color
    }
}

Ahora, desde nuestros gsp, cuando mostremos la información del usuario, podemos hacer:

Nombre: ${usuario.nombre}, email: ${usuario.email}
Estado: <span style="color: ${usuario.estado.color};">${usuario.estado.text}</span>

Ponemos texto y color, pero bien podríamos haber puesto su código i18n y un nombre de estilo CSS.

Un ejemplo de uso desde un Gsp con un combo:

<g:select name="estado" from="${Estado.values()}" optionValue="text"/>

El atributo optionValue especifica el atributo del enumerado a mostrar en el combo (lo que ve el usuario). También se pueden definir un closure que se ejecutará en cada enumerado. Si no se especifica nada, se muestra el resultado de toString()

Por defecto, el key de cada elemento del combo es el toString() del enumerado, que devuelve lo mismo que name(). Pero si hemos sobreescrito el método toString() para que devuelva otra cosa, entonces tendremos que añadir el atributo optionKey a nuestra etiqueta, especificando un closure que devuelva el name() de cada enumerado:

<g:select name="estado" from="${Estado.values()}" optionValue="text"
          optionKey="${ {estado -> estado.name()} }"/>

Internacionalización en enums y combos

Si queremos internacionalizar nuestros enumerados y combos, solo tenemos que hacer que el atributo text tenga el código de nuestro resource bundle.

enum Estado {
    valido("enum.estado.valido", "blue"),
    eliminado("enum.estado.eliminado", "gray"),
    bloqueado("enum.estado.bloqueado", "red")

    String text, color
    Estado(text, color) {
        this.text = text
        this.text = color
    }
}

En nuestro messages_es.properties:

enum.estado.valido=Valido
enum.estado.eliminado=Eliminado
enum.estado.bloqueado=Bloqueado

Y a la hora de mostrar nuestros datos:

Nombre: ${usuario.nombre}, email: ${usuario.email}
Estado: <span style="color: ${usuario.estado.color}"><g:message code="${usuario.estado.text}"/></span>

Los combos tendrán ahora un closure como optionValue, en vez de un atributo como tenían antes, con el fin de transformar el código del atributo text, en su correspondiente traducción usando el taglib g.message :

<g:select name="estado" from="${Estado.values()}"
          optionValue="${ {estado -> g.message(code:estado.text)} }"/>

Aunque también podemos usar la solución que proponen en este post, aunque personalmente me parece un poco más rebuscada.

Lógica en los enumerados y buenas prácticas

Los enumerados al igual que las clases pueden tener métodos. Una buena práctica es separar la lógica de los enumerados en métodos dentro. ¿Para que querríamos hacer esto? A veces nuestra aplicación se debe comportar de manera diferente en función de si una variable es un enumerado u otro. Una manera de cambiar este comportamiento es añadir una condición que compare el valor de una variable con un enumerado (o varios) en concreto, y obrar en consecuencia. Sin embargo, esto es una mala práctica. Veamos como arreglarlo.

Ejemplo de código que sería una mala práctica:

// EJEMPLO DE MALA PRACTICA
def login(nombre, password) {
    def usuario = Usuario.findByNombreAndPassword(nombre, password)
    if (usuario.estado == Estado.bloqueado || usuario.estado == Estado.eliminado) // NO PASAR
    else …. // OK
}

Estamos usando el valor que hay en usuario.estado para compararlo dos enumerados (bloqueado y eliminado). Si la condición es cierta, no se permite la entrada al usuario a la aplicación. Pero, ¿no sería más legible si, en vez de comparar los valores, le preguntáramos al enumerado si se le permite la entrada a la aplicación?

// EJEMPLO DE BUENA PRACTICA
def login(nombre, password) {
    def usuario = Usuario.findByNombreAndPassword(nombre, password)

    if (usuario.estado.canLogin()) // NO PASAR
    else …. // OK
}

Queda bastante más claro. Ahora tenemos que ser capaces de que cada enumerado tenga un método que devuelva true o false si se le permite entrar en la aplicación o no. La manera más cómoda de hacerlo es añadiendo un atributo boolean a cada enumerado, especificando su valor durante su creación. Y que el método canLogin() devuelva este valor, que será distinto para cada enumerado.

enum Estado {
    valido(true), eliminado(false), bloqueado(false)

    boolean logable
    Estado(logable) {
        this.logable = logable
    }
    boolean canLogin() { logable }
}

Esto tiene la gran ventaja de que si añadimos un nuevo enumerado, no tendremos que buscar por toda nuestra aplicación donde se usa para modificar las condiciones. O si queremos cambiar el comportamiento de la aplicación donde se use el enumerado, solo tendremos que modificar la implementación, no todas las veces donde se use.

En general, podemos decir que tener en la lógica de nuestra aplicación una condición en la que se compara un enumerado con otro es siempre una mala práctica. En su lugar, se debería usar un método dentro del enumerado que devuelva el resultado de esta condición. Y que cada enumerado defina, en su creación como atributos, los valores necesarios que se usarán para evaluar dicha condición.

Resumiendo…

Y nada más. El soporte de enumerados en Grails es muy bueno: se transforman correctamente en el binding de los controladores y se persisten facilmente en nuestras clases de dominio. Y desde el punto de vista del diseño, el código es más claro y estructurado, y permite añadir atributos y lógica a cada enumerado. En definitiva, usar enumerados es una gran solución que solo proporciona ventajas.

Bola extra: alguien se preguntará para que sirve el método name(), si el método toString() de los enumerados ya devuelve el nombre de éste. Sencillo: toString() se puede sobreescribir, y name() no porque es final. Y esto es bueno porque name() nunca debe cambiar, ya que el nombre es una propiedad intrínseca e inmutable de cada enumerado. Y el toString() es simplemente su representación visual, y puede devolver cualquier cadena que decida el que diseñe la aplicación.