Una clase de datos es un concepto no vinculado a ningún lenguaje de programación específico, es un patrón lo suficientemente conveniente para la mayoría de los programadores como una forma sencilla de representar, encapsular y mover información.

Una clase de datos se refiere a una clase que contiene solo campos y métodos crud para acceder a ellos (getters y setters). Estos son simplemente contenedores para datos utilizados por otras clases. Estas clases no contienen ninguna funcionalidad adicional y no pueden operar de forma independiente en los datos que poseen.

Generalmente, las clases de datos representan entidades del mundo real, y es común tener docenas o cientos de estas clases en un proyecto, lo que significa que crear, modificar y manipular estos objetos es una tarea muy común para un desarrollador.

Una clase de datos en Java

Así es como suele verse una clase de datos en Java:

Notará que estamos sobreescribiendo los métodos toString() , equals() y hashCode() (declarados en la clase Object.java). ¿Por qué estos métodos son relevantes en las clases de datos?

Al implementar esos tres métodos, creamos objetos de valor, clases para las que dos instancias cualesquiera con valores de campo adecuadamente iguales se consideran intercambiables. (Nota: Para ser completamente cierto, necesitaremos hacer que la clase sea inmutable. Hacer que los campos sean finales y quitar los fijadores ayuda. La inmutabilidad a menudo se considera una buena práctica y se recomienda cuando sea posible)

  • equals() : Por defecto, devuelve true solo si las dos variables se refieren al mismo objeto, es decir, si la posición en memoria es la misma. Reemplazamos este método para devolver true si los objetos contienen la misma información, en otras palabras, si los objetos representan la misma entidad. Comprueba que las propiedades son las mismas y contienen el mismo valor.
  • hashCode() : De forma predeterminada, devuelve la dirección de memoria del objeto en hexadecimal. Es un valor numérico para identificar un objeto durante la prueba de igualdad, es decir, objetos iguales tienen el mismo código hash. Este método debe sobrescribirse cuando toString() se sobrescribe para que devuelva un código hash calculado a partir de los valores de las propiedades.
  • toString(): De forma predeterminada, devuelve el tipo de objeto y hashCode(), por ejemplo [email protected]+ . Anulamos este método para tener una versión más legible para el ser humano del objeto como User(name=Steve, surname=Jobs) .

A pesar de ser un concepto tan importante, no hay nada en el código Java anterior que haga que esta clase sea diferente de cualquier otra. Los programadores pueden reconocerlo como una clase de datos debido a la estructura y los patrones de clase, pero desde el punto de vista del compilador, esta clase es solo otra clase.

Crear clases de datos es tan común que los desarrolladores a menudo usan el IDE y otros complementos para ayudarlos con esta tarea repetitiva. El dolor de crear una clase de datos en Java se puede aliviar con complementos o el IDE, pero la mayoría de los errores se introducen en modificaciones adicionales de esas clases. Es muy fácil olvidarse de modificar todos los métodos complementarios en consecuencia cada vez que se elimina o agrega un campo.

Una clase de datos en Java carece de soporte de lenguaje. Es una tarea repetitiva y propensa a errores que representa demasiada fricción para un lenguaje de programación moderno.

Una clase de datos en Kotlin

La misma clase de datos en Kotlin se vería algo como esto:

data class User(var name: String, var age: Int)

Kotlin eleva las clases de datos a ciudadanos de primera clase introduciendo la palabra clave data. Vamos a desglosarlo.

  • Getters y setters

Los getters y setters se crean automáticamente en Kotlin cuando declaramos propiedades. En resumen, lo que var name: String significa es que la clase User tiene una propiedad que es pública (visibilidad predeterminada en Kotlin), mutable (var) y es de tipo String. Dado que es público, crea el getter, y dado que es mutable, crea el setter.

Si queremos que la clase sea de solo lectura (sin configuradores), necesitamos usar val :

data class User(val name: String, val age: Int)

Podemos mezclar val y var en la misma declaración de clase. Puede pensar en val como una variable final en Java.

Todo lo explicado hasta ahora, es común a cualquier declaración de clase en Kotlin, pero la palabra clave data es lo que está marcando la diferencia aquí.

  • La palabra clave data

Declarar una clase como clase de datos nos va a implementar toString(), hashCode() y equals() automáticamente de la misma manera que describimos anteriormente para la clase Java. Así que si creamos una clase de usuario como User("Steve Jobs",56) y llamamos al método toString() obtendremos algo como:User(name=Steve Jobs, age=56) .

Declaraciones de desestructuración

La palabra clave data proporciona funciones que permiten declaraciones de desestructuración. En resumen, crea una función para cada propiedad para que podamos hacer cosas como esta:

Función de copia

La palabra clave data nos da una forma práctica de copiar clases cambiando el valor de algunas propiedades. Si queremos crear una copia de un usuario cambiando la edad, esta es la forma en que lo haríamos:

Las propiedades declaradas en el cuerpo de la clase se ignoran

El compilador solo utiliza las propiedades definidas dentro del constructor primario para las funciones generadas automáticamente.

