Clases selladas y enumerados en Kotlin

0
483

Índice de contenidos

1. Introducción

Las clases selladas (sealed class) en Kotlin son aquellas empleadas para construir una herencia cerrada en la que el compilador conoce cuáles son las únicas clases hijas, ya que no puede haber otras.

Otra definición —rápida e informal— de las clases selladas podría ser la de «enumerados híper vitaminados». Esto es porque su uso se da en los mismos casos que los de los enumerados —cuando tenemos un valor que puede ser de un solo tipo de un conjunto específico de tipos— pero nos permiten que cada elemento de ese «híper enumerado» sea una clase, con las ventajas que ello conlleva frente a las limitaciones de los clásicos enumerados.

2. Enumerados

Antes de ver las clases selladas, echemos un vistazo a los enumerados en Kotlin. Estos se escriben de la siguiente manera:

Pueden tener valor y comportamiento:

2.1. Valor

Por cierto, los guiones bajos empleados como separador decimal sirven únicamente para mejorar la legibilidad del código y su uso es opcional.

2.2. Comportamiento

Pueden tener comportamiento de dos maneras:

a) Definiendo sus propios métodos, abstractos o no:

b) Implementando interfaces, aunque no extendiendo otras clases:

Como vemos, podemos implementar los métodos de las interfaces tipo a tipo (getDensity()) o de forma única para todos los tipos (isPlanet()).

Sí, están fuertes los enumerados de Kotlin, pero espera a ver las clases selladas, que son más versátiles.

3. Clases selladas

La principal limitación de los enumerados es que todos los subtipos siguen una misma estructura. Por ejemplo, en el caso de los planetas todos tenían un radio, un método para obtener su densidad, otro para obtener su nombre en español, etc. Sin embargo, cada subtipo (subclase) de una clase sellada es una clase que puede ser de la manera que quiera, con sus propios valores y métodos.

3.1. Ejemplo

Imaginemos que hacemos peticiones a un repositorio y queremos controlar posibles errores. Sabemos que tendremos un error si el usuario no está autorizado, otro si no tiene permisos para acceder al recurso y otro desconocido por lo que pueda pasar. En el caso de no estar autorizado, el repo nos dice el nombre del usuario, mientras que en el caso de falta de permisos el repo nos responde con una lista de roles de nuestro usuario; en caso de error desconocido, no tenemos ninguna información adicional.

Para modelar este conjunto de errores ya no nos sirven los enumerados pero… ¡sí las clases selladas!

3.2. Definición

El ejemplo anterior lo modelaríamos de la siguiente manera:

Las llaves son opcionales, por lo que es válido también lo siguiente:

RepositoryFailure es una clase abstracta que tiene únicamente tres hijas: Unauthorized, Forbidden y Unknown. Por tanto, cuando manejemos un RepositoryFailure este solamente podrá ser de estos tipos. Por cierto, estamos obligados a definir las subclases en el mismo fichero que la clase sellada madre.

3.3. Ventajas de las clases selladas

Las ventajas frente a los enumerados vienen dadas por lo que ya hemos comentado, que es el hecho de que los elementos sean clases (class, data class, object e incluso sealed class):

  • La más importante es que cada subclase puede tener sus propios valores y sus propios métodos, a diferencia de los enumerados, cuyos elementos siguen todos la misma estructura.
  • Además, los enumerados solamente pueden tener una instancia, mientras que las subclases de clases selladas pueden tener varias instancias, cada una con su estado, o una si la definimos como object.

4. Manejo de enumerados y clases selladas

Dado un enumerado o una clase sellada, su uso en un condicional puede ser como el siguiente:

Como vemos, simplemente estamos definiendo comportamiento para un subconjunto de los planetas pero, normalmente, queremos definirlo para todos los casos. Podríamos poner todos los subtipos en el when, pero el código tendría un problema: si en un futuro Plutón da el estirón y pasa a ser considerado planeta, aunque nosotros lo metamos en el enumerado, en el código anterior no le estaremos dando comportamiento. Podríamos pensar en crear los condicionales con un else, pero lo que en realidad estaría chulo es que el compilador nos avisase de aquellos fragmentos de código en los que no hemos tenido en cuenta al pobre Plutón. Y esto en Kotlin se puede hacer si utilizamos los condicionales como expresiones.

4.1. Condicionales como expresiones

Una (otra) de las maravillas de Kotlin es que los condicionales se pueden usar no solamente como declaraciones, sino también como expresiones, es decir, como sentencias que devuelven valor. Por ejemplo:

o:

Cuando los condicionales son utilizados como expresiones, el compilador nos obliga a contemplar todos los casos, lo cual hacemos con un else en el par de ejemplos anteriores.

4.2. Enumerados y clases selladas en condicionales como expresiones

En el caso de los enumerados y clases selladas no es necesario añadir ningún else para que el compilador no se queje, sino que nos basta con definir todos los casos:

Además, si ahora añadimos Plutón al enumerado de planetas, el código anterior no compilaría. Y esto es lo deseable, ya que los errores los queremos en tiempo de compilación, no de ejecución.

Y para terminar, si empleamos clases selladas, entra en juego el smart cast y ya nuestra experiencia de programación es colosal:

El smart cast de Kotlin hace que, si entramos en la rama de Unauthorized, por ejemplo, nuestra variable failure sea ya un Unauthorized en ese ámbito y nos evitar tener que hacer un cast. Como vemos, en el ejemplo estamos accediendo al atributo username que solamente el tipo Unauthorized tiene.

Por cierto, el condicional de las ramas de un when para comprobar el tipo de una instancia se construye con is en el caso de las clases y sin is en el caso de los objects.

4.3. Extra: exhaustive

Personalmente, intento utilizar siempre condicionales como expresiones para manejar enumerados y clases selladas, así estoy obligado a declarar el comportamiento para cada subtipo. Sin embargo, no siempre quiero estructurar el código así, por la razón que sea, pero tampoco quiero perder la detección del compilador de que me estoy dejando algo.

Pues bien, esto lo podemos solucionar añadiendo al final del when una llamada a exhaustive:

Al hacer esto, nuestro código deja de compilar porque no se están contemplando todos los casos.

No lo entiendo.

Vayamos por partes. exhaustive es una propiedad de extensión que nos creamos así:

Como extensión que es, se puede llamar desde cualquier tipo (Any?). Definimos el get() para que no devuelva nada, pues si quisiésemos el valor devuelto por el when, entonces usaríamos este directamente como expresión, sin tener que recurrir al exhaustive.

Al añadirlo a un when de tipo declaración lo convertimos a expresión porque el exhaustive es una extensión de un tipo, que será el que el when devuelva. Y, al ser una expresión, ya estamos obligados a dar comportamiento a todos los tipos.

En fin, un pequeño hack aprovechando las posibilidades de Kotlin.

5. Conclusión

Las clases selladas son la alternativa a utilizar cuando los enumerados se nos quedan cortos, cuando queremos una herencia cerrada. Son muy útiles, por ejemplo, para modelar errores en el caso en el que no todos tengan la misma estructura. Y su uso con when como expresión y smart cast hace nuestro código robusto y legible, respectivamente.

6. Referencias

Documentación oficial de Kotlin:

Dejar respuesta

Please enter your comment!
Please enter your name here