Моки нужны чтобы при тестировании PersonSrc.mapToDestination не создавать настоящий объект SalarySrc и не вызывать SalarySrc. mapToDestination. И то и другое сделают этот тест избыточным.
Тестирование – это отдельный момент, на который хотелось бы обратить внимание.
Для начала стоит определиться с тем, что такое PersonSrc и SalarySrc: это DTO или это умные доменные объекты с бизнес-логикой.
• Если это модели в которых есть бизнес логика, то у нас уже речь про явно сильно связанную систему и тут mock-объекты уместны.
С моей точки зрения, системы с «умными» моделями – очень тяжело разрабатывать и поддерживать. В этом случае я задал бы лишь один вопрос: «почему был выбран такой путь?» :)
• Если же, эти классы – это просто DTO, то наши проблемы с тестированием из-за того, что мы в DTO засунули ответственность не только за передачу данных, но и за маппинг (нарушение SRP).
В случае, когда мы тестируем маппинг, мы должны тестировать его полностью. Абсолютно нету смысла разбивать его на части, у нас нету (и не может быть) вариативности в ожидаемом результате.
Единственное что должны проверять тесты для мапперов:
— в правильные ли поля были положены данные
— что будет в случае если у нас null
Юнит тесты могут тестировать связку больше чем из одного класса и в конкретно этом случае, это будет довольно правильный подход.
Использование моков – довольно громоздкий подход для тестирования мапперов, хотя его тоже можно использовать.
Но в случае с моками обязательно следует проверять вызывался ли конкретный метод и содержит ли результирующая модель тот объект, который вернул mock.
Наличие публичных пропертей, торчащих из модели наружу – никоим образом не сказывается на связности между модулями.
С открытыми пропертями другая «проблема» – мы делаем «глупые» объекты передачи данных (DTO).
– Это плата за слабую связанность кода и возможность его переиспользовать.
Яркий пример: если для того, чтобы перетянуть визуальный компонент в другой проект необходимо каким-либо образом изменять код визуального компонента – значит у компонента высокая связность с кодом проекта, аналогично и с API слоем, и с бизнес слоем.
В идеале перенос модуля должен быть простым копированием пакета со всеми классами конкретного компонента в другой проект, без необходимости удалять/добавлять что-то в скопированном коде.
В примере с Вариант А при переносе компонента в другой проект придется удалить все методы отвечающие за маппинг в другую модель, т.е. изменить код, который был скопирован. Это не сложно, но это и есть факт наличия связности между модулями.
ps: если мы используем DTO, в них нету смысла скрывать поля. Если же мы используем более умные доменные модели с кусками бизнес логики – это уже совсем другой разговор, но такой подход используется не часто.
В варианте А EntitySrc зависит от EntityDst: итого 1 зависимость.
В варианте Б mapEntity зависит от EntitySrc и от EntityDst: итого 2 зависимости.
Т.е. слои, конечно, в варианте Б лучше выражены, но весь код в итоге более связан.
Мапперы – это часть кода проекта, единственная ответственность которой – быть связующим звеном между компонентами системы.
При этом, компоненты остаются независимыми друг от друга и от проекта.
Меньше связей – меньше работы для модификации.
Увы, здесь обратная зависимость. Для достижения меньшей связности по коду, нужно делать больше работы.
Если нужно добавить 1 пропертю, но при этом сохранить компоненты независимыми, нужно добавить эту пропертю в каждый из компонентов и в связующий их код (сделать 3 правки).
Но стоит отметить:
1. Связность имеет более широкий спектр градации: en.wikipedia.org/wiki/Coupling_(computer_programming)
2. Далеко не всем проектам необходимо достигать низкий уровень связности – иногда это бывает просто бессмысленной тратой ресурсов (для поддержания слабой связности, нужно тратить больше времени/денег)
Более того, react-redux подход практически так и работает.
// глобальный Store (State)
const store = createStore(todoApp)
render(
<Provider store={store}>
<App /> <-- все дочерние компоненты, которые зависят от Store
</Provider>,
document.getElementById('root')
)
Есть некий глобальный Store (со своим глобальным State), а есть локальные State внутри каждого конкретного компонента. Глобальный State инкапсулирован в компоненте Provider, а все остальные компоненты добавляются как дочерние по отношению к Provider.
Компонент Provider подписывается на изменение внутри Store/State и отвечает за обновление всех подписанных (через high order component react-redux: connect()) компонентов.
Селектор – это маппер данных из глобального State в локальный State конкретного компонента (добавляется обычно через ту же функцию connect()).
При необходимости изменить глобальный State, кидается событие (event) с конкретным типом (action: redux.js.org/basics/actions). Это событие обрабатывается, изменяет глобальный State и приводят к оповещению всех подписанных на изменения и вызову селекторов (см. выше).
правильно я понимаю что state — это [условно] глобальная переменная из которой достаются собственно эти юзеры итп?
В оригинальной концепции используется понятие Store, у которого уже есть текущий State.
Этот Store создается в точности как Вы описали: let store = createStore(counter)
и на него вешается подписчик изменений: store.subscribe(() => console.log(store.getState()))
в первом же примере это показано: redux.js.org/introduction/getting-started
Но так как State внутри Store – неизменяемый, то при необходимости изменить какое-то значение внутри State создается новый объект State, соответственно подписчик (listener) вешается на изменение всего State.
А селекторы уже приводят текущий State к необходимой модели конкретного компонента.
В статье описан и осуждается оверинжениринг при описании мапперов(селекторов) для приведения одной модели к другой.
Метод №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: каким образом моки помогут в тестировании маппинга моделей?
Это уже называется serialization или маршалинг. Мы не можем его использовать, чтобы мапить модели слоя представления на модели бизнес логики или API модели. В подавляющем большинстве случаев эти модели имеют разную структуру.
Маршалинг же в чистом виде требует чтобы модель источника и приемника была одинаковой по структуре.
Если конструктор целевого объекта знает про модель источника данных – мы получаем сильную связанность. Более того, чтобы из конструктора целевого объекта получить данные, в модели обязательно должны быть публичные свойства.
Обычно маппинг происходит между моделями разных слоев. А значит они не имеют доступа к приватным свойствам друг друга. Да и по-хорошему вообще ничего не должны знать друг о друге, иначе получится так, что какая-то вьюха (фрагмент или активити) будет зависеть от моделей API слоя, или наоборот, модели из API слоя будут зависеть от вьюх, и то и другое – очень плохо.
Правильно ли я понимаю, что приведенный пример — это применение некой функции f к каждому элементу списка и возврат нового списка в результате?
Если я правильно понял, в той же многословной java это будет:
class Fmap {
static <A, B extends A> Collection<B> fmap(Function<A, B> f, Collection<A> a) {
return a.stream().map(f).collect(toList());
}
}
и из церемоний тут:
1. описание явной сигнатуры метода (да, громоздко)
2. обернуть все в класс и объявить как static (т.к. функций первого порядка нету, но есть статик импорты)
Без сомнения, у хаскеля система типов мощнее и кода пишется меньше. Но сложность этого кода для понимания на порядок выше. Учитывая, что программист тратит 60-80% времени на чтение кода, в случае со столь сложной системой типов необходимо приложить колоссальные усилия, чтобы разобрать пусть небольшой но невероятно сложный код. С точки зрения поддержки и командной работы над проектом – вся команда должна будет состоять из очень одаренных людей. Увы, таких людей очень мало.
Вот честно, я, как java-разработчик, не совсем понимаю, зачем в современном мире начинать новый проект на базе JavaEE серверов? Даже тот же Spring Boot и то лучше будет.
Думаю, reforms просто привел не самый лучший пример. Да, действительно хранить свойства отображения в enum – не лучшее решение, придет требование вводить Theme и будет много боли. Все же emun — это про логику/состояние, а не про отображение/визуализацию.
Более хороший пример: TimeUnit из java.
В этом примере можно увидеть классический enum + некоторые связанные с ним методы.
В случае с классическим enum, нужно было бы описывать сам enum и плодить пачку независимых функций для конвертации, и тут уже все будет зависеть от парадигмы программирования: OOP vs FP.
Нет, в таком случае обычно вводится ещё один промежуточный класс в иерархию
Что в свою очередь делает всю систему еще более запутанной. Видел я систему компонентов из 7+ уровней наследования, казалось бы все красиво и круто, но разбираться в этом – то еще удовольствие :(
Если существовала бы какая-то «серебряная пуля», которая помогала бы реализовать сложное поведение с помощью простой и очевидной модели, мы бы все ей пользовались :)
Из своего опыта могу сказать, что для меня «серебряная пуля» – это модульный подход (использовать interface и лишь один уровень иерархии).
Да, придется дублировать код в разных модулях, но в результате мы получаем Очевидную реализацию и поведение, легкость в тестировании и следование принципам LSP и SRP, что в свою очередь дает нам взаимозаменяемость модулей.
На первый взгляд – да, крутая идея и очень удобно!
Но отсутствие контроля за сборкой – это больше проблема, чем удобство. Неизвестно когда, что и как будет собираться. И как эту сборку отключить? (к примеру в целях отладки)
ну и есть еще другие интересные вопросы:
Сколько популярных вещей есть в js мире?
Как много кода для поддержки их всех нужно добавить в сборщик?
Как они все будут между собою взаимодействовать?
Как долго будет тянуться поддержка «легаси» версий, чтобы никому ничего не сломать?
…
С моей точки зрения, стало сложнее.
1. Вы стали описывать типы пропертей компонента в нескольких местах (копипаст)
2. Теперь у вас есть метод render() внутри самого React-компонента и есть еще какой-то коллбек render(args) для connectRxProps
3. compose со последующим запутанным куском кода – лишь все усложняет, и это для тривиального примера, что будет, если пример будет более сложным?
4. Теперь у вас в коде компонента есть 2 способа изменить состояние этого компонента: this.setState и render( state )
ps: в последнем примере вы потеряли пропертю loading
Моей целью ни в коем случае не было оскорбить или зацепить ваши чувства или сказать, что я в чем-то лучше Вас. Отнюдь, я лишь сказать хотел, что не стоит использовать элементы грязного маркетинга и писать технический буллщит.
В самом начале топика написано, что Вы автор этой статьи. Никаких ссылок на оригинал и что авторство не ваше, нету.
Но даже если это не ваш текст или вы частично позаимствовали его – стоит выкинуть оттуда всю «грязь».
Под грязными приемами я имею ввиду такой:
Классическая (как в современной Java) модель синхронной параллельной обработки ориентируется на использование потоков операционной системы… Допустим, нам необходимо обслужить 10000 клиентских соединений — нам придется породить 10000 потоков операционной системы, технически это возможно.
В этом фрагменте есть ключевые поинты:
— отсылка на «старые подходы» (классические)
— на конкретную современную платформу, в которой эти старые подходы до сих пор используются
— и самое вкусное в конце: «10k соединений = 10k потоков», – это же ужасно!!!
Любой неокрепший ум воспринимает такой текст как «Вот эту платформу ни в коем случае нельзя использовать – она нерационально использует ресурсы!».
Этот фрагмент написан в таком виде умышленно, почему и зачем – это хорошие вопросы, автору виднее. Но один из вариантов – для поднятия хайпа и обилия комментов, что выведет статью в топы комментам и просмотрам.
Вы сделали акцент (выделили отдельным блоком и подкрасили цветом), что Java – это всегда блокирующее IO. Вы то же самое сделали в отношении Perl.
Да, я понимаю, что статья была о другом, но за этим акцентом теряется весь глубокий смысл статьи, т.к. если автор технически не подкован и несет бред, то нету смысла читать дальше. Уж извините.
ps: я так же понимаю, что этой строкой Вы хотели подогреть статью и подцепить на крючек неокрепшие умы, которые где-то прочитали/услышали, что Java – это плохо. Но Вы же инженер! (я надеюсь). Не нужно так =)
Тестирование – это отдельный момент, на который хотелось бы обратить внимание.
Для начала стоит определиться с тем, что такое PersonSrc и SalarySrc: это DTO или это умные доменные объекты с бизнес-логикой.
• Если это модели в которых есть бизнес логика, то у нас уже речь про явно сильно связанную систему и тут mock-объекты уместны.
С моей точки зрения, системы с «умными» моделями – очень тяжело разрабатывать и поддерживать. В этом случае я задал бы лишь один вопрос: «почему был выбран такой путь?» :)
• Если же, эти классы – это просто DTO, то наши проблемы с тестированием из-за того, что мы в DTO засунули ответственность не только за передачу данных, но и за маппинг (нарушение SRP).
В случае, когда мы тестируем маппинг, мы должны тестировать его полностью. Абсолютно нету смысла разбивать его на части, у нас нету (и не может быть) вариативности в ожидаемом результате.
Единственное что должны проверять тесты для мапперов:
— в правильные ли поля были положены данные
— что будет в случае если у нас null
Юнит тесты могут тестировать связку больше чем из одного класса и в конкретно этом случае, это будет довольно правильный подход.
Использование моков – довольно громоздкий подход для тестирования мапперов, хотя его тоже можно использовать.
Но в случае с моками обязательно следует проверять вызывался ли конкретный метод и содержит ли результирующая модель тот объект, который вернул mock.
Связность кода определяется через зависимости: чем больше один модуль зависит от классов другого – тем больше связность, и на оборот.
upload.wikimedia.org/wikipedia/commons/9/9c/Coupling_sketches_cropped_1.svg
Наличие публичных пропертей, торчащих из модели наружу – никоим образом не сказывается на связности между модулями.
С открытыми пропертями другая «проблема» – мы делаем «глупые» объекты передачи данных (DTO).
– Это плата за слабую связанность кода и возможность его переиспользовать.
Яркий пример: если для того, чтобы перетянуть визуальный компонент в другой проект необходимо каким-либо образом изменять код визуального компонента – значит у компонента высокая связность с кодом проекта, аналогично и с API слоем, и с бизнес слоем.
В идеале перенос модуля должен быть простым копированием пакета со всеми классами конкретного компонента в другой проект, без необходимости удалять/добавлять что-то в скопированном коде.
В примере с Вариант А при переносе компонента в другой проект придется удалить все методы отвечающие за маппинг в другую модель, т.е. изменить код, который был скопирован. Это не сложно, но это и есть факт наличия связности между модулями.
ps: если мы используем DTO, в них нету смысла скрывать поля. Если же мы используем более умные доменные модели с кусками бизнес логики – это уже совсем другой разговор, но такой подход используется не часто.
Мапперы – это часть кода проекта, единственная ответственность которой – быть связующим звеном между компонентами системы.
При этом, компоненты остаются независимыми друг от друга и от проекта.
Увы, здесь обратная зависимость. Для достижения меньшей связности по коду, нужно делать больше работы.
Если нужно добавить 1 пропертю, но при этом сохранить компоненты независимыми, нужно добавить эту пропертю в каждый из компонентов и в связующий их код (сделать 3 правки).
Но стоит отметить:
1. Связность имеет более широкий спектр градации: en.wikipedia.org/wiki/Coupling_(computer_programming)
2. Далеко не всем проектам необходимо достигать низкий уровень связности – иногда это бывает просто бессмысленной тратой ресурсов (для поддержания слабой связности, нужно тратить больше времени/денег)
источник: redux.js.org/basics/usage-with-react
Есть некий глобальный Store (со своим глобальным State), а есть локальные State внутри каждого конкретного компонента. Глобальный State инкапсулирован в компоненте Provider, а все остальные компоненты добавляются как дочерние по отношению к Provider.
Компонент Provider подписывается на изменение внутри Store/State и отвечает за обновление всех подписанных (через high order component react-redux: connect()) компонентов.
Селектор – это маппер данных из глобального State в локальный State конкретного компонента (добавляется обычно через ту же функцию connect()).
При необходимости изменить глобальный State, кидается событие (event) с конкретным типом (action: redux.js.org/basics/actions). Это событие обрабатывается, изменяет глобальный State и приводят к оповещению всех подписанных на изменения и вызову селекторов (см. выше).
В оригинальной концепции используется понятие Store, у которого уже есть текущий State.
Этот Store создается в точности как Вы описали: let store = createStore(counter)
и на него вешается подписчик изменений: store.subscribe(() => console.log(store.getState()))
в первом же примере это показано: redux.js.org/introduction/getting-started
Но так как State внутри Store – неизменяемый, то при необходимости изменить какое-то значение внутри State создается новый объект State, соответственно подписчик (listener) вешается на изменение всего State.
А селекторы уже приводят текущий State к необходимой модели конкретного компонента.
В статье описан и осуждается оверинжениринг при описании мапперов(селекторов) для приведения одной модели к другой.
Так, у Вас в коде в дата-классе SourceUser все свойства публичные, как и было описано для этого подхода.
И да, это, пожалуй, самый красивый подход для маппинга.
В этом подходе связанность между моделями максимально высокая. Вы в примере показали, что класс PersonSrc знает про класс PersonDst и наоборот. На более сложной структуре данных эта связанность будет кошмарной.
Тут все же Высокая связность кода, и как раз из-за этой высокой связности вытекает сложность в тестировании.
ps: каким образом моки помогут в тестировании маппинга моделей?
Маршалинг же в чистом виде требует чтобы модель источника и приемника была одинаковой по структуре.
Обычно маппинг происходит между моделями разных слоев. А значит они не имеют доступа к приватным свойствам друг друга. Да и по-хорошему вообще ничего не должны знать друг о друге, иначе получится так, что какая-то вьюха (фрагмент или активити) будет зависеть от моделей API слоя, или наоборот, модели из API слоя будут зависеть от вьюх, и то и другое – очень плохо.
Если я правильно понял, в той же многословной java это будет:
и из церемоний тут:
1. описание явной сигнатуры метода (да, громоздко)
2. обернуть все в класс и объявить как static (т.к. функций первого порядка нету, но есть статик импорты)
Без сомнения, у хаскеля система типов мощнее и кода пишется меньше. Но сложность этого кода для понимания на порядок выше. Учитывая, что программист тратит 60-80% времени на чтение кода, в случае со столь сложной системой типов необходимо приложить колоссальные усилия, чтобы разобрать пусть небольшой но невероятно сложный код. С точки зрения поддержки и командной работы над проектом – вся команда должна будет состоять из очень одаренных людей. Увы, таких людей очень мало.
Кстати, та же декларация на TypeScript:
И на JS:
Более хороший пример: TimeUnit из java.
В этом примере можно увидеть классический enum + некоторые связанные с ним методы.
К примеру, это можно использовать для конфигов:
В случае с классическим enum, нужно было бы описывать сам enum и плодить пачку независимых функций для конвертации, и тут уже все будет зависеть от парадигмы программирования: OOP vs FP.
google.github.io/guava/releases/21.0/api/docs/com/google/common/base/Preconditions.html
Как минимум код делегирования нужно будет дублировать. Лично я не считаю это проблемой, но в контексте этой статьи – это «проблема».
Из своего опыта могу сказать, что для меня «серебряная пуля» – это модульный подход (использовать interface и лишь один уровень иерархии).
Да, придется дублировать код в разных модулях, но в результате мы получаем Очевидную реализацию и поведение, легкость в тестировании и следование принципам LSP и SRP, что в свою очередь дает нам взаимозаменяемость модулей.
Но отсутствие контроля за сборкой – это больше проблема, чем удобство. Неизвестно когда, что и как будет собираться. И как эту сборку отключить? (к примеру в целях отладки)
ну и есть еще другие интересные вопросы:
Сколько популярных вещей есть в js мире?
Как много кода для поддержки их всех нужно добавить в сборщик?
Как они все будут между собою взаимодействовать?
Как долго будет тянуться поддержка «легаси» версий, чтобы никому ничего не сломать?
…
1. Вы стали описывать типы пропертей компонента в нескольких местах (копипаст)
2. Теперь у вас есть метод render() внутри самого React-компонента и есть еще какой-то коллбек render(args) для connectRxProps
3. compose со последующим запутанным куском кода – лишь все усложняет, и это для тривиального примера, что будет, если пример будет более сложным?
4. Теперь у вас в коде компонента есть 2 способа изменить состояние этого компонента: this.setState и render( state )
ps: в последнем примере вы потеряли пропертю loading
Более корректным будет вопрос: «Как использовать JS, чтобы не сильно ухудшить производительность веб-страницы?»
В самом начале топика написано, что Вы автор этой статьи. Никаких ссылок на оригинал и что авторство не ваше, нету.
Но даже если это не ваш текст или вы частично позаимствовали его – стоит выкинуть оттуда всю «грязь».
Под грязными приемами я имею ввиду такой:
В этом фрагменте есть ключевые поинты:
— отсылка на «старые подходы» (классические)
— на конкретную современную платформу, в которой эти старые подходы до сих пор используются
— и самое вкусное в конце: «10k соединений = 10k потоков», – это же ужасно!!!
Любой неокрепший ум воспринимает такой текст как «Вот эту платформу ни в коем случае нельзя использовать – она нерационально использует ресурсы!».
Этот фрагмент написан в таком виде умышленно, почему и зачем – это хорошие вопросы, автору виднее. Но один из вариантов – для поднятия хайпа и обилия комментов, что выведет статью в топы комментам и просмотрам.
Да, я понимаю, что статья была о другом, но за этим акцентом теряется весь глубокий смысл статьи, т.к. если автор технически не подкован и несет бред, то нету смысла читать дальше. Уж извините.
ps: я так же понимаю, что этой строкой Вы хотели подогреть статью и подцепить на крючек неокрепшие умы, которые где-то прочитали/услышали, что Java – это плохо. Но Вы же инженер! (я надеюсь). Не нужно так =)