Clases sealed En Kotlin

En este tutorial verás cómo declarar clases sealed en Kotlin con el fin de restringir la creación de subclases de una clase.

El Modificador sealed

Marca una clase con el modificador sealed para que restrinja su jerarquía y especificar al compilador que solo existirán determinados descendientes y ninguno más.

Considera la declaración de clase de ejemplo Padre junto a sus descendientes HijoA, HijoB e HijoC.

sealed class Padre
class HijoA : Padre()
class HijoB : Padre()
class HijoC : Padre()

Al marcar a Padre con el modificador sealed no te será posible crear más subclases de esta clase por fuera del archivo.

Por eso al español podríamos referirnos a esta naturaleza como clases selladas, un adjetivo que representa hermetismo sobre la herencia.

Internamente las clases sealed en Kotlin:

  • Son clases abstractas
  • Tienen constructores son privados por defecto
  • Tienen subclases que deben declararse en el mismo archivo Kotlin

Ejemplo De Clases sealed En Kotlin

Tomemos como ilustración el diseño de un videojuego, donde se han modelado unidades través de la clase base GameUnit y dos subclases para aldeanos (Villager) y milicias (Militia).

open class GameUnit(var hitPoints: Int)
data class Villager(var health: Int = 25) : GameUnit(health)
data class Militia(var health: Int = 40, var armor: Int = 2) : GameUnit(health)

GameUnit establece los puntos de vida de cada unidad con hitPoints. La clase Villager proporciona un valor por defecto a los puntos de vida de 25.

Militia posee 40 puntos junto a una propiedad armor para representar los puntos de armadura contra cualquier tipo de daño.

Si quisieras calcular el daño causado a nuestras unidades podríamos crear una función que aplique sentencias de reducción de hitPoints junto a la expresión when:

private fun damage(unit: GameUnit, attack: Int): String {
    return when (unit) {
        is Villager -> {
            unit.hitPoints -= attack
            "El aldeano recibe $attack daños. Vida: ${unit.hitPoints}"
        }
        is Militia -> {
            val damage = max(0, attack - unit.armor)
            unit.hitPoints -= damage
            "La milicia recibe $damage daños. Vida: ${unit.hitPoints}"
        }
        else -> throw IllegalArgumentException("Unidad desconocidad")
    }
}

En el caso de los aldeanos el daño causado es plano, por lo que el ataque es recibido al 100%. Para la milicia la reducción es la resta entre el daño y los puntos de armadura, si la armadura es mayor al daño, entonces recibe 0 daños.

Con este código podrás simular los daños causados a varias unidades a disposición del jugador:

fun main() {
    // Unidades
    val units = listOf(
        Villager(), Militia(), Villager()
    )

    // Las unidades reciben daños
    units.forEach {
        val attack = Random.nextInt(1, 10)
        println(damage(it, attack))
    }

}

Salida:

El aldeano recibe 8 daños. Vida: 17
La milicia recibe 2 daños. Vida: 38
El aldeano recibe 4 daños. Vida: 21

Sin embargo, nuestra función puede ser reescrita de forma más corta como verás a continuación.

Expresión when con Clases sealed

Ya que la expresión when en el ejemplo anterior no es exhaustiva, el uso de return nos obliga a usar la rama para else con el fin de cubrir los demás casos.

else -> throw IllegalArgumentException("Unidad desconocidad")

Sin embargo, sabemos que no existen más casos. Y es justo allí donde el modificador sealed viene de maravilla para el diseño.

Si reemplazamos a open por sealed en GameUnit de la siguiente forma:

sealed class GameUnit(var hitPoints: Int)

Inmediatamente verás que IntelliJ IDEA muestra en gris el uso de la rama else, indicándote que la implementación es redundante.

Clases sealed en Kotlin. La rama else es redundante

Esto significa que el uso de las clases sealed le permite al compilador de Kotlin dar por terminada las comprobaciones de las posibles subclases.

private fun damage(unit: GameUnit, attack: Int): String {
    return when (unit) {
        is Villager -> {
            unit.hitPoints -= attack
            "El aldeano recibe $attack daños. Vida: ${unit.hitPoints}"
        }
        is Militia -> {
            val damage = max(0, attack - unit.armor)
            unit.hitPoints -= damage
            "La milicia recibe $damage daños. Vida: ${unit.hitPoints}"
        }
    }
}

De esta forma la restricción de la jerarquía de una clase te permitirá acotar las verificaciones de subtipos cuando sabes de antemano que tu modelo tiene limitaciones en su extensión.

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!