Для преобразования одних классов в другие удобно использовать библиотеки для автоматического преобразования вместо того, чтобы писать мапперы вручную. На мой взгляд одна из лучших библиотек для этого MapStruct
Ее преимущество- ошибки при преобразовании, например если формат не совпадает, могут показываться при компиляции проекта, а не в рантайме, для этого создаем
import org.mapstruct.MapperConfig import org.mapstruct.ReportingPolicy @MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR) interface StrictMapperConfig
и используем его в дальнейшем. Я буду использовать базовый класс BaseMapper
abstract class BaseMapper<From, To> { abstract fun map(from: From): To open fun mapList(fromList: List<From>): List<To> { return fromList.mapTo(ArrayList(fromList.size), this::map) } }
ВНИМАНИЕ, если одной из моделей, которые необходимо преобразовать, только одно поле, MapStruct выдает ошибку
error: Unmapped target property: "copy". public abstract com._.To map(@org.jetbrains.annotations.NotNull
Похожая ошибка может быть например и при наследовании классами интерфейсов, не совместимых с MapStruct, или в некоторых других случаях.
ВНИМАНИЕ, MapStruct, как понятно по использованию аннотаций, генерирует код для преобразования, и если вы будете использовать названия переменных в формате is…, маппер выдаст ошибку из за конфликта в логике. Лучшее решение- переименовать переменную, есть и другие решения на stackoverflow.
ВНИМАНИЕ, MapStruct не умеет нормально обрабатывать nullable и не nullable значения, следите чтобы в начальном и конечном классе тип переменной был одинаковый
ВНИМАНИЕ, если в интерфейсе, отмеченном аннотацией Mapper, предполагаются обычные функции по преобразованию, используйте абстрактный класс, а не интерфейс, при использовании функций интерфейса велика вероятность ошибки при компиляции
Простейший вариант преобразования, когда классы полностью совпадают
data class To( val one: String, val two: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) fun interface ModelMapper { fun map(from: From): To } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
Если имя одного или нескольких полей отличается, добавляем аннотацию Mapping, или несколько аннотаций, если это необходимо
data class To( val oneNewName: String, val twoNewName: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) fun interface ModelMapper { @Mapping(source = "one", target = "oneNewName") @Mapping(source = "two", target = "twoNewName") fun map(from: From): To } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
А вот пример преобразования, если необходимо заменить одно из полей, например они разного типа
data class To( val one: List<String>, val two: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) abstract class ModelMapper { abstract fun map(from: From): To fun mapOne(one: String): List<String> { return listOf(one) } } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
также вы можете использовать аннотацию Named
data class To( val one: List<String>, val two: String, ) data class From( val one: String, val two: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { companion object { private const val MAP_ONE = "MAP_ONE" } @Mapper(config = StrictMapperConfig::class) abstract class ModelMapper { @Mapping(target = "one", source = "one", qualifiedByName = [MAP_ONE]) abstract fun map(from: From): To @Named(MAP_ONE) fun mapOne(one: String): List<String> { return listOf(one) } } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
Чтобы переиспользовать логику, можно разделить преобразование на разные классы / интерфейсы
data class To( val one: String, val two: List<ToItem>, ) data class ToItem( val three: List<String>, val four: String, ) data class From( val one: String, val two: List<FromItem>, ) data class FromItem( val three: String, val four: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class, uses = [ItemMapper::class]) fun interface ModelMapper { fun map(from: From): To } @Mapper(config = StrictMapperConfig::class) abstract class ItemMapper { fun mapThree(three: String): List<String> { return listOf(three) } } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
можно наоборот из обычной функции вызывать абстрактную, если например нужно преобразовать вложенные листы с другими классами, содержимое которых одинаково
data class To( val one: String, val two: List<ToItem>, ) data class ToItem( val three: String, val four: String, ) data class From( val one: String, val two: List<FromItem>, ) data class FromItem( val three: String, val four: String, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers class SomeModelMapper : BaseMapper<From, To>() { @Mapper(config = StrictMapperConfig::class) abstract class ModelMapper { fun map(from: From): To { return with(from) { To( one = one, two = two.map(::mapItem), ) } } abstract fun mapItem(two: FromItem): ToItem } override fun map(from: From): To { return Mappers.getMapper(ModelMapper::class.java).map(from) } }
По аналогии, можно создавать и более сложные конструкции, используя в абстрактном классе с аннотацией Mapper абстрактные и обычные функции, соблюдая при этом имена переменных и методов. MapStruct в общем и целом довольно капризная библиотека с большим количеством нюансов, но из тех, что я пробовал использовать, она оказалась самая безопасная в плане ошибок при работе.
Приведу еще один интересный пример, когда в модели в текстовом поле находится XML, который необходимо распарсить в класс, для этого буду использовать библиотеку org.simpleframework.xml
https://javadoc.io/doc/org.simpleframework/simple-xml/latest/index.html
implementation 'org.simpleframework:simple-xml:2.7.1'
В примере, для удобства, в конечном классе сделал 2 вложенных класса- с данными из оригинального класса и данными из XML, но структура может быть любой
<data> <one>text one</one> <two>text two</two> </data>
data class To( val data: ToData?, val xml: ToXML?, ) data class ToData( val one: String?, val two: String?, ) data class ToXML( val one: String?, val two: String?, ) data class From( val one: String?, val two: String?, val xml: String?, )
import org.simpleframework.xml.Element import org.simpleframework.xml.Root @Root(name = "data") data class FromXML( @field:Element(name = "one", required = false) var one: String? = null, @field:Element(name = "two", required = false) var two: String? = null, )
import org.mapstruct.Mapper import org.mapstruct.factory.Mappers import org.koin.core.component.inject import org.koin.core.component.KoinComponent import org.simpleframework.xml.core.Persister class SomeModelMapper: BaseMapper<From, To>(), KoinComponent { private val serializer: Persister by inject() @Mapper(config = StrictMapperConfig::class) interface ModelMapper { fun mapData(from: From): ToData fun mapXML(from: FromXML): ToXML } override fun map(from: From): To { val data = Mappers.getMapper(FromXML::class.java).mapJSON(from) return try { val xml = serializer.read(FromXML::class.java, from.xml!!)!! To( data = data, xml = Mappers.getMapper(ModelMapper::class.java).mapXML(xml), ) } catch (e: Exception) { logError("parse xml exception: ${e.message}", e, TAG) To( data = data, xml = null, ) } } }