Cartographie des objets de données dans Kotlin

Cartographie des objets de données dans Kotlin

1. Intro

Lorsque vous travaillez avec une base de code héritée, en utilisant une bibliothèque externe ou en s'intégrant dans un framework, nous avons régulièrement des cas d'utilisation où nous souhaitons mapper entre différents objets ou structures de données.

Dans ce tutoriel, nous verrons comment atteindre facilement cet objectif à l'aide des fonctionnalités intégrées de Kotlin.

2. Fonction d'extension simple

Prenons l'exemple suivant: Nous avons une classe User, qui peut être une classe de notre domaine principal. Il est également possible qu’il s’agisse d’une entité que nous chargeons à partir d’une base de données relationnelle.

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

Maintenant, nous voulons donner un point de vue différent sur ces données. Nous avons décidé d'appeler cette classeUserView et nous pouvons imaginer qu'elle est utilisée comme réponse envoyée par un contrôleur Web. Bien qu'il représente les mêmes données dans notre domaine, certains champs sont un agrégat des champs de notre classe User et certains champs ont simplement un nom différent:

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

Ce dont nous avons besoin maintenant, c'est d'une fonction de mappage, qui mapperaUser → _UserView_. PuisqueUserView se trouve sur la couche externe de notre application, nous ne voulons pas ajouter cette fonction à notre classe User. Nous ne voulons pas non plus interrompre l’encapsulation de notre classe User et utiliser une classe d’assistance pour accéder à notre objet User et extraire ses données, afin de créer un objetUserView.

Heureusement, Kotlin fournit une fonctionnalité de langage appeléeExtension Functions. Nous pouvons définir une fonction d'extension sur notre classe User et la rendre accessible uniquement à l'intérieur de la portée du package pour laquelle nous l'avons définie:

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

Utilisons cette fonction dans un test pour avoir une idée de son utilisation:

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. Caractéristiques de Kotlin Reflection

Bien que l'exemple ci-dessus soit très simple (et donc recommandé dans la plupart des cas d'utilisation), il implique néanmoins un peu de code passe-partout. Et si nous avons une classe avec beaucoup de champs (peut-être des centaines) et que la plupart d'entre eux doivent être mappés sur le champ avec le même nom dans la classe cible?

Dans ce cas, nous pouvons penser à utiliser les fonctionnalitésKotlin Reflection pour éviter d'écrire la majeure partie du code de mappage.

La fonction de mappage utilisant la réflexion ressemble à ceci:

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

Nous utilisons le constructeur par défaut deUserView comme récepteur d'appel de méthode en utilisant la fonction Kotlinwith(). Dans la fonction lambda fournie àwith(),, nous utilisons la réflexion pour obtenir unMap de propriétés de membre (avec le nom du membre comme clé et la propriété membre comme valeur) en utilisantUser::class.memberProperties.associateBy \{ it.name }.

Ensuite, nous appelons le constructeurUserView avec un mappage de paramètres personnalisé. À l'intérieur du lambda, nous fournissons un mappage conditionnel, en utilisant le mot cléwhen.

Un fait intéressant est que nous pouvons mapper les noms de paramètres réels que nous récupérons en utilisant la réflexion, commeUserView::name.name au lieu de simplesStrings. This means we can completely leverage the Kotlin compiler here, nous aidant en cas de refactorisation sans craindre que notre code ne casse.

Nous avons des mappages spéciaux pour les paramètres nom, adresse et téléphone, tandis que nous utilisons un mappage basé sur le nom par défaut pour tous les autres champs.

Bien que l'approche basée sur la réflexion semble très intéressante à première vue, gardez à l'esprit que cela introduit une complexité supplémentaire dans la base de code et que l'utilisation de la réflexion pourrait avoir un impact négatif sur les performances d'exécution.

4. Conclusion

Nous avons vu que nous pouvons facilement résoudre des cas d'utilisation simples de mappage de données à l'aide des fonctionnalités intégrées du langage Kotlin. Tandis que l'écriture manuelle du code de mappage convient aux cas d'utilisation simples, nous pouvons également écrire des solutions plus complexes en utilisant la réflexion.

Vous pouvez trouver tous les exemples de codeover on GitHub.