Skip to main content

Modelado avanzado con enumeraciones

En Kotlin, una enum class no se limita a representar un conjunto finito de etiquetas simbólicas.
A diferencia de otros lenguajes donde los enums son simples constantes, en Kotlin pueden tener propiedades, métodos e incluso lógica especializada en cada instancia. Esto permite modelar comportamientos complejos de manera clara, expresiva y con un alto grado de encapsulamiento.

En esta lección aprenderás a usar enumeraciones como:

  • Tipos con estado interno mutable, útiles para representar componentes que evolucionan (como versiones).
  • Clases abstractas con comportamiento polimórfico, donde cada valor define su propia lógica.
  • Colecciones exhaustivas de estrategias, reglas o modos de operación, aprovechando su naturaleza cerrada.

También conocerás funciones y propiedades estándar que Kotlin ofrece para trabajar con enum class de forma segura y eficiente, como name, entries, ordinal y valueOf.

Al finalizar, estarás en condiciones de aplicar enumeraciones como herramientas de modelado avanzadas, en lugar de verlas solo como simples colecciones de valores fijos.

🔢 Enumeraciones con estado y comportamiento

Una enumeración en Kotlin no se limita a representar un conjunto fijo de valores.
También puede tener valores asociados, propiedades mutables y métodos, lo que permite modelar comportamientos más ricos y con estado.

sum/src/main/kotlin/cl/ravenhill/semver/VersionComponent.kt
enum class VersionComponent(val identifier: String, var current: Int) {
MAJOR("major", 0),
MINOR("minor", 0),
PATCH("patch", 0);

fun bump() {
current++
}

fun reset() {
current = 0
}
}

Este patrón puede ser útil para representar componentes que evolucionan en el tiempo y requieren comportamiento específico por cada valor enumerado.

🧬 Enums como clases con comportamiento especializado

En Kotlin, enum class puede actuar como una clase abstracta, permitiendo que cada uno de sus valores sobrescriba métodos o defina comportamiento propio.
Cada valor del enum es una instancia única de una subclase anónima1 implícita que extiende la clase base del enum.

sum/src/main/kotlin/cl/ravenhill/semver/ReleaseChannel.kt
enum class ReleaseChannel {
STABLE {
override val description: String =
"Ready for production use, fully tested and stable"
},
BETA {
override val description: String =
"Version with new features, may contain bugs but is more stable than alpha"
},
ALPHA {
override val description: String =
"Experimental version subject to significant changes"
};

abstract val description: String
}

Uso con interfaces

Como los enum class pueden declarar métodos abstractos y sobrescribirlos en cada instancia, también pueden implementar interfaces, al igual que las clases.
Aunque no profundizaremos aún en este tema, lo retomaremos más adelante cuando exploremos cómo funcionan las interfaces y la reutilización de comportamiento en Kotlin.
Este debería ser un concepto familiar si has trabajado con otros lenguajes orientados a objetos como Java, C# o Scala (donde se denominan traits).2

🧰 Utilidades disponibles en todos los enum class

En Kotlin, cada enum class viene acompañado de un conjunto de funciones y propiedades útiles que permiten trabajar con sus valores de forma segura y expresiva.
Esto resulta especialmente práctico al diseñar bibliotecas reutilizables o construir sistemas donde los estados, niveles o fases deben modelarse de forma clara.

Supongamos que trabajamos en una biblioteca de compilación que define las fases del ciclo de construcción de un proyecto de forma cíclica: Al terminar una fase, se inicia la siguiente, y al llegar al final, se vuelve a la primera.

sum/src/main/kotlin/cl/ravenhill/compiler/OptimizationPass.kt
enum class OptimizationPass {
INLINE_FUNCTIONS, REMOVE_DEAD_CODE, FOLD_CONSTANTS
}

Veamos algunas de las cosas que podemos hacer con este enum:

🏷️ name: String

Propiedad que devuelve el nombre exacto del valor tal como fue declarado en el enum:

sum/src/main/kotlin/cl/ravenhill/compiler/OptimizationPass.kt
val name: String = OptimizationPass.INLINE_FUNCTIONS.name
println("${OptimizationPass.INLINE_FUNCTIONS} name: $name")
// Output: "INLINE_FUNCTIONS name: INLINE_FUNCTIONS"

En este caso, interpolar el valor del enum directamente produce el mismo resultado que acceder a name.
Esto se debe a que, por defecto, el método toString() de un enum en Kotlin devuelve su name.

📋 entries: EnumEntries<EnumClass>

Desde Kotlin 1.9, todos los enum class exponen la propiedad entries, una lista especializada3 que contiene todos los valores del enum en el orden en que fueron declarados.

sum/src/main/kotlin/cl/ravenhill/compiler/OptimizationPass.kt
for (phase in OptimizationPass.entries) {
println("> ${phase.name}")
}

