Комментарии 15
Непонятно почему для расширерий-мапперов обязательно нужны публичные свойства — можно сделать просто конструктор в целевом объекте. И тогда недостатков у расширений, вроде как, не остаётся
Обычно маппинг происходит между моделями разных слоев. А значит они не имеют доступа к приватным свойствам друг друга. Да и по-хорошему вообще ничего не должны знать друг о друге, иначе получится так, что какая-то вьюха (фрагмент или активити) будет зависеть от моделей API слоя, или наоборот, модели из API слоя будут зависеть от вьюх, и то и другое – очень плохо.
fun SourceUser.mapToTargetUser(): TargetUser {
return TargetUser(this.name, this.age, this.address)
}
data class SourceUser(
val name: String,
val age: Int,
val address: String
)
class TargetUser(
private val name: String,
age: Int,
val address: String
) {
var login = name
private set
private val birthDate = Utils.parseAge(age)
}
Здесь:
В исходном классе все свойства неизменяемы. В целевом классе наружу видны только неизменяемые извне address, login, birthDate.
PS: Пример дан исключительно для иллюстрации моих слов в случае, когда по какой-то причине не все инициализируемые свойства нужно показывать вне объекта. Лучше все же сделать все свойства видимыми, но неизменяемыми — тогда вся связь между слоями будет содержаться в маппере слоев. А здесь уже никакого криминала.
Маршалинг же в чистом виде требует чтобы модель источника и приемника была одинаковой по структуре.
Метод №1: Методы-мапперы
…
Резюме метода маппинга:
+ Быстро писать код, маппинг всегда под рукой
+ Легкая модификация
+ Низкая связность кода
— Затруднено Unit-тестирование (нужны моки)
— Не всегда позволено архитектурой
В этом подходе связанность между моделями максимально высокая. Вы в примере показали, что класс PersonSrc знает про класс PersonDst и наоборот. На более сложной структуре данных эта связанность будет кошмарной.
class PersonSrc(private val name: String, private val salary: SalarySrc) {
fun mapToDestination() = PersonDst(name, salary.mapToDestination())
}
class SalarySrc(private val amount: Int) {
fun mapToDestination() = SalaryDst(amount)
}
Тут все же Высокая связность кода, и как раз из-за этой высокой связности вытекает сложность в тестировании.
ps: каким образом моки помогут в тестировании маппинга моделей?
Вы в примере показали, что класс PersonSrc знает про класс PersonDst и наоборот. На более сложной структуре данных эта связанность будет кошмарной.
Вовсе нет. Я показал один пример, когда Src зависим от Dst, и другой пример, когда Dst зависим от Src. Это разные примеры. Конечно, когда они зависимы друг от друга – это антипример будет.
Про низкую связанность:
Во-первый, ограничение видимости полей класса ведет к уменьшению связанности кода. В примере с методами как раз можно ограничить видимость. Не очень хорошо, когда маппинг заставляет делать поля открытыми.
Во-вторых, давайте сравним такие варианты кода.
Вариант А:
// файл 1
class EntitySrc(private val id: Int) {
fun mapToDst(src: EntitySrc) = EntityDst(src.id)
}
// файл 2
class EntityDst(val id: Int)
Вариант Б:
// файл 1
class EntitySrc(val id: Int)
// файл 2
class EntityDst(val id: Int)
// файл 3
fun mapEntity(src: EntitySrc) = EntityDst(src.id)
В варианте А EntitySrc зависит от EntityDst: итого 1 зависимость.
В варианте Б mapEntity зависит от EntitySrc и от EntityDst: итого 2 зависимости.
Т.е. слои, конечно, в варианте Б лучше выражены, но весь код в итоге более связан.
Чтобы быть ближе к практике, можно рассмотреть типичную задачу: добавление одного поля и пробрасывание его через все слои. Задача возникает постоянно. В варианте А надо модифицировать только 2 файла, в варианте Б, надо модифицировать 3 файла. Меньше связей – меньше работы для модификации.
ps: каким образом моки помогут в тестировании маппинга моделей?
Если мы используем методы-мапперы и хотим протестировать только один маппер нам надо написать такой код:
internal class PersonSrcTest {
@Test
fun mapToDestination() {
val salarySrc: SalarySrc = mock(SalarySrc::class.java)
`when`(salarySrc.mapToDestination()).thenReturn(SalaryDst(0))
val personSrc = PersonSrc("Somebody", salarySrc)
val mapped = personSrc.mapToDestination()
assertEquals("Somebody", mapped.name)
}
}
Моки нужны чтобы при тестировании PersonSrc.mapToDestination не создавать настоящий объект SalarySrc и не вызывать SalarySrc. mapToDestination. И то и другое сделают этот тест избыточным.
ограничение видимости полей класса ведет к уменьшению связанности кода.
Связность кода определяется через зависимости: чем больше один модуль зависит от классов другого – тем больше связность, и на оборот.
upload.wikimedia.org/wikipedia/commons/9/9c/Coupling_sketches_cropped_1.svg
Наличие публичных пропертей, торчащих из модели наружу – никоим образом не сказывается на связности между модулями.
С открытыми пропертями другая «проблема» – мы делаем «глупые» объекты передачи данных (DTO).
– Это плата за слабую связанность кода и возможность его переиспользовать.
Яркий пример: если для того, чтобы перетянуть визуальный компонент в другой проект необходимо каким-либо образом изменять код визуального компонента – значит у компонента высокая связность с кодом проекта, аналогично и с API слоем, и с бизнес слоем.
В идеале перенос модуля должен быть простым копированием пакета со всеми классами конкретного компонента в другой проект, без необходимости удалять/добавлять что-то в скопированном коде.
В примере с Вариант А при переносе компонента в другой проект придется удалить все методы отвечающие за маппинг в другую модель, т.е. изменить код, который был скопирован. Это не сложно, но это и есть факт наличия связности между модулями.
ps: если мы используем DTO, в них нету смысла скрывать поля. Если же мы используем более умные доменные модели с кусками бизнес логики – это уже совсем другой разговор, но такой подход используется не часто.
В варианте А EntitySrc зависит от EntityDst: итого 1 зависимость.
В варианте Б mapEntity зависит от EntitySrc и от EntityDst: итого 2 зависимости.
Т.е. слои, конечно, в варианте Б лучше выражены, но весь код в итоге более связан.
Мапперы – это часть кода проекта, единственная ответственность которой – быть связующим звеном между компонентами системы.
При этом, компоненты остаются независимыми друг от друга и от проекта.
Меньше связей – меньше работы для модификации.
Увы, здесь обратная зависимость. Для достижения меньшей связности по коду, нужно делать больше работы.
Если нужно добавить 1 пропертю, но при этом сохранить компоненты независимыми, нужно добавить эту пропертю в каждый из компонентов и в связующий их код (сделать 3 правки).
Но стоит отметить:
1. Связность имеет более широкий спектр градации: en.wikipedia.org/wiki/Coupling_(computer_programming)
2. Далеко не всем проектам необходимо достигать низкий уровень связности – иногда это бывает просто бессмысленной тратой ресурсов (для поддержания слабой связности, нужно тратить больше времени/денег)
Моки нужны чтобы при тестировании PersonSrc.mapToDestination не создавать настоящий объект SalarySrc и не вызывать SalarySrc. mapToDestination. И то и другое сделают этот тест избыточным.
Тестирование – это отдельный момент, на который хотелось бы обратить внимание.
Для начала стоит определиться с тем, что такое PersonSrc и SalarySrc: это DTO или это умные доменные объекты с бизнес-логикой.
• Если это модели в которых есть бизнес логика, то у нас уже речь про явно сильно связанную систему и тут mock-объекты уместны.
С моей точки зрения, системы с «умными» моделями – очень тяжело разрабатывать и поддерживать. В этом случае я задал бы лишь один вопрос: «почему был выбран такой путь?» :)
• Если же, эти классы – это просто DTO, то наши проблемы с тестированием из-за того, что мы в DTO засунули ответственность не только за передачу данных, но и за маппинг (нарушение SRP).
В случае, когда мы тестируем маппинг, мы должны тестировать его полностью. Абсолютно нету смысла разбивать его на части, у нас нету (и не может быть) вариативности в ожидаемом результате.
Единственное что должны проверять тесты для мапперов:
— в правильные ли поля были положены данные
— что будет в случае если у нас null
Юнит тесты могут тестировать связку больше чем из одного класса и в конкретно этом случае, это будет довольно правильный подход.
Использование моков – довольно громоздкий подход для тестирования мапперов, хотя его тоже можно использовать.
Но в случае с моками обязательно следует проверять вызывался ли конкретный метод и содержит ли результирующая модель тот объект, который вернул mock.
И да — для полноценного объекта вытаскивать всё состояние наружу через геттеры это плохой подход, так делать нельзя.
Практичные способы маппинга данных в Kotlin