Skip to main content

Enumeraciones

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


r8vnhill/functional-programming-kt

En el contexto del diseño de bibliotecas de software, una de las herramientas más importantes para representar estados, comandos o decisiones es el uso de tipos suma. Ya hemos estudiado qué son estos tipos y cómo nos permiten modelar alternativas mutuamente excluyentes de forma segura.

En esta lección, exploramos una de sus expresiones más directas y prácticas: las enumeraciones. Si bien los tipos suma pueden representarse con clases selladas para capturar estructuras más complejas, las enumeraciones son ideales cuando los casos posibles están completamente definidos de antemano y comparten una representación compacta.

Desde el punto de vista de una biblioteca, utilizar enumeraciones no solo mejora la claridad del código, sino que también permite expresar restricciones con precisión: dejamos explícito que solo existen ciertos valores válidos, lo cual es útil tanto para el uso interno como para quien consume la API.

Veremos cómo las enumeraciones en Kotlin permiten definir comportamientos personalizados para cada caso, cómo pueden extenderse con métodos o constructores, y cómo se integran naturalmente con expresiones when exhaustivas para reforzar la seguridad y la mantenibilidad del sistema.

¿Qué son las enumeraciones?

Las enumeraciones en lenguajes de programación son una forma concreta y directa de representar tipos suma, es decir, valores que pueden ser uno entre varios casos posibles y conocidos.

En el contexto del diseño de bibliotecas de software, las enumeraciones resultan especialmente útiles para exponer conjuntos limitados de estados o variantes que lxs usuarixs deben manejar de forma segura. En lugar de depender de cadenas o números mágicos, una enumeración garantiza que solo se utilicen valores válidos y bien definidos, lo que reduce errores, mejora la legibilidad y facilita el análisis estático del código.

Imaginemos que estamos desarrollando una biblioteca para gestionar pedidos en una aplicación de e-commerce. Una parte de la API puede requerir que lxs usuarixs controlen los diferentes estados por los que pasa un pedido. En lugar de representar estos estados con strings como "paid" o "shipped", podemos definir una enumeración que explicite todos los estados posibles:

package com.github.username.sum.enums

enum class DeliveryState {
PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}
¿Qué acabamos de hacer?

DeliveryState modela los estados válidos por los que puede transitar un pedido. Esta enumeración puede ser parte de la API pública de una biblioteca, y al usarla, se asegura que quienes la consumen trabajen con un conjunto finito y validado de opciones. Esto mejora la autocompletación en entornos de desarrollo, facilita la documentación, y permite que el compilador detecte fácilmente usos incorrectos o incompletos.

Uso en un when exhaustivo

Si quieres crear los archivos desde la terminal...

$SumTestDir = "adt\src\test\kotlin\$Group\sum"
$EnumTestDir = "$SumTestDir\enums"
New-Item -Path $EnumTestDir -ItemType Directory -Force
"package $Group.sum.enums" -replace '\\', '.' |
Out-File -FilePath "$EnumTestDir\DeliveryStateTest.kt"

En Kotlin, un when es exhaustivo cuando cubre todos los posibles valores de una enumeración. Esto significa que no necesitas un bloque else si has manejado todos los estados posibles:

withData(
DeliveryState.PENDING to "Order is pending",
DeliveryState.PAID to "Order is paid",
DeliveryState.SHIPPED to "Order is shipped",
DeliveryState.DELIVERED to "Order is delivered",
DeliveryState.CANCELLED to "Order is cancelled"
) { (state, expected) ->
handleState(state) shouldBe expected
}
fun handleState(state: DeliveryState) = when (state) {
DeliveryState.PENDING -> "Order is pending"
DeliveryState.PAID -> "Order is paid"
DeliveryState.SHIPPED -> "Order is shipped"
DeliveryState.DELIVERED -> "Order is delivered"
DeliveryState.CANCELLED -> "Order is cancelled"
}
¿Qué acabamos de hacer?

Aquí, handleState es una función que toma un estado de entrega (DeliveryState) y devuelve una representación en cadena del estado. Al utilizar un when exhaustivo, se garantiza que todos los posibles estados de entrega sean manejados, evitando la necesidad de un bloque else adicional. Si se agrega un nuevo estado a la enumeración, el compilador de Kotlin emitirá un error si no se maneja en el when, lo que ayuda a mantener el código completo y actualizado.

Métodos en Enumeraciones

Las enumeraciones en Kotlin pueden contener tanto métodos abstractos como concretos, lo que permite que cada valor de la enumeración tenga su propia implementación personalizada, al mismo tiempo que comparten comportamientos comunes.

