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
}
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...
- Windows
- Windows (corto)
- Linux/Mac
$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"
$SumTestDir = "adt\src\test\kotlin\$Group\sum"
$EnumTestDir = "$SumTestDir\enums"
md $EnumTestDir
"package $Group.sum.enums" -replace '\\', '.' > `
"$EnumTestDir\DeliveryStateTest.kt"
SUM_TEST_DIR=adt/src/test/kotlin/$GROUP/sum
ENUM_TEST_DIR=$SUM_TEST_DIR/enums
mkdir -p $ENUM_TEST_DIR
echo "package ${GROUP//\//.}.sum.enums" > \
"$ENUM_TEST_DIR/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:
- Código esencial
- Código completo
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"
}
package com.github.username.sum.enums
import io.kotest.core.spec.style.FreeSpec
import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
class DeliveryStateTest : FreeSpec({
"A delivery state" - {
"when handled" - {
"should return a string representation" - {
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
}
}
}
}
})
package com.github.username.sum.enums
enum class DeliveryState {
PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}
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"
}
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.
- Código esencial
- Código completo
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
}
package com.github.username.sum.enums
import io.kotest.core.spec.style.FreeSpec
import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
class DeliveryStateTest : FreeSpec({
"A delivery state" - {
"when handled" - {
"should return a string representation" - {
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
}
}
}
}
})
package com.github.username.sum.enums
enum class DeliveryState {
PENDING {
override fun signal() = "Order is pending"
}, PAID {
override fun signal() = "Order is paid"
}, SHIPPED {
override fun signal() = "Order is shipped"
}, DELIVERED {
override fun signal() = "Order is delivered"
}, CANCELLED {
override fun signal() = "Order is cancelled"
};
abstract fun signal(): String
val isFinal get() = this == DELIVERED || this == CANCELLED
}
fun handleState(state: DeliveryState) = if (state.isFinal) {
"Final state: ${state.signal()}"
} else {
"Non-final state: ${state.signal()}"
}
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.
- Código esencial
- Código completo
enum class DeliveryState(private val description: String, val code: Int) {
PENDING("Order is pending", 0),
CANCELLED("Order is cancelled", 4);
fun signal(): String = description
}
package com.github.username.sum.enums
enum class DeliveryState(private val description: String, val code: Int) {
PENDING("Order is pending", 0),
PAID("Order is paid", 1),
SHIPPED("Order is shipped", 2),
DELIVERED("Order is delivered", 3),
CANCELLED("Order is cancelled", 4);
fun signal(): String = description
val isFinal: Boolean
get() = this == DELIVERED || this == CANCELLED
}
fun handleState(state: DeliveryState) = if (state.isFinal) {
"Final state: ${state.signal()}"
} else {
"Non-final state: ${state.signal()}"
}
- 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.
- Código esencial
- Código completo
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
}
package com.github.username.sum.enums
import io.kotest.core.spec.style.FreeSpec
import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
class DeliveryStateTest : FreeSpec({
"A delivery state" - {
"when handled" - {
"should return a string representation" - {
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
}
}
}
"when accessing its code" - {
"should return the index of the state" - {
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.
- Código esencial
- Código completo
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 {
// ...
}
interface Subscriber {
fun subscribe()
}
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 {
PENDING("Order is pending", 0),
PAID("Order is paid", 1),
SHIPPED("Order is shipped", 2),
DELIVERED("Order is delivered", 3),
CANCELLED("Order is cancelled", 4);
fun signal(): String = description
fun isFinalState() = this == DELIVERED || this == CANCELLED
}
fun main() {
val state = DeliveryState.PAID
println(state.signal())
state.notify(object : Subscriber {
override fun subscribe() = println("Subscribing to notifications")
}, "Order is paid")
state.store()
println(state.isFinalState())
/*
Output:
Order is paid
Notifying null: Order is paid
Storing data
false
*/
}
- [1-4]:
Notifier
define un métodonotify
que toma un suscriptor y un mensaje, y proporciona una implementación predeterminada para notificar al suscriptor. - [6-8]:
Storable
define un métodostore
con una implementación predeterminada que simula el almacenamiento de datos. - [10-12]:
DeliveryState
es una enumeración que implementa tantoNotifier
comoStorable
, por lo que hereda sus comportamientos.
Funciones Útiles en Enumeraciones
entries
Puedes acceder a todas las entradas de la enumeración con entries
:
- Código esencial
- Código completo
fun listOrderStates() = DeliveryState.entries.forEach { println(it) }
fun listOrderStates() = DeliveryState.entries.forEach { println(it) }
fun main() {
listOrderStates()
/* Output:
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
*/
}
valueOf
Puedes buscar un valor de la enumeración por su nombre con valueOf
, aunque debes manejar posibles excepciones si el valor no existe:
- Código esencial
- Código completo
fun getOrderState(name: String) = DeliveryState.valueOf(name)
fun getOrderState(name: String) = DeliveryState.valueOf(name)
fun main() {
try {
val state = getOrderState("PAID")
println("State: ${state.description}, Code: ${state.code}")
} catch (e: IllegalArgumentException) {
println("Invalid state")
}
/* Output: State: Order is paid, Code: 1 */
}
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 bloqueselse
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:
renew()
: Intenta renovar la suscripción.nextState()
: Cambia el estado a otro basado en una acción del sistema.
Requerimientos:
ACTIVE
: Permite renovar y pasa aEXPIRED
ennextState()
.SUSPENDED
: No permite renovar, pero puede pasar aACTIVE
.CANCELLED
: No permite renovar ni cambiar de estado.EXPIRED
: Permite renovar y vuelve aACTIVE
.
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
- 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.
- Uso de
when
exhaustivo: En Kotlin, al trabajar con enumeraciones en una sentenciawhen
, el compilador garantiza que todos los casos posibles sean manejados sin necesidad de un bloqueelse
, lo que mejora la legibilidad y mantenibilidad del código. - 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.
- 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.
- 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.
- 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 comoIllegalArgumentException
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.