Отображение объектов данных в Котлине

Отображение объектов данных в Котлине

1. вступление

При работе с устаревшей кодовой базой, с использованием внешней библиотеки или интеграции с платформой у нас регулярно возникают случаи, когда мы хотим отобразить различные объекты или структуры данных.

В этом руководстве мы рассмотрим, как легко достичь этой цели с помощью встроенных функций Kotlin.

2. Простая функция расширения

Давайте воспользуемся следующим примером: у нас есть класс User, который может быть классом из нашего основного домена. Также возможно, что это объект, который мы загружаем из реляционной базы данных.

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

Теперь мы хотим дать другое представление об этих данных. Мы решили назвать этот классUserView и можем представить, что он будет использоваться в качестве ответа от веб-контроллера. Хотя он представляет одни и те же данные в нашем домене, некоторые поля являются совокупностью полей нашего класса User, а некоторые поля просто имеют другое имя:

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

Теперь нам нужна функция отображения, которая будет отображатьUser → _UserView_. ПосколькуUserView находится на внешнем уровне нашего приложения, мы не хотим добавлять эту функцию в наш класс User. Мы также не хотим нарушать инкапсуляцию нашего класса User и использовать вспомогательный класс для доступа к нашему объекту User и извлечения его данных, чтобы создать объектUserView.

К счастью, Kotlin предоставляет языковую функцию под названиемExtension Functions. Мы можем определить функцию расширения в нашем классе User и сделать ее доступной только в той области пакета, в которой мы ее определили:

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

Давайте воспользуемся этой функцией внутри теста, чтобы понять, как ее использовать:

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

Хотя приведенный выше пример очень прост (и поэтому рекомендуется для большинства случаев использования), он все же включает в себя немного стандартного кода. Что если у нас есть класс с большим количеством полей (возможно, сотен), и большинство из них должны быть сопоставлены с полем с тем же именем в целевом классе?

В этом случае мы можем подумать об использовании функцийKotlin Reflection, чтобы избежать написания большей части кода сопоставления.

Функция отображения с использованием отражения выглядит следующим образом:

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

Мы используем конструктор по умолчаниюUserView в качестве получателя вызова метода, используя функцию Kotlinwith(). Внутри лямбда-функции, предоставленнойwith(),, мы используем отражение, чтобы получитьMap свойств члена (с именем члена в качестве ключа и свойством члена в качестве значения) с использованиемUser::class.memberProperties.associateBy \{ it.name }.

Затем мы вызываем конструкторUserView с отображением настраиваемых параметров. Внутри лямбда-выражения мы обеспечиваем условное отображение, используя ключевое словоwhen.

Интересным фактом является то, что мы можем отображать фактические имена параметров, которые мы получаем, используя отражение, напримерUserView::name.name вместо простогоStrings. This means we can completely leverage the Kotlin compiler here, помогая нам в случае рефакторинга, не опасаясь, что наш код может сломаться.

У нас есть некоторые специальные сопоставления для имени параметра, адреса и телефона, в то время как мы используем сопоставление по умолчанию на основе имени для каждого другого поля.

Хотя подход, основанный на отражении, на первый взгляд кажется очень интересным, имейте в виду, что это вносит дополнительную сложность в базу кода, и использование отражения может оказать негативное влияние на производительность во время выполнения.

4. Заключение

Мы увидели, что можем легко решать простые варианты использования отображения данных, используя встроенные функции языка Kotlin. Хотя написание кода отображения вручную подходит для простых случаев использования, мы также можем писать более сложные решения с использованием отражения.

Вы можете найти все примеры кодаover on GitHub.