Mapping von Datenobjekten in Kotlin

Abbildung von Datenobjekten in Kotlin

1. Intro

Bei der Arbeit mit einer älteren Codebasis, der Verwendung einer externen Bibliothek oder der Integration in ein Framework treten regelmäßig Anwendungsfälle auf, in denen unterschiedliche Objekte oder Datenstrukturen abgebildet werden sollen.

In diesem Tutorial werden wir uns ansehen, wie Sie dieses Ziel mithilfe der integrierten Kotlin-Funktionen auf einfache Weise erreichen können.

2. Einfache Erweiterungsfunktion

Verwenden wir das folgende Beispiel: Wir haben eine Klasse User, bei der es sich möglicherweise um eine Klasse aus unserer Kerndomäne handelt. Es ist auch möglich, dass es sich um eine Entität handelt, die wir aus einer relationalen Datenbank laden.

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

Jetzt wollen wir eine andere Sicht auf diese Daten geben. Wir haben beschlossen, diese KlasseUserView zu nennen, und können uns vorstellen, dass sie als Antwortsendung von einem Webcontroller verwendet wird. Während es dieselben Daten in unserer Domäne darstellt, sind einige Felder eine Zusammenfassung der Felder unserer Benutzerklasse, und einige Felder haben einfach einen anderen Namen:

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

Was wir jetzt brauchen, ist eine Mapping-Funktion, dieUser → _UserView_ abbildet. Da sichUserView auf der äußeren Ebene unserer Anwendung befindet, möchten wir diese Funktion nicht zu unserer Benutzerklasse hinzufügen. Wir möchten auch nicht die Kapselung unserer Benutzerklasse aufheben und eine Hilfsklasse verwenden, um in unser Benutzerobjekt zu greifen und dessen Daten abzurufen, um einUserView-Objekt zu erstellen.

Glücklicherweise bietet Kotlin eine Sprachfunktion namensExtension Functions. Wir können eine Erweiterungsfunktion für unsere Benutzerklasse definieren und sie nur innerhalb des Paketbereichs zugänglich machen, den wir definiert haben:

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

Verwenden Sie diese Funktion in einem Test, um ein Gefühl für die Verwendung zu bekommen:

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. Kotlin-Reflexionsmerkmale

Während das obige Beispiel sehr einfach ist (und daher für die meisten Anwendungsfälle empfohlen wird), handelt es sich immer noch um ein bisschen Code für das Boilerplate. Was ist, wenn wir eine Klasse mit vielen Feldern (vielleicht Hunderten) haben und die meisten von ihnen dem Feld mit dem gleichen Namen in der Zielklasse zugeordnet werden müssen?

In diesem Fall können wir darüber nachdenken, die Funktionen vonKotlin Reflectionzu verwenden, um zu vermeiden, dass der größte Teil des Zuordnungscodes geschrieben wird.

Die Zuordnungsfunktion mit Reflektion sieht folgendermaßen aus:

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

Wir verwenden den StandardkonstruktorUserViewals Methodenaufrufempfänger, indem wir die Funktion Kotlinwith()verwenden. Innerhalb der Lambda-Funktion, diewith(), zur Verfügung gestellt wird, verwenden wir Reflexion, umMap der Elementeigenschaften (mit dem Elementnamen als Schlüssel und der Elementeigenschaft als Wert) unter Verwendung vonUser::class.memberProperties.associateBy \{ it.name } zu erhalten.

Als nächstes rufen wir den KonstruktorUserViewmit einer benutzerdefinierten Parameterzuordnung auf. Innerhalb des Lambda stellen wir eine bedingte Zuordnung mit dem Schlüsselwortwhen bereit.

Eine interessante Tatsache ist, dass wir die tatsächlichen Parameternamen, die wir mit Reflexion abrufen, wieUserView::name.name anstelle von einfachenStrings abbilden können. This means we can completely leverage the Kotlin compiler here hilft uns bei Refactorings, ohne zu befürchten, dass unser Code beschädigt wird.

Wir haben einige spezielle Zuordnungen für den Parameternamen, die Adresse und das Telefon, während wir für jedes andere Feld eine auf dem Standardnamen basierende Zuordnung verwenden.

Während der reflexionsbasierte Ansatz auf den ersten Blick sehr interessant erscheint, sollten Sie bedenken, dass dies die Codebasis komplexer macht und sich die Verwendung von Reflexion möglicherweise negativ auf die Laufzeitleistung auswirkt.

4. Fazit

Wir haben gesehen, dass wir einfache Anwendungsfälle für die Datenzuordnung mithilfe der integrierten Kotlin-Sprachfunktionen leicht lösen können. Während das Schreiben des Zuordnungscodes von Hand für einfache Anwendungsfälle in Ordnung ist, können wir auch komplexere Lösungen mit Reflektion schreiben.

Sie finden alle Codebeispieleover on GitHub.