Tip

Usar entries es especialmente útil en bibliotecas y sistemas de construcción donde:

  • Necesitas recorrer todas las fases de un ciclo, como en un sistema de compilación modular (INIT, COMPILE, LINK, PACKAGE).
  • Quieres generar dinámicamente un menú, selector o documentación con los modos o configuraciones disponibles (Mode.entries, LogLevel.entries, etc.).
  • Implementas un validador o visualizador genérico que debe operar sobre todos los casos de un tipo cerrado, sin acoplarse a su número o nombres específicos.
  • Usas enums como plugins o estrategias registradas (por ejemplo, en una arquitectura de procesamiento por fases o canalizaciones), y quieres evitar mantener listas manuales.

De este modo, entries reemplaza listas redundantes, elimina errores por omisión y asegura que tu lógica siempre esté alineada con los valores declarados.

entries vs values()

Antes de Kotlin 1.9, el método values() era la forma estándar de obtener todas las constantes de una enumeración.
Este método, heredado de Java, devuelve un array mutable con los valores del enum.

A partir de Kotlin 1.9, se introdujo la propiedad entries como reemplazo preferido, y values() fue marcado como obsoleto (@Deprecated) para su futura eliminación.

Problemas con values()

  • Cada llamada a values() crea una nueva copia del array, lo que puede producir problemas de rendimiento, especialmente si se invoca en bucles o métodos frecuentemente utilizados.
  • Es difícil de detectar como cuello de botella, ya que el impacto depende del uso en tiempo de ejecución, no del tamaño del enum.
  • El resultado es un Array, pero la mayoría de las APIs modernas en Kotlin usan List, por lo que requiere una conversión explícita.
  • Se considera un "bug de diseño" heredado de Java, ampliamente documentado en propuestas como JDK-8073381 y mencionado como causa de fugas de memoria o problemas en bibliotecas como kotlinx.serialization o el conector MySQL JDBC.

Ventajas de entries

  • Es una colección inmutable especializada (EnumEntries), más segura y alineada con la API de Kotlin.
  • No requiere crear nuevas instancias innecesarias.
  • Permite usar operaciones de colección (map, filter, etc.) directamente sin conversión.
  • Mejora la legibilidad e intención del código.

🔢 ordinal: Int

Indica la posición del valor en la declaración, empezando desde 0:

sum/src/main/kotlin/cl/ravenhill/compiler/OptimizationPass.kt
enum class OptimizationPass {
INLINE_FUNCTIONS, REMOVE_DEAD_CODE, FOLD_CONSTANTS;

fun next(): OptimizationPass =
entries[(ordinal + 1) % entries.size]
}

🔎 valueOf(value: String): Enum

Permite obtener una instancia de la enumeración a partir de su nombre como cadena:

sum/src/main/kotlin/cl/ravenhill/build/BuildPhase.kt
val phase = OptimizationPass.valueOf("INLINE_FUNCTIONS")
println("Current build phase: ${phase.name}")
try {
OptimizationPass.valueOf("INVALID_PHASE")
} catch (e: IllegalArgumentException) {
println("Caught an exception: ${e.message}")
}

🧩 Ejercicio de cierre: Enums como estrategias de validación

Imagina que estás diseñando una biblioteca para validar nombres de usuario en distintas plataformas. Cada plataforma tiene reglas diferentes:

  • En Web: debe tener entre 4 y 12 caracteres, y solo letras o números.
  • En Móvil: debe comenzar con letra y puede contener guiones bajos.
  • En Consola: debe tener exactamente 8 caracteres y no contener vocales.

Con este fin:

  1. Declara un enum class llamado ValidationPlatform con los valores WEB, MOBILE y CONSOLE.
  2. Asigna a cada valor del enum una función validate(name: String): Boolean con su lógica correspondiente.
  3. Agrega una propiedad description que explique la validación que aplica cada plataforma.
  4. Escribe una función printValidationInfo(name: String) —fuera del enum— que imprima si el nombre es válido en cada plataforma, junto a su descripción.
Solución
sum/src/main/kotlin/cl/ravenhill/platform/ValidationPlatform.kt
enum class ValidationPlatform {
WEB {
override val description: String =
"Web platform with strict naming rules: 4-12 characters, alphanumeric only"

override fun validate(name: String): Boolean =
name.length in 4..12 && name.all { it.isLetterOrDigit() }
},
MOBILE {
override val description: String =
"Mobile platform with flexible naming: first character must be a letter, rest alphanumeric or underscore"

override fun validate(name: String): Boolean =
name.first().isLetter() && name.all { it.isLetterOrDigit() || it == '_' }
},
CONSOLE {
override val description: String =
"Console platform with unique naming: 8 characters, no vowels allowed"

override fun validate(name: String): Boolean {
val vowels = "aeiouAEIOU".toSet()
return name.length == 8 && name.none { it in vowels }
}
};

abstract val description: String

abstract fun validate(name: String): Boolean
}

