В Java во многих проектах я использовал MapStruct. Когда начинаешь писать на новом языке (в данном случае Котлин), то возникает естественное желание перенести часть практик.

Более того, сам MapStruct вполне себе работает с Котлином. Только генерирует Java-код. Из-за этого возникают ограничения (все-таки Kotlin содержит больше конструкций, чем Java – те же именованные параметры и функции без классов). Особенно грустно, что пропадает типизация null (в Java любая переменная может быть null на уровне языка).

Возникает очевидное желание сделать такое же, но уже полностью адаптированное под Котлин. Я даже начал делать (и многова-то написал), но в итоге решил не публиковать и самому не пользоваться.

Какие проблемы MapStruct и им подобных?

  1. При развитии приложения время от времени эти магические функции конвертации выходят боком (ошибками) – лучше когда оно типизировано сломается и будет принято осознанное решение как изменить.
  2. Довольно часто нет типизации – и типы, и куски Java-кода передаются в строках
  3. Какой-то очередной DSL, который нужно дополнительно учить и вспоминать, если какое-то время не пользовался
  4. Нет экономии времени на тестах – их нужно обязательно писать, т.к. не так уж и редко они действительно помогают исправить баги
  5. Время от времени оно не работает – нужно лезть в код и разбираться почему оно не генерирует то, что нужно
  6. Увеличивается время компиляции, причем обычно всех модулей, что всегда неприятно

Все эти проблемы не связаны с Котлин и почти не связаны с конкретной реализацией. MapStruct по сути синтаксический сахар, который в случае с Котлином нужен гораздо меньше. Давай рассмотрим код.

Исходный пример с сайта MapStruct на Java:

public class Car {

  private String make;
  private int numberOfSeats;
  private CarType type;

  //constructor, getters, setters etc.
}

public class CarDto {

  private String make;
  private int seatCount;
  private String type;

  //constructor, getters, setters etc.
}

@Mapper
public interface CarMapper {

  CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );

  @Mapping(source = "numberOfSeats", target = "seatCount")
  CarDto carToCarDto(Car car);
}

// CarDto dto = CarMapper.INSTANCE.carToCarDto(car)

И то же самое, но на Котлине (причем без сокращений как было в Java и с полной типизацией, а не в строках):

data class Car(  
    val make: String,  
    val numberOfSeats: Int,  
    val type: CarType,  
)
  
data class CarDto(  
    val make: String,  
    val seatCount: Int,  
    val type: String,  
) {  
    compation object{  
        fun from(from: Car) = CarDto(  
            make = from.make,  
            seatCount = from.numberOfSeats,  
            type = from.type.toString()  
        )  
    }  
}

val dto = CarDto.from(car)

Причем функцию конвертации можно добавить и в исходный класс:

data class Car(  
    val make: String,  
    val numberOfSeats: Int,  
    val type: CarType,  
) {  
    fun toDto() = CarDto(  
        make = make,  
        seatCount = numberOfSeats,  
        type = type.toString()  
    )  
}
  
data class CarDto(  
    val make: String,  
    val seatCount: Int,  
    val type: String,  
)

val dto = car.toDto()

Просто как функцию:

fun car2dto(from: Car) = CarDto(  
    make = from.make,  
    seatCount = from.numberOfSeats,  
    type = from.type.toString()  
)

val dto = car2dto(car)

Или как функцию-расширение:

fun Car.toDto() = CarDto(  
    make = make,  
    seatCount = numberOfSeats,  
    type = type.toString()  
)

val dto = car.toDto()

Так что можно выбирать подход в зависимости от того как организовано приложение.

Если еще что-то не напишу, то обязательно буду держать в курсе).