Formas prácticas de mapear datos en Kotlin

La asignación de datos es una forma de separar el código de la aplicación en capas. El mapeo se usa ampliamente en aplicaciones de Android. Un ejemplo popular de la arquitectura de la aplicación móvil Android-CleanArchitecture utiliza el mapeo tanto en la versión original ( un ejemplo de un mapeador de CleanArchitecture ) como en la nueva versión de Kotlin ( un ejemplo de un mapeador ).


La asignación le permite desatar las capas de la aplicación (por ejemplo, deshacerse de la API), simplificar y hacer que el código sea más visual.


En el diagrama se muestra un ejemplo de mapeo útil:



No es necesario transferir todos los campos del modelo Person si en la parte de la aplicación que nos interesa solo necesitamos dos campos: nombre de login y password . Si es más conveniente para nosotros considerar a Person como un usuario de la aplicación, después del mapa podemos usar fácilmente un modelo con un nombre que entendemos.


Consideremos métodos prácticos y prácticos de mapeo de datos usando el ejemplo de convertir dos modelos de Person y Salary de la capa Source al modelo de capa Destination .



Por ejemplo, los modelos están simplificados. Person contiene Salary en ambas capas de la aplicación.


En este código, si tiene el mismo modelo, puede valer la pena revisar las capas de la aplicación y no utilizar el mapeo.


Método n. ° 1: métodos de mapeador