La propiedad address no va a ser tratada por la palabra clave data, por lo que significa que las implementaciones generadas automáticamente la ignorarán.

Par y Triple

Pair y Triple son clases de datos estándar en la biblioteca, pero los documentos de Kotin en sí desalientan su uso en favor de clases de datos más legibles y personalizadas.

Requisitos y limitaciones

  • Un constructor de clase de datos debe tener al menos un parámetro.
  • Todos los parámetros deben comercializarse como val o var.
  • Una clase de datos no puede ser abstract, open, sealed o inner .
  • equals , toString y los métodos hashCode se pueden sobrescribir explícitamente.
  • No se permiten implementaciones explícitas para las funciones componentN() y copy().
  • Derivar una clase de datos de un tipo con una firma coincidente de función copy() fue obsoleta en Kotlin 1.2 y prohibida en Kotlin 1.3.
  • Una clase data no puede extenderse desde otra clase data.
  • Una clase data puede extender otras clases (desde Kotlin 1.1)

Las clases de datos son ciudadanos de primera clase en Kotlin. En una sintaxis muy corta, ofrecen una solución sin fricción con todas las ventajas y sin compromisos.

Qué significa para Android

Voy a tratar de explicar cuáles son las principales ventajas que he encontrado usando clases de datos Kotlin en mis proyectos de Android. Estos no son todos y podrían no ser los más importantes, pero estos son los más obvios de mi experiencia hasta ahora.

  • modelos de Datos

Arquitectura en Android era (y todavía es) un tema candente. Hay múltiples opciones, pero la mayoría de ellas tienen en común la separación de preocupaciones y principios de responsabilidad única. La arquitectura limpia, muy famosa en la comunidad de Android, es un conjunto de buenas prácticas a seguir cuando se busca una buena arquitectura que pone énfasis en el uso de diferentes modelos para diferentes capas de la arquitectura. Esto significa que un proyecto con 3 o 4 modelos de datos diferentes se convierte en estándar.

Eso significa que el número de clases de datos en un proyecto es muy alto, y operaciones como crear, leer, modificar, copiar, comparar, mapear classes las clases de modelo de datos son tareas diarias que se benefician del código generado automáticamente por la clase de datos Kotin. Ahorra una gran cantidad de tiempo y reduce las oportunidades de introducir errores.

  • No más Valor automático

El uso de tipos de valor es una buena práctica muy extendida en Android, especialmente entre las clases de datos.

Un tipo de valor es un objeto de una clase de valor inmutable.
Una clase de valor es una clase para la que la igualdad depende de su contenido.
Una clase inmutable es una clase que no se puede modificar después de su creación.

AutoValue es una popular biblioteca de Google que nos ayuda a crear tipos de valor. Hace su trabajo pero es muy detallado para algo que no debería ser tan complicado.

El uso de las clases kotlin data con el modificador de acceso val nos da una aproximación lo suficientemente cercana a los tipos de valor.

data class User(val name : String, val age : Int)

La clase de usuario anterior es una clase lo suficientemente cercana a los tipos de valor, en una sintaxis mucho más corta. Para un buen grupo de personas, AutoValue puede ser reemplazado por Kotlin. Menos código, sin procesamiento de anotaciones y una biblioteca menos de la que depender.

Nota: Kotlin ofrece propiedades de solo lectura y clases con la palabra clave val. Solo lectura e inmutable no es lo mismo (más aquí), pero en general, se considera lo suficientemente bueno para fines prácticos.

  • No more Lombok

Una de las formas en que los desarrolladores intentaron ahorrar tiempo al tratar con clases de datos es utilizando bibliotecas para generar métodos getter y setter. Lombok es uno de los (in)famosos en Android / Java. Requiere no solo la biblioteca, sino también un complemento para AS. La larga historia corta es que para la mayoría de los desarrolladores Lombok trae tantas ventajas como dolores de cabeza, por lo que se convierte en esa biblioteca que empiezas a amar, pero después de un tiempo, no puedes esperar a deshacerte de ella, pero nunca lo haces porque está en todas partes.

Dado que las clases de datos Kotlin no requieren escribir manualmente métodos getter/setter, la necesidad principal de Lombok ha desaparecido.

  • RecyclerView DiffUtil

RecyclerView en Android es el widget. Está en todas las aplicaciones en varias pantallas. Un componente importante al implementar adaptadores RecyclerView es la clase DiffUtil. DiffUtil calcula la diferencia entre dos listas para enviar los cambios, si los hay, al adaptador. Es mucho más eficiente que actualizar toda la lista una y otra vez, y anima los cambios maravillosamente.

DiffUtil depende en gran medida de la igualdad de los elementos, lo que significa que dos elementos son iguales cuando su contenido es el mismo. Cada vez que use un RecyclerView, debería usar DiffUtil, lo que significa que necesita una forma de comparar si dos objetos contienen la misma información. Esta es una de las razones por las que necesitamos anular equals en nuestras clases de datos Java, pero con las clases Kotlin data es gratis.

