Clases restringidas o selladas (sealed classes) en Kotlin
Antes de entrar en las clases selladas vamos a recordar lo que vimos en su día sobre los enumerados o enum class en Kotlin con un ejemplo:
enum class DiaSemana {LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO} fun main() { val diaSemana = DiaSemana.LUNES println(diaSemana) // LUNES }También se pueden inicializar haciendo referencia a una propiedad definida en el constructor (un valor):
enum class DiaSemana(val dia: Int) { LUNES(1), MARTES(2), MIERCOLES(3), JUEVES(4), VIERNES(5), SABADO(6), DOMINGO(7) }Pero además de valor pueden tener comportamiento, de dos maneras:
- Definiendo sus propios métodos, abstractos o no:
- Implementando interfaces, aunque no extendiendo otras clases:
enum class DiaSemana(val dia: Int) { LUNES(1) { override fun esFinDeSemana() = false }, MARTES(2) { override fun esFinDeSemana() = false }, MIERCOLES(3) { override fun esFinDeSemana() = false }, JUEVES(4) { override fun esFinDeSemana() = false }, VIERNES(5) { override fun esFinDeSemana() = false }, SABADO(6) { override fun esFinDeSemana() = true }, DOMINGO(7) { override fun esFinDeSemana() = true }; abstract fun esFinDeSemana(): Boolean fun anteriorA(diaSemana: DiaSemana): Boolean = dia < diaSemana.dia } fun main() { val dia1 = DiaSemana.JUEVES val dia2 = DiaSemana.DOMINGO println(dia1.esFinDeSemana()) // false println(dia2.esFinDeSemana()) // true println(dia1.anteriorA(dia2)) // true println(dia2.anteriorA(dia1)) // false }
interface Dia { fun esFinDeSemana(): Boolean fun esDiaSemana(): Boolean } enum class DiaSemana(val dia: Int) : Dia { LUNES(1) { override fun esFinDeSemana() = false }, MARTES(2) { override fun esFinDeSemana() = false }, MIERCOLES(3) { override fun esFinDeSemana() = false }, JUEVES(4) { override fun esFinDeSemana() = false }, VIERNES(5) { override fun esFinDeSemana() = false }, SABADO(6) { override fun esFinDeSemana() = true }, DOMINGO(7) { override fun esFinDeSemana() = true }; override fun esDiaSemana(): Boolean = true }
Imagina que queremos controlar los posibles errores de acceso en una pantalla de identificación con usuario y contraseña; tendremos un error si alguno de los campos está en blanco, otro si el usuario no está registrado, otro si la contraseña no corresponde al usuario registrado y otro desconocido por lo que pueda pasar. Esto lo podemos modelar de esta manera (para declarar una clase sellada colocamos el modificador sealed antes del nombre de la clase):
sealed class LoginError { data class Incompleto(val username: String, val password: String) : LoginError() class NoRegistrado(val username: String) : LoginError() class PassError(val password: String) : LoginError() object Indefinido : LoginError() }Las llaves son opcionales, por lo que podemos escribir los mismo de esta manera:
sealed class LoginError data class Incompleto(val username: String, val password: String) : LoginError() class NoRegistrado(val username: String) : LoginError() class PassError(val password: String) : LoginError() object Indefinido : LoginError()Donde LoginError es una clase abstracta que tiene únicamente cuatro hijos: Incompleto, NoRegistrado, PassError e Indefinido, y por tanto cuando manejamos un LoginError éste solo puede ser de uno de estos tipos. A partir de aquí podemos utilizar la expresión when de esta manera (con is en el caso de las clases y sin is en el caso de los objectos, y como una expresión no requiere else al contemplar todas las opciones posibles):
sealed class LoginError { data class Incompleto(val username: String, val password: String) : LoginError() class NoRegistrado(val username: String) : LoginError() class PassError(val password: String) : LoginError() object Indefinido : LoginError() } fun main() { val error = LoginError.PassError("1234") println(getError(error)) } fun getError(error: LoginError): String = when (error) { is LoginError.Incompleto -> "Campos de usuario y contraseña requeridos." is LoginError.NoRegistrado -> "El usuario ${error.username} no está registrado." is LoginError.PassError -> "Contraseña ${error.password} no reconocida" LoginError.Indefinido -> "Error desconocido." }Esto hace que si entramos, por ejemplo, en la opción NoRegistrado nuestra variable error sea inmediatamente (por conversión implícita o smart cast) un NoRegistrado en ese ámbito. Observa que, en el ejemplo, estamos accediendo al atributo password que no es compartido por todos los elementos.
Como vemos, frente a los enumerados, la principal ventaja de las clases selladas es el hecho de que los elementos son clases (class, data class, object e incluso sealed class), y que cada subclase puede tener sus propios valores y métodos (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).
Por tanto, las clases selladas, además de entenderse como una extensión de la clase enum o superenumerados, son 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; representan jerarquías de clases restringidas en las que un valor debe tener uno de los tipos de un conjunto limitado, pero no puede tener ningún otro tipo. Y aunque el conjunto de valores para un tipo enum también está restringido, para cada constante enum existe solo una única instancia (sólo tenemos un único objeto por tipo) mientras que una subclase de una clase sellada puede tener múltiples instancias que pueden contener estado.
Vamos a ver unos ejemplos utilizando sealed classes:
sealed class Expr data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() fun main() { val pi = Const(3.1415) println(eval(pi)) // 3.1415 println(eval(Sum(pi, pi))) // 6.283 } fun eval(expr: Expr): Double = when (expr) { is Const -> expr.number is Sum -> eval(expr.e1) + eval(expr.e2) NotANumber -> Double.NaN // else no es requerido porque cubrimos todos los casos }Otro ejemplo:
// enum class PuntoCardinal { NORTE, SUR, OESTE, ESTE } sealed class PuntoCardinal { class Norte(val value: Int) : PuntoCardinal() class Sur(val value: Int) : PuntoCardinal() class Oeste(val value: Int) : PuntoCardinal() class Este(val value: Int) : PuntoCardinal() } fun main() { var orientacion: PuntoCardinal orientacion = PuntoCardinal.Sur(30) println(rumbo(orientacion)) // 210 orientacion = PuntoCardinal.Este(10) println(rumbo(orientacion)) // 100 } fun rumbo(orientacion: PuntoCardinal) = when (orientacion) { is PuntoCardinal.Norte -> orientacion.value + 0 is PuntoCardinal.Este -> orientacion.value + 90 is PuntoCardinal.Sur -> orientacion.value + 180 is PuntoCardinal.Oeste -> orientacion.value + 270 }Y otro más:
sealed class Operacion { class Suma(val valor: Int) : Operacion() class Resta(val valor: Int) : Operacion() class Multiplicacion(val valor: Int) : Operacion() class Division(val valor: Int) : Operacion() } fun main() { val numero = 5 var operacion: Operacion operacion = Operacion.Suma(3) val numero2 = calcular(numero, operacion) println(numero2) // 8 operacion = Operacion.Multiplicacion(6) println(calcular(numero2, operacion)) // 48 } fun calcular(x: Int, op: Operacion) = when (op) { is Operacion.Suma -> x + op.valor is Operacion.Resta -> x - op.valor is Operacion.Multiplicacion -> x * op.valor is Operacion.Division -> x / op.valor }
Comentarios
Publicar un comentario