Maneiras práticas de mapear dados no Kotlin

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() //    Salary ) } class SalarySrc( private val amount: Int ) { fun mapToDestination() = SalaryDst(amount) } 

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 //  - ) = PersonDst( src.name, salaryMapper.invoke(src.salary) ) fun mapSalary(src: SalarySrc) = SalaryDst(src.amount) 

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 //    ) 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) //     } }) } 

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.

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


All Articles