withData(
DeliveryState.PENDING to "Non-final state: Order is pending",
DeliveryState.PAID to "Non-final state: Order is paid",
DeliveryState.SHIPPED to "Non-final state: Order is shipped",
DeliveryState.DELIVERED to "Final state: Order is delivered",
DeliveryState.CANCELLED to "Final state: Order is cancelled"
) { (state, expected) ->
handleState(state) shouldBe expected
}
enum class DeliveryState {
PENDING {
override fun signal() = "Order is pending"
}, CANCELLED {
override fun signal() = "Order is cancelled"
};

abstract fun signal(): String

val isFinal get() = this == DELIVERED || this == CANCELLED
}
¿Qué acabamos de hacer?

Aquí, DeliveryState es una enumeración que representa los estados de entrega de un pedido. Cada valor de la enumeración tiene su propio método signal que devuelve un mensaje específico para ese estado. Además, la enumeración tiene un método concreto isFinal que verifica si el estado es final (DELIVERED o CANCELLED). Al utilizar estos métodos, se puede personalizar el comportamiento de cada estado de entrega, al mismo tiempo que se comparten comportamientos comunes.

Este enfoque permite tener tanto comportamientos específicos como comunes en una enumeración.

Constructores en Enumeraciones

En Kotlin, las enumeraciones pueden tener constructores. Esto significa que, además de los valores predefinidos, puedes asociar datos adicionales a cada valor de la enumeración. Los constructores permiten inicializar las enumeraciones con parámetros que pueden representar cualquier dato relevante para cada valor.

Definición de Enumeraciones con Constructores

Para crear una enumeración con un constructor en Kotlin, defines los parámetros del constructor en la declaración de la enumeración y luego pasas los valores correspondientes a cada miembro de la enumeración.

enum class DeliveryState(private val description: String, val code: Int) {
PENDING("Order is pending", 0),
CANCELLED("Order is cancelled", 4);

fun signal(): String = description
}
¿Qué acabamos de hacer?
  • Descripción: Aquí, DeliveryState es una enumeración que representa los estados de entrega de un pedido. Cada valor de la enumeración tiene un constructor que toma una descripción y un código numérico. Estos valores se utilizan para inicializar cada estado de entrega con información adicional.
  • Código: Se asocia un código numérico (code) a cada estado de entrega.

Uso de los Valores del Constructor

Puedes acceder a los valores del constructor de la enumeración como lo harías con cualquier propiedad de una clase. Esto hace que las enumeraciones sean más flexibles y útiles cuando necesitas asociar información adicional a cada valor.

withData(
DeliveryState.PENDING to 0,
DeliveryState.PAID to 1,
DeliveryState.SHIPPED to 2,
DeliveryState.DELIVERED to 3,
DeliveryState.CANCELLED to 4
) { (state, expected) ->
state.code shouldBe expected
}

Implementación de Interfaces

Las enumeraciones en Kotlin pueden implementar interfaces, lo que les permite heredar comportamientos y métodos predeterminados definidos en estas interfaces. Esto es útil cuando se desea que los valores de una enumeración compartan comportamientos comunes sin necesidad de duplicar código en cada valor de la enumeración. A continuación, se presenta un ejemplo de cómo hacer que una enumeración implemente múltiples interfaces, con métodos que no necesitan ser sobrescritos.

interface Notifier {
fun notify(subscriber: Subscriber, message: String) =
println("Notifying ${subscriber::class.simpleName}: $message")
}

interface Storable {
fun store() = println("Storing data")
}

enum class DeliveryState(private val description: String, val code: Int) : Notifier, Storable {
// ...
}
Explicación del Código
  • [1-4]: Notifier define un método notify que toma un suscriptor y un mensaje, y proporciona una implementación predeterminada para notificar al suscriptor.
  • [6-8]: Storable define un método store con una implementación predeterminada que simula el almacenamiento de datos.
  • [10-12]: DeliveryState es una enumeración que implementa tanto Notifier como Storable, por lo que hereda sus comportamientos.

Funciones Útiles en Enumeraciones

entries

Puedes acceder a todas las entradas de la enumeración con entries:

fun listOrderStates() = DeliveryState.entries.forEach { println(it) }

valueOf

Puedes buscar un valor de la enumeración por su nombre con valueOf, aunque debes manejar posibles excepciones si el valor no existe:

fun getOrderState(name: String) = DeliveryState.valueOf(name)

