Маппинг данных – один из способов для разделения кода приложения на слои. Маппинг широко используется в Android приложениях. Популярный пример архитектуры мобильного приложения Android-CleanArchitecture использует маппинг как в оригинальной версии (пример маппера из CleanArchitecture), так и в новой Kotlin версии (пример маппера).
Маппинг позволяет развязать слои приложения (например, отвязаться от API), упростить и сделать код более наглядным.
Пример полезного маппинга изображен на схеме:
Нет нужды передавать все поля модели Person
, если в интересующей нас части приложения нам нужно только два поля: login
и password
. Если для нас удобнее рассматривать Person
как пользователя приложения, после маппига мы можем легко использовать модель с понятным нам именем.
Рассмотрим удобные и практичные методы маппинга данных на примере преобразования двух моделей Person
и Salary
из слоя Source
в модели слоя Destination
.
Для примера модели упрощены. Person
содержит Salary
в обоих слоях приложения.
В настоящем коде, если у вас одинаковые модели, возможно, стоит пересмотреть слои приложения и не использовать маппинг.
Метод №1: Методы-мапперы
Пример:
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)
}
Самый быстрый и простой метод. Именно он используется в CleanArchitecture Kotlin (пример маппинга).
Плюсом является возможность сокрытия полей. Поля в PersonSrc
могут быть private
, код, использующий класс PersonSrc
, не зависим от них, значит связанность кода сокращается.
Такой код быстрее писать и проще модифицировать – объявления полей и их использование находятся в одном месте. Не надо бегать по проекту и модифицировать разные файлы при изменении полей класса.
Однако, этот вариант сложнее тестировать. В методе-маппере класса PersonSrc
прописан вызов метода-маппера SalarySrc
. Значит протестировать маппинг только Person
без маппинга Salary
будет сложнее. Для этого придется использовать моки.
Еще проблема может возникнуть если по требованиям архитектуры слои приложения не могут знать друг о друге: т.е. в классе Src
слоя нельзя работать со слоем Dst
и наоборот. В этом случае такой вариант маппинга использовать не получится.
В рассмотренном примере слой Src
зависим от слоя Dst
и может создавать классы этого слоя. Для обратной ситуации (когда Dst
зависим от Src
) подойдет вариант со статическими методами-фабриками:
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)
}
}
Маппинг находится внутри классов Dst
слоя, значит эти классы не раскрывают все свои свойства и структуру использующему их коду.
Если в приложении один слой зависим от другого и осуществляется передача данных между слоями приложения в обоих направлениях, статические методы-фабрики логично использовать вместе с методами-мапперами.
Резюме метода маппинга:
+
Быстро писать код, маппинг всегда под рукой
+
Легкая модификация
+
Низкая связность кода
-
Затруднено Unit-тестирование (нужны моки)
-
Не всегда позволено архитектурой
Метод №2: функции-мапперы
Модели:
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)
Мапперы:
fun mapPerson(
src: PersonSrc,
salaryMapper: (SalarySrc) -> SalaryDst = ::mapSalary // аргумент по-умолчанию
) = PersonDst(
src.name,
salaryMapper.invoke(src.salary)
)
fun mapSalary(src: SalarySrc) = SalaryDst(src.amount)
В этом примере mapPerson
является функцией высшего порядка, т.к. ей передается маппер для модели Salary
. Интересной особенностью конкретного примера является аргумент по умолчанию для этой функции. Такой подход позволяет упростить вызывающий код и одновременно без проблем переопределять маппер в unit-тестах. Можно использовать этот метод маппинга и без метода по умолчанию, передавая его всегда в вызывающем коде.
Размещение маппера и классов, с которыми он работает, в разных местах проекта не всегда удобно. При частой модификации класса придётся искать и изменять разные файлы в разных местах.
Этот метод маппинга требует, чтобы все свойства с данными класса были видимы мапперу, т.е. для них нельзя использовать видимость private
.
Резюме метода маппинга:
+
Простое Unit-тестирование
-
Затруднена модификация
-
Требуются открытые поля у классов с данными
Метод № 3: Функции-расширения
Мапперы:
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)
}
В целом то же, что и функции-мапперы, но проще синтаксис вызова маппера: .toDestination()
.
При этом стоит учесть, что функции расширения могут приводить к неожиданному поведению из-за своей статической природы: https://kotlinlang.org/docs/reference/extensions.html#extensions-are-resolved-statically
Резюме метода маппинга:
+
Простое Unit-тестирование
-
Затруднена модификация
-
Требуются открытые поля у классов с данными
Метод №4: Классы-мапперы с интерфейсом
У примеров с функциями есть недостаток. Они позволяют в качестве маппера использовать любую функцию с сигнатурой (SalarySrc) -> SalaryDst
. Сделать код более очевидным поможет наличие интерфейса Mapper<SRC, DST>
.
Пример:
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
)
}
В данном примере SalaryMapper
– зависимость PersonMapper
. Это позволяет удобно подменять маппер Salary
для unit-тестов.
Относительно маппинга в функции у этого примера только один недостаток – необходимость писать немного больше кода.
Резюме метода маппинга:
+
Лучше типизация
-
Больше кода
Как и функции-мапперы:
+
Простое Unit-тестирование
-
Затруднена модификация
-
требует открытые поля у классов с данными
Метод 5: Рефлексия
Метод черной магии. Рассмотрим этот метод на других моделях.
Модели:
data class EmployeeSrc(
val firstName: String,
val lastName: String,
val age: Int
// много других полей
)
data class EmployeeDst(
val name: String, // одно поле, а не два
val age: Int
// много других полей
)
Маппер:
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) // остальные поля без изменений
}
})
}
Пример подсмотрен тут.
В данном примере EmployeeSrc
и EmployeeDst
хранят имя в разных форматах. Мапперу нужно только составить имя для новой модели. Остальные поля обработаются автоматически, без написания кода (вариант else
в when
).
Метод может быть полезен, например, если у вас большие модели с кучей полей и поля в основном совпадают у одних и тех же моделей из разных слоев.
Большая проблема возникнет, например, если вы добавите обязательные поля в Dst и его случайно не окажется в Src
или в маппере: cлучится IllegalArgumentException
в runtime. Также рефлексия имеет проблемы с производительностью.
Резюме метода маппинга:
+
меньше кода
+
простое Unit-тестирование
-
опасен
-
может негативно сказаться на производительности
Выводы
Такие выводы можно сделать из нашего рассмотрения:
Методы-мапперы — наглядный код, быстрее писать и поддерживать
Функции-мапперы и функции расширения – просто тестировать маппинг.
Классы мапперы с интерфейсом — просто тестировать маппинг и яснее код.
Рефлексия – подходит для нестандартных ситуаций.
Автор: Oleg Koltunov