Sealed classes en Kotlin: ¿Por qué y para qué?

Facundo Rodríguez Arceri
Droid LATAM
Published in
4 min readAug 10, 2020

Este artículo es un resumen en forma escrita de mi charla en el canal Thank’s God It’s Kotlin, para aquellos que prefieren tener una referencia escrita en lugar de verla en video. De todas formas, si te interesa el video, te lo dejo por acá:

¿Por qué existen las sealed classes?

Hay veces en las que necesitamos representar una jerarquía limitada y cerrada de casos posibles para ciertos tipos de datos. Por lo general cuando se nos presenta esta situación, y ya tenemos experiencia con otros lenguajes de programación pensamos inmediatamente en los enumerados. Veamos qué podemos hacer con ellos modelando el sistema solar.

Usando enum para modelar los planetas

En ese ejemplo incluímos solo tres planetas (para no hacer demasiado largo el código de ejemplo) pero básicamente vemos las dos principales características de los enumerados: los subtipos posibles del tipo Planet son limitados: no pueden ser otros que los definidos dentro de este. Además, hemos definido un atributo id para cada Planeta, y una función orbit que todos estos deben implementar.

Las limitaciones también están a la vista: no pueden representarse subjerarquías dentro de Planet, cada tipo es concreto, motivo por el cual todas las instancias de este tipo tienen exactamente los mismos atributos y funciones, limitando la flexibilidad de esta herramienta.
Otra limitación que hay que tener clara es que cuando uno escribe enums lo que hace (tanto en Java como en Kotlin) es generar una única instancia de cada valor definido, por lo tanto no pueden existir dos objetos distintos que representen a, por ejemplo, Mercury.

Existe otro tipo de clase en Kotlin que puede ayudarnos a romper con estas limitaciones: las clases abstractas.

Usando abstract class para modelar los planetas

Vemos como ahora podemos crear subtipos dentro de Planet, en este caso BasicPlanet y PlanetWithSatellites, y a su vez crear a demanda las instancias que querramos, como se ve en la implementación de la función main.

De esta forma podemos representar subjerarquías, y que cada una de ellas agregue los atributos y funciones que sean necesarios para diferenciarse entre ellas. En el ejemplo, PlanetWithSatellites tiene un listado con los nombres de los satélites de los planetas que representa.

La principal limitación de las clases abstractas es que dejan de tener un conjunto limitado de valores, puesto que otros desarrolladores podría crear una clase que herede de Planet en cualquier otra parte del proyecto, por lo cual perdemos el control sobre los subtipos posibles. Las sealed classes vienen a cubrir este hueco que hay entre los enumerados y las clases abstractas. Veamos cómo:

Usando sealed class para modelar los planetas

El ejemplo es casi idéntico al de la clase abstracta, pero con una particularidad: para poder heredar de Planet se deberá escribir esa clase dentro del mismo módulo y paquete donde la sealed class está definida (o como clases anidadas, como se ve en el código), caso contrario el compilador dará un error. De esta forma recuperamos la ventaja que tenían los enumerados: los subtipos posibles están limitados. Anteriormente, las subclases de una sealed class debían estar todas dentro del mismo archivo que la clase padre, pero esto cambió desde Kotlin 1.5.0.

Cabe destacar también que, por definición, las sealed classes funcionan como clases abstractas y que los subtipos de estas pueden ser clases de cualquier tipo: class, abstract class, data class, object, open class y… ¡sealed class!. Por lo tanto las posibilidades son infinitas y es posible adaptar el uso de estas clases a nuestra necesidad puntual.

Sobre el uso de if y when como expresiones

Existe una similitud más entre los enums y las sealed classes, y es que estos tipos de datos son exhaustivos cuando usamos when o if como expresiones, es decir, cuando queremos obtener un resultado a partir de la ejecución de un condicional sobre un dato que es de estos tipos. Esto nos obliga a cubrir todos los casos posibles, caso contrario el compilador no sabrá de qué tipo será la variable resultante. En la siguiente captura podemos ver esto.

Y el código resultante al agregar las ramas de ejecución faltantes es el siguiente.

Por supuesto que también podríamos haber agregado un else y listo, pero esto no es recomendable, ya que en caso de agregar nuevos subtipos a la sealed class no se producirá un error de compilación en la aplicación. El uso exhaustivo con enums y sealed classes es una ventaja y podemos aprovecharnos de ello para forzar errores de compilación en lugares del código donde habría que tomar consideraciones adicionales al agregar nuevos tipos. El problema es que este uso exhaustivo funciona sólo al usar if I when como expresiones. Podemos forzar a que estas operaciones sean exhaustivas siempre agregando una extension property. Para ello dejo en la sección de Links de interés el enlace al artículo “Sealed with a class”, donde se explica en detalle cómo lograr esto.

¿Te gustaría ver algunos ejemplos de uso?

Dejo a continuación el enlace a un repositorio de ejemplo que desarrollé donde muestro el uso de sealed classes en tres situaciones disintas:

  • Para modelar jerarquías cerradas (Planet)
  • Para modelar estados posibles de una vista (PlanetsViewModel.State)
  • Para modelar distintas combinaciones de parámetros que pueden utilizarse para acceder a una Activity (PlanetDetailParams).

Links de interés

--

--