Un ejemplo:


 class PersonSrc( private val name: String, private val salary: SalarySrc ) { fun mapToDestination() = PersonDst( name, salary.mapToDestination() //    Salary ) } class SalarySrc( private val amount: Int ) { fun mapToDestination() = SalaryDst(amount) } 

El método más rápido y fácil. Es él quien se utiliza en CleanArchitecture Kotlin ( un ejemplo de mapeo ).


Una ventaja es la capacidad de ocultar campos. Los campos en PersonSrc pueden ser private , el código que usa la clase PersonSrc es independiente de ellos, lo que significa que se reduce la coherencia del código.


Dicho código es más rápido de escribir y más fácil de modificar: las declaraciones de campo y su uso están en un solo lugar. No es necesario ejecutar el proyecto y modificar diferentes archivos al cambiar los campos de clase.


Sin embargo, esta opción es más difícil de probar. El método del mapeador de la clase PersonSrc PersonSrc llamada al método del mapeador SalarySrc . Por lo tanto, probar solo el mapeo de Person sin el mapeo de Salary será más difícil. Tendrás que usar moki para esto.


Puede surgir otro problema si, de acuerdo con los requisitos de la arquitectura, las capas de aplicación no pueden conocerse entre sí: es decir en la clase Src de una capa, no puede trabajar con una capa Dst y viceversa. En este caso, esta versión de mapeo no se puede usar.


En el ejemplo considerado, la capa Src depende de la capa Dst y puede crear clases de esta capa. Para la situación opuesta (cuando Dst depende de Src ), la opción con métodos de fábrica estáticos es adecuada:


 class PersonDst( private val name: String, private val salary: SalaryDst ) { companion object { fun fromSource( src: PersonSrc ) = PersonDst(src.name, SalaryDst.fromSource(src.salary)) } } class SalaryDst( private val amount: Int ) { companion object { fun fromSource(src: SalarySrc) = SalaryDst(src.amount) } } 

La asignación se encuentra dentro de las clases de la capa Dst , lo que significa que estas clases no revelan todas sus propiedades y estructura al código que las usa.


Si en la aplicación una capa depende de la otra y los datos se transfieren entre las capas de la aplicación en ambas direcciones, es lógico utilizar métodos de fábrica estáticos junto con métodos de mapeador.


Resumen del método de mapeo:


+ Escribe código rápidamente, el mapeo siempre está a mano
+ Modificación fácil
+ Conectividad de código bajo
- Prueba de unidad difícil (se necesita moki)
- No siempre permitido por la arquitectura


Método 2: Funciones del mapeador


Modelos:


 class PersonSrc( val name: String, val salary: SalarySrc ) class SalarySrc(val amount: Int) class PersonDst( val name: String, val salary: SalaryDst ) class SalaryDst(val amount: Int) 

Mapeadores:


 fun mapPerson( src: PersonSrc, salaryMapper: (SalarySrc) -> SalaryDst = ::mapSalary //  - ) = PersonDst( src.name, salaryMapper.invoke(src.salary) ) fun mapSalary(src: SalarySrc) = SalaryDst(src.amount) 

En este ejemplo, mapPerson es una función de orden superior ya que ella obtiene el mapeador para el modelo Salary . Una característica interesante del ejemplo específico es el argumento predeterminado para esta función. Este enfoque nos permite simplificar el código de llamada y al mismo tiempo redefinir fácilmente el mapeador en pruebas unitarias. Puede usar este método de mapeo sin el método predeterminado, pasándolo siempre en el código de llamada.


Colocar el asignador y las clases con las que trabaja en diferentes lugares del proyecto no siempre es conveniente. Con la modificación frecuente de la clase, tendrá que buscar y modificar diferentes archivos en diferentes lugares.


Este método de mapeo requiere que todas las propiedades con datos de clase sean visibles para el mapeador, es decir private visibilidad private no se puede utilizar para ellos.


Resumen del método de mapeo:


+ Prueba de unidad simple
- Modificación difícil
- Requiere campos abiertos para clases de datos


Método 3: Funciones de extensión


Mapeadores:


 fun PersonSrc.toDestination( salaryMapper: (SalarySrc) -> SalaryDst = SalarySrc::toDestination ): PersonDst { return PersonDst(this.name, salaryMapper.invoke(this.salary)) } fun SalarySrc.toDestination(): SalaryDst { return SalaryDst(this.amount) } 

En general, lo mismo que las funciones del mapeador, pero la sintaxis de la llamada del mapeador es más simple: .toDestination() .


Cabe señalar que las funciones de extensión pueden conducir a un comportamiento inesperado debido a su naturaleza estática: https://kotlinlang.org/docs/reference/extensions.html#extensions-are-resolved-statical


Resumen del método de mapeo:


+ Prueba de unidad simple
- Modificación difícil
- Requiere campos abiertos para clases de datos


Método 4: Clases de asignador con una interfaz


Los ejemplos de funciones tienen un inconveniente. Le permiten usar cualquier función con una firma (SalarySrc) -> SalaryDst . La presencia de la interfaz Mapper<SRC, DST> ayudará a que el código sea más obvio.


Un ejemplo:


 interface Mapper<SRC, DST> { fun transform(data: SRC): DST } class PersonMapper( private val salaryMapper: Mapper<SalarySrc, SalaryDst> ) : Mapper<PersonSrc, PersonDst> { override fun transform(src: PersonSrc) = PersonDst( src.name, salaryMapper.transform(src.salary) ) } class SalaryMapper : Mapper<SalarySrc, SalaryDst> { override fun transform(src: SalarrSrc) = SalaryDst( src.amount ) } 

En este ejemplo, SalaryMapper es una dependencia de PersonMapper . Esto le permite reemplazar convenientemente el mapeador de Salary para las pruebas unitarias.


Con respecto a la asignación en la función, este ejemplo solo tiene un inconveniente: la necesidad de escribir un poco más de código.


Resumen del método de mapeo:


+ Mejor escritura
- Más código


Al igual que las funciones del mapeador:


+ Prueba de unidad simple
- Modificación difícil
- requiere campos abiertos para clases de datos


Método 5: Reflexión


El método de la magia negra. Considere este método en otros modelos.


Modelos:


 data class EmployeeSrc( val firstName: String, val lastName: String, val age: Int //    ) data class EmployeeDst( val name: String, //  ,    val age: Int //    ) 

Mapeador:


 fun EmployeeSrc.mapWithRef() = with(::EmployeeDst) { val propertiesByName = EmployeeSrc::class.memberProperties.associateBy { it.name } callBy(parameters.associateWith { parameter -> when (parameter.name) { EmployeeDst::name.name -> "$firstName $lastName" //    name else -> propertiesByName[parameter.name]?.get(this@mapWithRef) //     } }) } 

Aquí se ve un ejemplo.


En este ejemplo, EmployeeSrc y EmployeeDst almacenan el nombre en diferentes formatos. Mapper solo necesita hacer un nombre para el nuevo modelo. Los campos restantes se procesan automáticamente, sin escribir código (la opción else es when ).


El método puede ser útil, por ejemplo, si tiene modelos grandes con un montón de campos y los campos básicamente coinciden para los mismos modelos de diferentes capas.


Un gran problema surgirá, por ejemplo, si agrega los campos obligatorios a Dst y no está en Src o en el asignador: una IllegalArgumentException en tiempo de ejecución. La reflexión también tiene problemas de rendimiento.


Resumen del método de mapeo:


+ menos código
+ prueba unitaria simple
peligroso
- puede afectar negativamente el rendimiento


Conclusiones


Dichas conclusiones pueden extraerse de nuestra consideración:


Métodos de mapeador : código claro, más rápido para escribir y mantener


Funciones de mapeador y funciones de extensión : solo pruebe el mapeo.


Clases de mapeador con interfaz : solo pruebe el mapeo y el código más claro.


Reflexión : adecuado para situaciones no estándar.

Source: https://habr.com/ru/post/485416/


All Articles