Creando enumerados con datos asociados en Kotlin

Últimamente, he estado desarrollando en Swift por motivos de trabajo y, para ser sincero, me encantan los enumerados con datos asociados de Swift. Si no sabes lo que es, no te preocupes, veremos ejemplos pero te puedo asegurar que luego no podrás vivir sin ellos.

Normalmente, un enum (enumerado) puede contener información, pero el tipo tiene que ser el mismo para todos los casos. Ejemplo:

public enum DayOfWeek {  
   MONDAY(1),
   TUESDAY(2),
   WEDNESDAY(3),
   THURSDAY(4),
   FRIDAY(5),
   SATURDAY(6),
   SUNDAY(7);

   private int dayNumber;
   private DayOfWeek(int dayNumber) {
      this.dayNumber = dayNumber;
   }
   public int getDayNumber() {
      return dayNumber;
   }
}

Mismo código en Kotlin (mucho más bonito):

enum class DayOfWeek(val dayNumber: Int) {  
  MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4),
  FRIDAY(5), SATURDAY(6), SUNDAY(7)
}

Los enumerados de Java no soportan diferentes tipos de valores por cada enum por lo que no podemos tener algo como esto:

   MONDAY(1),
   TUESDAY(2),
   WEDNESDAY(3),
   THURSDAY(4),
   FRIDAY(5),
   SATURDAY("WEEKEND"),
   SUNDAY("WEEKEND");

Lo sé, es un ejemplo un poco extraño, pero creo que es suficiente para entender cuál es la limitación en los enumerados de Java, ¿verdad?

Ahora, imagina que estás al cargo del desarrollo de una nueva aplicación de mensajería instantánea y estás trabajando en el diseño del modelo de todos los tipos que una conversación puede tener (aka Eventos):

  • Mensajes de texto
  • Imágenes
  • Mensajes de voz

Obviamente, existen varias propiedades en común para todos los tipos:

  • Fecha del evento
  • Sí es entrante o saliente
  • Estado de entrega, etc.

Quizá, tu primera aproximación sería crear una clase base llamada Event para empezar a crear subclases por cada tipo de evento. No pasa nada, yo probablemente hubiese hecho lo mismo pero, ¿y si te digo que hay una mejor forma de modelarlo usando enumerados con diferentes tipos de datos asociados? Suena bien, ¿verdad?

La idea es usar el poder de los datos asociados de los enumerados. Realmente, esto no se puede hacer en Kotlin directamente porque al ser un lenguaje que hereda de Java posee las mismas limitaciones en los enumerados pero los chicos de JetBrains ya pensaron en ello, añadieron una característica llamada Sealed classes en Kotlin que emula todo lo que hemos hablado directamente, de hecho los consideran como el reemplazo de los enumerados estándar de Java.

En vez de explicar que es una Sealed classes mostraré un ejemplo, para ser más exactos un ejemplo de la propiedad DeliveryStatus. La idea es almacenar el estado de un evento por lo que podemos tener tres posibilidades:

  • Entregado
  • Entregando.
  • NoEntregado. Éste es un caso especial porque queremos almacenar el mensaje real de error para poder mostrárselo al usuario en la aplicación.

Usando una sealed class llamada DeliveryStatys lo modelaríamos de la siguiente forma:

sealed class DeliveryStatus {  
    class Delivered : DeliveryStatus()
    class Delivering : DeliveryStatus()
    class NotDelivered(val error: String) : DeliveryStatus()
}

Esta clase sellada actuan como un enum realmente porque no puede ser extendida y cada "clase interna" actua como un caso de enumerado, de esta forma puedes definir los datos asociados por cada caso individualmente. Puedes leer más (no mucho más realmente) sobre ellos en Kotlin Docs.

Seguimos, dirección del evento:

  • Entrante. Debe incluir el remitente envío el evento,
  • Saliente. Debe incluir el estado de entrega que hablamos anteriormente.
sealed class Direction {  
    class Incoming(val from: String) : Direction()
    class Outgoing(val status: DeliveryStatus) : Direction()
}

Ahora, modelaremos el tipo de evento, recuerda que tenemos tres tipos de eventos: Menajes de Texto, Imágenes y Notas de Voz.

sealed class ContentType {  
    class Text(val body: String) : ContentType()
    class Image(val url: String, val caption: String) : ContentType()
    class Audio(val url: String, val duration: Int) : ContentType()
}

Fíjate como cada tipo de evento tiene su propio tipo de dato asociado: el cuerpo para los mensajes, URL y título para las imágenes y URL y duración para las notas de voz.

Finalmente, sólo nos queda crear la clase que define el contenido de un evento completo:

data class Event (val contentType: ContentType, val direction: Direction)  

Para ser un poco más claro aquí tienes un ejemplo de como usar este modelo:

// An event list example but this could be retrieved from a Conversation data store in a real app
val events = listOf(  
    Event(ContentType.Text("Hey, I'm Tony"), Direction.Incoming("Tony Stark")),
    Event(ContentType.Image("URL_TO_IMAGE", "Avengers"),  Direction.Incoming("Bruce Banner")),
    Event(ContentType.Audio("URL_TO_AUDIO", 15), Direction.Outgoing(DeliveryStatus.Delivered()))
)

for(event in events) {  
    renderEvent(event)
}

fun renderEvent(event: Event): Unit {  
    when(event.contentType) {
        is ContentType.Text -> println("${event.contentType.body}")
        is ContentType.Audio -> println("Audio of ${event.contentType.duration} secs.")
        is ContentType.Image -> println("Image (${event.contentType.caption})")
    }
}

Fíjate cómo event.contentType es casteado dentro de la cláusucla is, ésto nos permite acceder sus campos específicos en tiempo de compilación sin necesitas de hacer el casteo a mano (Kotlin ❤️ ).

Eso es todo, hemos modelado nuestros eventos usando clases selladas, pero además, esto nos ofrece aún más beneficios:

  • El compilador conoce cuántos tipos de la clase sellada hay por lo que la sentencia when es capaz de detectar si falta algún caso en tiempo de compilación.
  • Todos los eventos son inmutables.
  • Todo es mucho más claro y conciso.

Si estás interesado en jugar con el código fuente de ejemplo puedes hacerlo en Try Kotlin.

Para terminar, simplemente añadir que este tipo de modelado se está usando en la versión de iOS de Movistar TU que está escrita en Swift. Estamos muy contentos con el resultado ya que hemos conseguido que todo sea más legible y conciso y le hecho de poder hacer lo mismo en Android con Kotlin nos abre un montón de puertas nuevas.