fun printValidationInfo(name: String) {
for (platform in ValidationPlatform.entries) {
println(buildString {
appendLine("Platform: ${platform.name}")
appendLine("Description: ${platform.description}")
appendLine("Is '$name' valid? ${platform.validate(name)}")
})
}
}

🎯 Conclusiones

En esta lección exploramos cómo las enumeraciones en Kotlin no se limitan a representar conjuntos finitos de valores constantes, sino que pueden actuar como clases completas, con propiedades, estado interno y comportamiento específico por instancia.
Esto permite modelar estructuras más expresivas y reutilizables, encapsulando lógica sin depender de condicionales externos.

También revisamos herramientas estándar disponibles para trabajar con enum class, como name, entries, ordinal y valueOf, junto con sus beneficios y limitaciones. Finalmente, aplicamos estos conceptos para construir una estrategia de validación polimórfica basada en enumeraciones.

🔑 Puntos clave

  • Un enum class puede contener propiedades mutables, funciones y lógica especializada por instancia.
  • Las enumeraciones pueden actuar como clases abstractas, y cada valor puede sobrescribir miembros distintos.
  • Es posible implementar interfaces y usar enumeraciones como estrategias reutilizables o controladores de flujo.
  • La propiedad entries (desde Kotlin 1.9) reemplaza a values() como la forma preferida y segura de listar valores.
  • Aunque prácticas, funciones como ordinal o valueOf deben usarse con cuidado en contextos persistentes o externos.

🧰 ¿Qué nos llevamos?

Las enumeraciones avanzadas nos enseñan que no todo comportamiento debe modelarse con clases completas o jerarquías complejas.
Cuando los casos están cerrados y bien definidos, un enum class puede ser una forma compacta, segura y elegante de representar estrategias, reglas de validación, fases de un sistema, modos de operación o cualquier conjunto fijo de comportamientos diferenciados.

Este enfoque promueve un diseño más expresivo, menos propenso a errores y con menor acoplamiento, al centralizar el comportamiento en las propias instancias del enum.
Es una herramienta valiosa para quienes diseñan librerías reutilizables, DSLs o arquitecturas extensibles, y un excelente ejemplo de cómo Kotlin fusiona ideas orientadas a objetos con principios de programación funcional.

📖 ¿Con ganas de más?

🔥 Referencias recomendadas

  • 🌐 Working with Enums in Kotlin en Baeldung:
    Este artículo explora en profundidad las características avanzadas de las enumeraciones en Kotlin. Parte desde conceptos básicos como la definición de enums y su inicialización, hasta aspectos más complejos como clases anónimas por constante, implementación de interfaces, uso de entries en lugar de values, y los riesgos de depender del ordinal. Además, discute cómo extender el comportamiento de enums a través de interfaces o clases selladas, y presenta la distinción entre enums ordinales y no ordinales. Es una referencia práctica y detallada para modelar datos con enums en Kotlin de forma segura y expresiva.

🔹 Referencias adicionales

  • 🌐 Enum class declaration en Kotlin Language Specification:
    Explica la estructura y comportamiento de las enum classes en Kotlin. Describe sus propiedades principales (valores predefinidos, herencia de Enum, imposibilidad de heredar o parametrizar), así como sus miembros implícitos como name, ordinal, compareTo, entries, valueOf y values. También se destaca la diferencia entre entries y values, recomendando el uso de entries desde Kotlin 1.9. Incluye ejemplos de enums simples y con lógica específica en cada entrada.
  • 🌐 Decommission Enum.values() and replace it with Enum.entries en KEEP - Kotlin Evolution and Enhancement Process por Vsevolod Tolstopytov:
    Presenta la propuesta oficial para reemplazar Enum.values() por la propiedad Enum.entries en Kotlin, destacando sus ventajas en rendimiento, inmutabilidad y compatibilidad con APIs basadas en colecciones. Se introducen el tipo EnumEntries<E> y una estrategia de transición asistida por el IDE sin romper compatibilidad. La propuesta fue aceptada e implementada como parte de Kotlin 1.9, con un plan gradual de despriorización de values() y apoyo mediante funciones como enumEntries.

Footnotes

  1. Entraremos en detalle sobre las clases anónimas en Kotlin en una futura lección.

  2. La implementación de interfaces en Kotlin no es idéntica a los traits de Scala ni a las interfaces de Java o C#, aunque comparten la idea general de reutilizar comportamiento.

  3. EnumEntries es un subtipo optimizado de List, diseñado específicamente para trabajar con enumeraciones.
    Permite realizar ciertas operaciones —como verificar si un valor pertenece a la lista (contains)— en tiempo constante, a diferencia de una lista convencional, donde esa operación sería lineal.