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()
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
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
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"
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.