5.D Jerarquías cerradas y clases de datos¶
El problema: un conjunto de variantes conocido y cerrado¶
A veces los distintos tipos no se van a ampliar: un pago es en efectivo, con tarjeta o por Bizum, y no hay una cuarta posibilidad que inventar desde fuera. Con una jerarquía normal, quien use el tipo base nunca está seguro de haber cubierto todos los casos.
Una jerarquía cerrada (sealed) le dice al compilador exactamente cuáles son los subtipos posibles. Así, cuando se hace algo distinto según el subtipo, el compilador comprueba que no falte ninguno: si más adelante se añade un caso nuevo, avisa en todos los sitios donde falta tratarlo.
En Kotlin se declara sealed interface (o sealed class): las subclases deben estar en el mismo paquete y módulo, y el compilador conoce la lista completa. Con data class para los datos y when con is, que exige cubrir todos los casos; Kotlin además convierte (smart cast) el valor al tipo concreto dentro de cada rama.
sealed interface Pago
data class Efectivo(val importe: Int) : Pago
data class Tarjeta(val importe: Int, val ultimos4: String) : Pago
data class Bizum(val importe: Int, val telefono: String) : Pago
fun describir(pago: Pago) = when (pago) {
is Efectivo -> "Efectivo de ${pago.importe} €"
is Tarjeta -> "Tarjeta ****${pago.ultimos4} de ${pago.importe} €"
is Bizum -> "Bizum al ${pago.telefono} de ${pago.importe} €"
}
fun comision(pago: Pago) = when (pago) {
is Efectivo -> 0
is Tarjeta -> pago.importe * 2 / 100
is Bizum -> 1
}
fun main() {
val pagos = listOf<Pago>(Efectivo(50), Tarjeta(100, "1234"), Bizum(30, "600123456"))
var total = 0
for (pago in pagos) {
val c = comision(pago)
total += c
println("${describir(pago)} -> comisión $c €")
}
println("total de comisiones: $total €")
}
Salida:
Efectivo de 50 € -> comisión 0 €
Tarjeta ****1234 de 100 € -> comisión 2 €
Bizum al 600123456 de 30 € -> comisión 1 €
total de comisiones: 3 €
Observa:
- Cada variante lleva datos distintos (
ultimos4solo existe en la tarjeta,telefonosolo en Bizum). Por eso aquí no vale un enumerado. - Las variantes son clases de datos, que solo guardan información y comparan por valor (ver 4.B).
- Las funciones
describirycomisionno tienen caso por defecto: no hace falta, porque están cubiertos todos los posibles. - El comportamiento está fuera de las clases (en las funciones), no dentro de ellas. Se elige así cuando las variantes son solo datos y las operaciones se van añadiendo.
¿Enumerado, jerarquía cerrada, abstracta o interfaz?¶
| Situación | Usa |
|---|---|
| Valores fijos y sencillos, a lo sumo con datos iguales para todos (días, tamaños, estados) | Enumerado |
| Variantes cerradas, cada una con datos distintos (pagos, resultados, formas de un mensaje) | Jerarquía cerrada |
| Familia abierta con código común | Clase abstracta |
| Capacidad que clases distintas pueden ofrecer | Interfaz |
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
Poner un caso default/else que esconde los casos nuevos |
No lo pongas: así el compilador te avisa si falta uno |
| Usar una jerarquía cerrada cuando otras personas deben poder añadir variantes | Interfaz o clase abstracta (jerarquía abierta) |
Meter la lógica de cada caso en una cadena de if y instanceof |
Un switch/when/match con patrones |
| Usar un enumerado cuando cada variante necesita datos propios | Jerarquía cerrada |
Para practicar¶
El ejercicio de la biblioteca de U5.1 usa clases de datos y una jerarquía cerrada para los usuarios. La comparación entre lenguajes de todos estos tipos de clase está en Pasar de uno a otro.