Beneficios

  • Seguridad de Tipos: Las enumeraciones garantizan que solo se utilicen valores predefinidos, reduciendo errores por entradas no válidas o mal escritas.
  • Exhaustividad: En Kotlin, el uso de enumeraciones con when asegura que todos los casos estén cubiertos, evitando el uso de bloques else innecesarios.
  • Legibilidad y Mantenibilidad: Las enumeraciones mejoran la claridad del código, ya que representan estados o condiciones predefinidos de manera explícita y centralizada.
  • Comportamiento Personalizado: Puedes agregar métodos abstractos o concretos a los valores de la enumeración, lo que permite personalizar el comportamiento de cada uno.

Limitaciones

  • Limitaciones para Estados Complejos: Las enumeraciones son menos flexibles cuando se necesita manejar estados con múltiples variaciones o comportamientos más complejos. Para estos casos, las clases selladas pueden ser una mejor opción.
  • Datos Estáticos: Aunque puedes asociar datos adicionales con enumeraciones mediante constructores, los valores siguen siendo estáticos, lo que puede no ser adecuado para todos los escenarios.
Ejercicio: Cambios de Estado de Suscripción

Implementa una enumeración SubscriptionStatus que represente los siguientes estados de una suscripción: ACTIVE, SUSPENDED, CANCELLED, EXPIRED.

Cada estado tiene dos comportamientos:

  1. renew(): Intenta renovar la suscripción.
  2. nextState(): Cambia el estado a otro basado en una acción del sistema.

Requerimientos:

  • ACTIVE: Permite renovar y pasa a EXPIRED en nextState().
  • SUSPENDED: No permite renovar, pero puede pasar a ACTIVE.
  • CANCELLED: No permite renovar ni cambiar de estado.
  • EXPIRED: Permite renovar y vuelve a ACTIVE.
Solución
enum class SubscriptionStatus {
ACTIVE {
override fun renew() = println("Renewed!")
override fun nextState() = EXPIRED
},
SUSPENDED {
override fun renew() = println("Cannot renew while suspended")
override fun nextState() = ACTIVE
},
CANCELLED {
override fun renew() = println("Cannot renew, it's cancelled")
override fun nextState() = CANCELLED
},
EXPIRED {
override fun renew() = println("Renewed!")
override fun nextState() = ACTIVE
};

abstract fun renew()
abstract fun nextState(): SubscriptionStatus
}

¿Qué Aprendimos?

En esta lección hemos explorado las enumeraciones en Kotlin y cómo pueden utilizarse para modelar tipos suma concretos, representar estados específicos y proporcionar comportamientos comunes entre sus valores.

Principales conceptos

  1. Enumeraciones como tipos suma: Las enumeraciones permiten definir un conjunto fijo de valores, asegurando que solo estos valores sean utilizados, lo que proporciona seguridad de tipos y evita errores por entradas inválidas.
  2. Uso de when exhaustivo: En Kotlin, al trabajar con enumeraciones en una sentencia when, el compilador garantiza que todos los casos posibles sean manejados sin necesidad de un bloque else, lo que mejora la legibilidad y mantenibilidad del código.
  3. Métodos en enumeraciones: Las enumeraciones pueden contener métodos tanto abstractos como concretos, lo que permite comportamientos personalizados para cada valor de la enumeración. Además, los métodos compartidos ayudan a evitar la repetición de código.
  4. Constructores en enumeraciones: Las enumeraciones en Kotlin pueden tener constructores que permiten asociar datos adicionales a cada valor, como descripciones o códigos. Esto las hace más flexibles al permitir el almacenamiento de datos junto a los estados.
  5. Implementación de interfaces: Al implementar interfaces, las enumeraciones pueden heredar comportamientos comunes que se aplican a todos sus valores sin sobrescribir métodos. Esto permite una mayor reutilización de código.
  6. Funciones útiles en enumeraciones:
    • entries: Permite acceder a todos los valores de una enumeración, lo que facilita su recorrido o manipulación.
    • valueOf: Permite obtener un valor de la enumeración a partir de su nombre, aunque no es una operación completamente segura, ya que es necesario manejar excepciones como IllegalArgumentException si el valor no existe.

Ventajas y limitaciones

  • Las enumeraciones son ideales cuando se necesita trabajar con estados finitos y conocidos. Proporcionan seguridad de tipos, aseguran que todos los estados sean manejados y facilitan la implementación de comportamientos comunes.
  • Sin embargo, las clases selladas pueden ser más adecuadas para escenarios más complejos donde los estados tienen múltiples variaciones o comportamientos dinámicos.

Esta lección proporciona una base sólida para entender y aplicar enumeraciones en el desarrollo de software, permitiendo modelar estados de manera más eficiente y segura en Kotlin.