Skip to main content

Clases selladas

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/

Las clases e interfaces selladas son clases abstractas que restringen la herencia a un conjunto específico de subclases, controlando las posibles extensiones. Esto las hace ideales para escenarios donde se quiere limitar las variantes posibles, como cuando se modelan estados finitos o se define una jerarquía cerrada de tipos.

🎯 Características Clave

  • Propósito: Controlar y restringir la jerarquía de tipos. Solo las subclases definidas en el mismo paquete y módulo pueden extenderlas.
  • Restricciones: Las subclases deben estar dentro del mismo paquete y módulo que la clase o interfaz sellada.
  • Beneficios:
    • Control de jerarquía: Garantiza que todas las posibles subclases estén controladas y conocidas en tiempo de compilación.
    • Patrones exhaustivos: Facilita el uso de when exhaustivos, mejorando la legibilidad y evitando errores de estado no manejados.
    • Mantenimiento: Simplifica el mantenimiento del código, ya que no se puede extender la clase fuera de su contexto definido.

💳 Ejemplo: Sistema de Pagos

sealed interface Payment
data class CreditCard(val number: String) : Payment
data class Cash(val amount: Int) : Payment
data object Unpaid : Payment

Payment define un conjunto cerrado de opciones: CreditCard, Cash, y Unpaid. Este enfoque garantiza que, al manejar pagos, siempre sabrás qué opciones pueden estar presentes.

🔁 Tercer Enfoque: Modelado de Estados con Clases Selladas

Son especialmente útiles para modelar estados finitos o flujos controlados en sistemas. Veamos un ejemplo donde se modelan los estados de un pedido en una tienda en línea:

sealed class DeliveryState {
abstract fun signal(): String
open fun isFinalState() = false
}

data object Pending : DeliveryState() {
override fun signal() = "Order is pending"
}

data class Cancelled(val reason: String) : DeliveryState() {
override fun signal() = "Order is cancelled because $reason"
override fun isFinalState() = true
}

🧩 Manejo con when Exhaustivo

El uso de un when exhaustivo asegura que todos los posibles estados sean manejados, lo que ayuda a evitar errores en tiempo de ejecución.

fun handleOrderState(state: DeliveryState) = when (state) {
is Pending -> println(state.signal())
is Paid -> println(state.signal())
is Shipped -> println(state.signal())
is Delivered -> println(state.signal())
is Cancelled -> println(state.signal())
}

🔬 Uso de Reflexión con Clases Selladas

Para obtener las subclases de una clase o interfaz sellada en Kotlin, puedes usar reflexión. Para ello, es necesario añadir la dependencia de reflexión en el archivo build.gradle.kts:

dependencies {
implementation(kotlin("reflect"))
}

🔍 Subclases directas

La función sealedSubclasses de Kotlin retorna solo las subclases directas de una clase sellada. Si hay subclases anidadas que también son selladas, no se listan recursivamente:

fun listOrderStates(): List<KClass<out DeliveryState>> =
DeliveryState::class.sealedSubclasses

🧬 Subclases directas e indirectas (recursivo)

Si quieres obtener todas las subclases posibles —incluyendo las indirectas—, puedes usar una función recursiva como la siguiente:

fun <T : Any> allSealedVariants(k: KClass<T>): List<KClass<out T>> =
k.sealedSubclasses.flatMap {
listOf(it) + if (it.isSealed) allSealedVariants(it as KClass<T>) else emptyList()
}
note

Esta técnica es útil si estás desarrollando herramientas de análisis estático o DSLs internos que necesitan conocer todas las variantes de un tipo cerrado.

⚖️ Beneficios y Limitaciones

Beneficios

  • Facilitan el uso de when exhaustivo sin else.
  • Evitan extensiones fuera del módulo/paquete previsto.
  • Buen reemplazo para jerarquías de tipos + enum class.

Limitaciones

  • Menos flexibles si la jerarquía debe ser abierta o extensible por otres.
  • Requieren reflexión si quieres inspeccionar dinámicamente sus subtipos.

🎧 Ejercicio: Sistema multimedia

Ejercicio

Desarrolla un sistema que maneje distintos tipos de contenido multimedia. Define una interfaz sellada Media que represente diferentes tipos de contenido, como Music y Video. La interfaz debe incluir dos métodos: play() y pause().

Instrucciones:

  1. Interfaz Media: Define una interfaz sellada Media con los métodos play() y pause().

  2. Clases Music y Video: Implementa dos clases: Music, que tendrá un método adicional skip(), y Video, con un método fastForward().

Ejemplo de Uso

fun main() {
val mediaList: List<Media> = listOf(
Music("Ace of Spades", "Motörhead"),
Music("MANIAC", "Stray Kids"),
Video("Le Locataire", duration = 125)
)

mediaList.forEach { media ->
media.play()
media.pause()

when (media) {
is Music -> media.skip()
is Video -> media.fastForward()
}
}

println("Media types: ${listMediaTypes()}")
}
Solución
sealed interface Media {
fun play()
fun pause()
}
data class Music(val title: String, val artist: String) : Media {
override fun play() = println("Playing $title by $artist")

override fun pause() = println("Pausing $title by $artist")

fun skip() = println("Skipping $title by $artist")
}
data class Video(val title: String, val duration: Int) : Media {
override fun play() = println("Playing $title")

override fun pause() = println("Pausing $title")

fun fastForward() = println("Fast forwarding $title")
}

🎯 Conclusiones

Las clases e interfaces selladas en Kotlin permiten modelar jerarquías cerradas y controladas de tipos. Este enfoque potencia la expresividad del lenguaje cuando trabajamos con dominios limitados y bien definidos, como flujos de estados, eventos, resultados, o entidades de un sistema. Al restringir la extensión fuera de su módulo y paquete, facilitan el uso de when exhaustivo, reducen errores y hacen el código más claro y mantenible.

🔑 Puntos clave

  • Las clases e interfaces selladas restringen su jerarquía a subclases definidas dentro del mismo módulo y paquete.
  • Permiten usar when sin cláusula else, obligando a manejar todos los casos explícitamente.
  • Son ideales para representar alternativas finitas y conocidas, como estados o comandos.
  • La función sealedSubclasses permite inspeccionar la jerarquía en tiempo de ejecución, pero solo devuelve subclases directas.
  • Es posible obtener todas las subclases (directas e indirectas) usando reflexión recursiva.

🧰 ¿Qué nos llevamos?

Al trabajar con clases e interfaces selladas, no solo obtenemos una herramienta técnica para restringir jerarquías de tipos: también incorporamos una forma de pensar que prioriza la claridad, la exhaustividad y el control sobre nuestro modelo de dominio. Nos obliga a ser explícitas sobre las variantes posibles, a prever todos los escenarios, y a construir estructuras más robustas y sostenibles.

En lugar de abrir la puerta a extensiones arbitrarias, las clases selladas nos invitan a delimitar nuestro universo de casos válidos. Esto no solo reduce errores, sino que hace que nuestro código cuente una historia más coherente, donde cada alternativa está prevista y cada decisión, justificada.

Al final, trabajar con clases selladas es una declaración de intención: queremos sistemas más predecibles, más legibles, y más fáciles de evolucionar con el tiempo. Y en ese camino, Kotlin nos da las herramientas —y el lenguaje— para lograrlo.

📖 Referencias

🔥 Recomendadas

🌐 Sealed classes and interfaces | Kotlin. (s. f.). Kotlin Help. Recuperado 22 de marzo de 2025, de https://kotlinlang.org/docs/sealed-classes.html