Программирование
Маппинг данных в Kotlin
В каждом проекте наступает момент, когда вам нужно отобразить данные из одного класса в другой.
В каждом проекте наступает момент, когда вам нужно отобразить данные из одного класса в другой, сделать маппинг данных. Особенно при работе в чистой архитектуре с отдельными моделями для слоев app
и data
. Давайте рассмотрим несколько способов отображения моделей в Kotlin и их отличия.
Чтобы упростить ситуацию, я буду использовать примеры: у вас есть модель слоя данных UserEntity
и модель доменного слоя User
, и вы хотите отобразить одну модель в другую:
// Data model data class UserEntity( val name: String, val surname: String, ) // Domain model data class User( val name: String, val surname: String, )
1. Функция расширения
Это просто создание функции расширения с именами toModel()
, toEntity()
и т.д. Функции расширения обычно хранятся в файле по контексту (например, файл пакета), это выглядит следующим образом:
// Extension function mapper fun UserEntity.toModel() = User( name = name, surname = surname, ) fun User.toEntity() = UserEntity( name = name, surname = surname, ) // Usage fun main() { val userEntity = UserEntity(name = "Name", surname = "Surname") val user = userEntity.toModel() // We get our model user.toEntity() // We can reverse it to get entity }
Расширения довольно круты и интуитивно понятны, но их самый существенный недостаток в том, что они не являются общими, то есть мы не можем писать общие операции отображения (вы увидите, что я имею в виду дальше в этой статье).
Однако у них есть и одно из самых значительных преимуществ: их легко внедрять в существующие кодовые базы, потому что они не навязчивы. Затраты на их добавление минимальны.
2. Конструктор
Еще один способ отображения в Kotlin — использование дополнительных конструкторов:
// Constructor mapper data class UserEntity( val name: String, val surname: String, ) { constructor(model: User) : this(name = model.name, surname = model.surname) // We can't create mapper for database to model mapper because // User is in domain layer and doesn't know UserEntity exists // we need this workaround fun toModel() = User(name = name, surname = surname) }
Их главный недостаток в том, что они находятся внутри класса, а это значит, что если вам нужно несколько мапперов для этого класса, то ваш класс сильно вырастет.
Кроме того, если вы захотите отобразить класс из внешнего фреймворка, вы не сможете этого сделать! А это не такая уж и редкость.
Я бы не рекомендовал использовать их в своих проектах, так как они навязчивы и ограничены.
3. Интерфейс маппера
Идея проста: создайте интерфейс Mapper
, который будет реализован каждым отображаемым классом.
// Interface mapper // since this interface will only have 1 abstract function // we can use fun interface fun interface Mapper<in From, out To> { fun map(from: From): To } // Those can be objects or classes depending on your needs object UserEntityToModelMapper : Mapper<UserEntity, User> { override fun map(from: UserEntity) = User( name = from.name, surname = from.surname, ) } object UserToEntityMapper : Mapper<User, UserEntity> { override fun map(from: User) = UserEntity( name = from.name, surname = from.surname, ) } // Usage fun main() { val userEntity = UserEntity(name = "Name", surname = "Surname") val user = UserEntityToModelMapper.map(userEntity) // We get our model UserToEntityMapper.map(user) // We can reverse it to get entity }
На первый взгляд, этот подход не имеет реальных преимуществ перед расширениями, и нам придется писать больше кода.
Однако мы можем написать общие алгоритмы отображения для таких вещей, как коллекции:
// First map from List is called // then map from Mapper is called fun <F, T> Mapper<F, T>.mapAll(list: List<F>) = list.map { map(it) } // You could add more collection mapping operations if needed // Usage fun main() { val userEntities = listOf( UserEntity(name = "Name1", surname = "Surname1"), UserEntity(name = "Name2", surname = "Surname2"), ) val users = UserEntityToModelMapper.mapAll(userEntities) }
Кроме того, при использовании инъекции зависимостей вы можете подстроить эти мапперы под свои нужды. Первоначальные затраты на них выше, но в долгосрочной перспективе они окупятся.
Я рекомендую использовать интерфейсный подход при запуске нового проекта, так как он окупается лучше всего из этих трех подходов.
Рассмотрите возможность использования функций расширения при работе с существующей кодовой базой, если у вас мало времени для рефакторинга. Если вы захотите использовать интерфейсный подход в будущем, то переход от него также не составит труда.
Спасибо за прочтение.