Mapeamento de objetos de dados no Kotlin

Mapeamento de objetos de dados no Kotlin

1. Introdução

Ao trabalhar com uma base de código herdada, usando uma biblioteca externa ou integrando-se a uma estrutura, temos regularmente casos de uso em que queremos mapear entre diferentes objetos ou estruturas de dados.

Neste tutorial, veremos como atingir facilmente esse objetivo usando os recursos integrados do Kotlin.

2. Função de extensão simples

Vamos usar o seguinte exemplo: Temos uma classe User, que pode ser uma classe de nosso domínio principal. Também é possível que seja uma entidade que carregamos de um banco de dados relacional.

data class User(
  val firstName: String,
  val lastName: String,
  val street: String,
  val houseNumber: String,
  val phone: String,
  val age: Int,
  val password: String)

Agora, queremos fornecer uma visão diferente desses dados. Decidimos chamar essa classe deUserViewe podemos imaginá-la sendo usada como uma resposta enviada de um controlador da web. Embora represente os mesmos dados em nosso domínio, alguns campos são um agregado dos campos de nossa classe de usuário e alguns campos simplesmente têm um nome diferente:

data class UserView(
  val name: String,
  val address: String,
  val telephone: String,
  val age: Int
)

O que precisamos agora é uma função de mapeamento, que mapearáUser → _UserView_. ComoUserView está na camada externa de nosso aplicativo, não queremos adicionar esta função à nossa classe de usuário. Também não queremos quebrar o encapsulamento de nossa classe User e usar uma classe auxiliar para acessar nosso objeto User e extrair seus dados, a fim de criar um objetoUserView.

Felizmente, o Kotlin oferece um recurso de linguagem chamadoExtension Functions. Podemos definir uma função de extensão em nossa classe de usuário e torná-la acessível apenas dentro do escopo do pacote que definimos:

fun User.toUserView() = UserView(
  name = "$firstName $lastName",
  address = "$street $houseNumber",
  telephone = phone,
  age = age
)

Vamos usar esta função dentro de um teste para ter uma ideia de como usá-la:

class UserTest {

    @Test
    fun `maps User to UserResponse using extension function`() {
        val p = buildUser()
        val view = p.toUserView()
        assertUserView(view)
    }

    private fun buildUser(): User {
        return User(
          "Java",
          "Duke",
          "Javastreet",
          "42",
          "1234567",
          30,
          "s3cr37"
        )
    }

    private fun assertUserView(pr: UserView) {
        assertAll(
          { assertEquals("Java Duke", pr.name) },
          { assertEquals("Javastreet 42", pr.address) },
          { assertEquals("1234567", pr.telephone) },
          { assertEquals(30, pr.age) }
        )
    }

3. Recursos de reflexão Kotlin

Embora o exemplo acima seja muito simples (e, portanto, recomendado para a maioria dos casos de uso), ele ainda envolve um pouco de código padrão. E se tivermos uma classe com muitos campos (talvez centenas) e a maioria deles tiver que ser mapeada para o campo com o mesmo nome na classe de destino?

Nesse caso, podemos pensar em usar os recursosKotlin Reflection para evitar a gravação da maior parte do código de mapeamento.

A função de mapeamento usando reflexão é assim:

fun User.toUserViewReflection() = with(::UserView) {
    val propertiesByName = User::class.memberProperties.associateBy { it.name }
    callBy(parameters.associate { parameter ->
        parameter to when (parameter.name) {
            UserView::name.name -> "$firstName $lastName"
            UserView::address.name -> "$street $houseNumber"
            UserView::telephone.name -> phone
            else -> propertiesByName[parameter.name]?.get([email protected])
        }
    })
}

Estamos usando o construtor padrãoUserView como o receptor de chamada de método usando a função Kotlinwith(). Dentro da função lambda fornecida awith(),, usamos reflexão para obterMap das propriedades do membro (com o nome do membro como a chave e a propriedade do membro como o valor) usandoUser::class.memberProperties.associateBy \{ it.name }.

Em seguida, chamamos o construtorUserView com um mapeamento de parâmetro personalizado. Dentro do lambda, fornecemos um mapeamento condicional, usando a palavra-chavewhen.

Um fato interessante é que podemos mapear os nomes dos parâmetros reais que recuperamos usando reflexão, comoUserView::name.name em vez deStrings simples. This means we can completely leverage the Kotlin compiler here, ajudando-nos no caso de refatorações sem temer que nosso código possa quebrar.

Temos alguns mapeamentos especiais para os parâmetros nome, endereço e telefone, enquanto usamos um mapeamento baseado em nome padrão para todos os outros campos.

Embora a abordagem baseada em reflexão pareça muito interessante à primeira vista, lembre-se de que isso introduz complexidade adicional na base de código e o uso da reflexão pode ter um impacto negativo no desempenho do tempo de execução.

4. Conclusão

Vimos que podemos resolver facilmente casos de uso de mapeamento de dados simples usando recursos integrados da linguagem Kotlin. Embora escrever o código de mapeamento manualmente seja adequado para casos de uso simples, também podemos escrever soluções mais complexas usando reflexão.

Você pode encontrar todos os exemplos de códigoover on GitHub.