O mapeamento de dados é uma maneira de separar o código do aplicativo em camadas. O mapeamento é amplamente usado em aplicativos Android. Um exemplo popular da arquitetura do aplicativo móvel Android-CleanArchitecture usa o mapeamento na versão original ( um exemplo de um mapeador da CleanArchitecture ) e na nova versão do Kotlin ( um exemplo de um mapeador ).
O mapeamento permite desatar as camadas do aplicativo (por exemplo, livrar-se da API), simplificar e tornar o código mais visual.
Um exemplo de mapeamento útil é mostrado no diagrama:

Não é necessário transferir todos os campos do modelo Person
se, na parte do aplicativo que nos interessa, precisarmos de apenas dois campos: login
e password
. Se for mais conveniente considerar Person
como um usuário do aplicativo, depois do mapa, podemos usar facilmente um modelo com um nome que entendemos.
Vamos considerar métodos convenientes e práticos de mapeamento de dados usando o exemplo de conversão de dois modelos de Person
e Salary
da camada Source
para o modelo da camada Destination
.

Por exemplo, os modelos são simplificados. Person
contém Salary
nas duas camadas do aplicativo.
Nesse código, se você tiver o mesmo modelo, pode valer a pena revisar as camadas do aplicativo e não usar o mapeamento.
Método # 1: Métodos do Mapeador
Um exemplo:
class PersonSrc( private val name: String, private val salary: SalarySrc ) { fun mapToDestination() = PersonDst( name, salary.mapToDestination()
O método mais rápido e fácil. É ele quem é usado no CleanArchitecture Kotlin ( um exemplo de mapeamento ).
Uma vantagem é a capacidade de ocultar campos. Os campos no PersonSrc
podem ser private
, o código que usa a classe PersonSrc
é independente deles, o que significa que a coerência do código é reduzida.
Esse código é mais rápido de escrever e mais fácil de modificar - as declarações de campo e seu uso estão em um só lugar. Não há necessidade de executar o projeto e modificar arquivos diferentes ao alterar os campos de classe.
No entanto, essa opção é mais difícil de testar. O método mapeador da classe PersonSrc PersonSrc
chamada para o método mapeador SalarySrc
. Portanto, testar apenas o mapeamento de pessoas sem o mapeamento de Salary
será mais difícil. Você terá que usar moki para isso.
Outro problema pode surgir se, de acordo com os requisitos da arquitetura, as camadas de aplicação não puderem se conhecer: na classe Src
de uma camada, você não pode trabalhar com uma camada Dst
e vice-versa. Nesse caso, esta versão do mapeamento não pode ser usada.
No exemplo considerado, a camada Src
é dependente da camada Dst
e pode criar classes dessa camada. Para a situação oposta (quando Dst
depende de Src
), a opção com métodos estáticos de fábrica é adequada:
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) } }
O mapeamento está dentro das classes da camada Dst
, o que significa que essas classes não revelam todas as suas propriedades e estruturas ao código que as utiliza.
Se no aplicativo uma camada é dependente da outra e os dados são transferidos entre as camadas do aplicativo em ambas as direções, é lógico usar métodos estáticos de fábrica junto com os métodos do mapeador.
Resumo do método de mapeamento:
+
Escreva código rapidamente, o mapeamento está sempre à mão
+
Fácil modificação
+
Baixa conectividade de código
-
Teste de unidade difícil (moki necessário)
-
Nem sempre é permitido pela arquitetura
Método 2: Funções do 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
Neste exemplo, mapPerson
é uma função de ordem superior, pois ela obtém o mapeador para o modelo Salary
. Um recurso interessante do exemplo específico é o argumento padrão para esta função. Essa abordagem nos permite simplificar o código de chamada e ao mesmo tempo redefinir facilmente o mapeador em testes de unidade. Você pode usar esse método de mapeamento sem o método padrão, passando sempre no código de chamada.
Colocar o mapeador e as classes com as quais ele trabalha em diferentes locais do projeto nem sempre é conveniente. Com modificações frequentes da classe, você terá que pesquisar e modificar arquivos diferentes em lugares diferentes.
Esse método de mapeamento exige que todas as propriedades com dados de classe sejam visíveis para o mapeador, ou seja, private
visibilidade private
não pode ser usada para eles.
Resumo do método de mapeamento:
+
Teste simples de unidade
-
Modificação difícil
-
Requer campos abertos para classes de dados
Método 3: Funções de extensão
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) }
Em geral, o mesmo que o mapeador funciona, mas a sintaxe da chamada do mapeador é mais simples: .toDestination()
.
Observe que as funções de extensão podem levar a um comportamento inesperado devido à sua natureza estática: https://kotlinlang.org/docs/reference/extensions.html#extensions-are-resolved-statically
Resumo do método de mapeamento:
+
Teste simples de unidade
-
Modificação difícil
-
Requer campos abertos para classes de dados
Método 4: Mapeador de Classes com uma Interface
Exemplos de funções têm uma desvantagem. Eles permitem que você use qualquer função com uma assinatura (SalarySrc) -> SalaryDst
. A presença da interface Mapper<SRC, DST>
ajudará a tornar o código mais óbvio.
Um exemplo:
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 ) }
Neste exemplo, SalaryMapper
é uma dependência de PersonMapper
. Isso permite que você substitua convenientemente o mapeador de Salary
para testes de unidade.
Em relação ao mapeamento na função, este exemplo tem apenas uma desvantagem - a necessidade de escrever um pouco mais de código.
Resumo do método de mapeamento:
+
Melhor digitação
-
Mais código
Como as funções do mapeador:
+
Teste simples de unidade
-
Modificação difícil
-
requer campos abertos para classes de dados
Método 5: Reflexão
O método da magia negra. Considere este método em outros 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"
Um exemplo é espionado aqui .
Neste exemplo, EmployeeSrc
e EmployeeDst
armazenam o nome em diferentes formatos. O Mapper precisa apenas criar um nome para o novo modelo. Os campos restantes são processados automaticamente, sem escrever código (a opção else
é when
).
O método pode ser útil, por exemplo, se você tiver modelos grandes com vários campos e os campos coincidirem basicamente com os mesmos modelos de camadas diferentes.
Um grande problema surgirá, por exemplo, se você adicionar os campos obrigatórios ao Dst e ele não estiver no Src
ou no mapeador por acidente: uma IllegalArgumentException
em tempo de execução. A reflexão também tem problemas de desempenho.
Resumo do método de mapeamento:
+
menos código
+
teste de unidade simples
-
perigoso
-
pode afetar adversamente o desempenho
Conclusões
Tais conclusões podem ser tiradas de nossa consideração:
Métodos do mapeador - código claro, mais rápido para escrever e manter
Funções do mapeador e funções de extensão - apenas teste o mapeamento.
Classes do mapeador com interface - apenas teste o mapeamento e um código mais claro.
Reflexão - adequado para situações fora do padrão.