Nota: Si no está familiarizado con DiffUtil, consulte esta guía de inicio rápido.

  • Pruebas

En las clases de prueba, comprobamos constantemente si los valores esperados coinciden con los valores reales. Comparar la igualdad de objetos (como se describió anteriormente) es una tarea muy común.

Incluso si no necesita reemplazar el triplete equals , toString y toHash para el tiempo de ejecución regular de su aplicación, las posibilidades de que necesite reemplazar esos métodos con fines de prueba son altas, por lo que las clases de datos Kotlin despejan el camino a las pruebas sin excusas.

Anexo

Hablemos de otros escenarios comunes que vale la pena mencionar cuando se trabaja con clases de datos Kotlin. Esto no está estrictamente relacionado con las clases de datos, pero son especialmente comunes entre ellas.

  • Varios constructores
data class User(val name : String, val age : Int)

En la clase User que definimos anteriormente, necesitamos especificar explícitamente name y age al crear una instancia como User("Steve", 56).

En Kotlin podemos definir valores predeterminados para los argumentos de tal manera que en caso de que no pasemos un valor para ese argumento, se le asigne el valor predeterminado.

data class User(val name : String, val age :Int = 0)

User("Steve", 56) sigue siendo válido, pero ahora se permite un segundo constructor User("Steve"). En ese caso, el valor age será 0.

Podemos asignar valores predeterminados a ambos parámetros permitiendo un tercer constructor User() donde el name estará vacío y el age será 0.

data class User(val name : String = "", val age : Int = 0)

Así que ahora User(), User("Steve") y User("Steve",56) todas son llamadas válidas.

Esto tiene algunas limitaciones: Los parámetros opcionales deben ser los últimos parámetros del constructor. El siguiente no compilará.

data class User(val name : String = "", val age : Int)
val user = User(56) // This doesn't compile

Otra limitación es que si tenemos múltiples parámetros opcionales, deben saltarse de derecha a izquierda.

data class User(
val name : String,
val surname : String = "",
val age : Int = 0
)User("Steve")
User("Steve", "Jobs")
User("Steve", "Jobs", 56)
User("Steve",56) // This wont compile

Para hacer frente a estas limitaciones, Kotlin ofrece argumentos con nombre. Esto nos permite especificar a qué argumento pertenece cada valor. Ahora podemos hacer cosas comoUser(name = "Steve", age = 56) – o más corto User("Steve", age = 56) – donde el apellido se asignará al valor predeterminado.

Los argumentos predeterminados y los argumentos con nombre son una forma muy práctica de ofrecer múltiples constructores y sobrecargas desde una declaración muy compacta.

  • @JvmOverloads

Todo lo explicado en el punto anterior no tiene en cuenta las llamadas desde el lado Java. Kotlin es interoperable con Java, por lo que necesitamos poder crear instancias de la clase User desde Java.

Si no va a usarlo desde Java, entonces habrá terminado, pero de lo contrario, necesitará la anotación @JvmOverloads dado que Java no ofrece argumentos con nombre.

data class User @JvmOverloads constructor(
val name : String,
val surname : String = "",
val age : Int = 0
)

Qué está haciendo esto generando múltiples constructores para que se pueda llamar desde Java.

Nota: Es importante notar que no va a crear todas las permutaciones posibles, sino las resultantes de eliminar los argumentos opcionales de derecha a izquierda. Más información aquí

  • Constructores

Dado que Kotlin ofrece parámetros con nombre y argumentos predeterminados, es menos probable que sea necesario crear constructores. Además de eso, los IDE modernos como Android Studio ya muestran el nombre del parámetro en el lado de llamada, lo que facilita la lectura, lo que hace que sea menos interesante solo por motivos de legibilidad.

En los casos en que el patrón de constructor todavía se necesita. Kotlin no ofrece nada especial para ayudarnos con esto. Un patrón de constructor en Kotlin se ve como:

  • Anotaciones

Es muy común que algunas clases de modelos de datos usen procesamiento de anotaciones. Por ejemplo, los modelos de API (clases de datos para representar respuestas deserializadas de la API) a menudo se anotan con anotaciones Gson o Moshi. Los modelos de persistencia (clases de datos para representar datos almacenados en bases de datos/almacenamiento local) a menudo se anotan con anotaciones de Sala o Reino.

En Kotlin todavía podemos usar esas anotaciones. Por ejemplo, este es un modelo de usuario que se almacena en la base de datos de la sala:

Resumen

Las clases de datos de Kotlin son el resultado de años de aprendizaje a partir del dolor y la frustración con las clases de datos en Java. Su objetivo es tener todas las ventajas y ninguna de las desventajas. Usarlas es muy simple y agradable y una vez que te acostumbras, es muy difícil mirar hacia atrás.

En Android, nos ayudan de diferentes maneras, pero sobre todo ahorran mucho tiempo y reducen los errores.

Una clase de datos Kotlin es un buen ejemplo de lo que es Kotlin como lenguaje de programación: conciso, pragmático y divertido para los desarrolladores.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

lg