В данной статье будут рассмотрены: библиотека lens-js и её обёртки lens-ts и react-lens. Мы попробуем сравнить их с популярными библиотеками (Redux и MobX/MST), а также объясним преимущества и недостатки.
Стоит отметить, что «Линзы» — это только концепция. Мы же будем рассматривать конкретную реализацию. Поэтому, нужно понимать, что другие библиотеки, реализующие «Линзы» могут оказаться лучше или чуть-чуть похуже.
Что такое линзы
«Линзы» — это концепция функционного программирования, позволяющая решать задачи связанные с организацией данных. Внешне, это большой ориентированный граф, где каждый из узлов является интерфейсом (или контроллером) своего сегмента данных, а сам граф — это правило (или агрегатор), описывающее полное состояние (например, вашего приложения).
«Линзы» реализуются при помощи «геттеров» и «сеттеров», которые хранят процедуры того, каким образом нужно присоединить отдельный узел ко графу.
Более подробнее о линзах и библиотеке lens-js можно узнать в Wiki
Сравнение с Redux
Redux прекрасная и функциональная библиотека с низким порогом вхождения, имеет большое комьюнити и пользуется огромной популярностью. Что же, на этом всё, статья закончена, выводы… до свидания…
Ну нет, мы собрались не для того, чтобы просто «сложить лапки». Давайте сначала обратим внимание и на слабые стороны Redux, а именно:
-
Гиперизбыточность.
-
Сложность адаптации новых сотрудников на проекте.
-
Плохая переносимость форм (неуниверсальность)
-
Производительность
Архитектурно, Redux реализует работу с состоянием в отдельных агрегаторах (reducer), управляемыми особыми действиями — action. Этот код расположен отдельно и обрабатывается независимо от места и времени вызова. Всё это формирует большой аспект применимый ко всему состоянию приложения. Не АОП, но недостатки, свойственные такому подходу имеются (мы же тут, чтобы немного покритиковать?).
Во-первых, аспект не может обладать полной информацией о контексте изменений, в связи с чем, для малоразличающихся действий приходится создавать отдельные обработчики. Иногда, различающиеся только одной строчкой.
Часто приходилось видеть, что для асинхронной работы страницы использовались аж целых 5 действий: INIT, LOADING, LOADED, SECCSES, ERROR. Для всего этого создавались «редьюсеры».
Во-вторых, аспект формирует отдельную подархитектуру, со своими особенностями, знания о которой нужно хранить и передавать. Другими словами, умения (даже хорошо) работать с Redux крайне недостаточно.
В-третьих, чем «больше» аспект, тем сильнее он способствует сильной зацеплённости и низкой связности сущностей. Это, к примеру, может повредить общей архитектуре приложения (см. GRASP).
А как с этим справляются «линзы»?
Давайте вспомним, что по сути, «линзы» — это граф, который является правилом агрегации состояния. Если в Redux мы эти правила описывали в «редьюсерах», то «линза» создаёт их автоматически. Работает это по двум принципам:
-
Статистический — с высокой вероятностью, мы не будем хаотично менять типы данных и/или способы их обработки, а напротив — постараемся их придерживаться.
-
Структурный — если сущность B является подструктурой A, а сущность D подструктурой C, то с высокой вероятностью они сохранят свои отношения, т. е. маловероятно, что B станет подструктурой C, при условии, что B и D не взаимозаменяемы.
Пусь только вероятностно, но это уже позволяет накопить достаточно информации для автоматической сборки состояния. Рассмотрим пример на линзах:
/* Мы хотим получить цвет бантика у котика */ const color = cat.go('ribbon').go('color');
Сложно представить иную структуру для этого. Однако для того чтобы записать новый цвет, мы должны собрать объект обратно. Хорошая новость в том, что получая дочерние узлы, мы сохраняли информацию о структуре. «Линза», как бы, уже подготовила для нас агрегатор для узла color.
/* Теперь поменяем цвет */ color.set('#FF0000');
Готовый объект будет иметь следующий вид:
{ ribbon: { color: '#FF0000' } }
Изначально, мы специально упустили способ получения узла cat. Это только имя переменной, связанной с неким узлом. Мы могли бы взять его из массива или другого поля, которое имело бы семантически-подобную структуру, например:
{ murzik: { ribbon: { color: '#0000FF' } }, pushok: { ribbon: { color: '#FF0000' } } }
Тогда код стал бы более универсальным:
function setColor(cat, color) { cat.go('ribbon').go('color').set(color); } setColor(cats.go('murzik'), '#FF0000'); setColor(cats.go('pushok'), '#0000FF');
А вот так, мы бы добавили Рыжика с ленточкой, как у Мурзика:
const murzik = cats.go('murzik'); const rizgik = cats.go('rizgik'); rizgik.go('ribbon').set( murzik.go('ribbon').get() );
Как видно из примеров, характер манипуляций очень схож и не требует специфических агрегаторов. Весь представленный код — это действительно всё, что потребуется для работы над подобными объектами.
«Линза» не привязывает к автоматизированному способу склейки данных, а только использует его по умолчанию. Вы можете использовать различные «мапперы». Подробнее можно узнать тут.
Целая «Линза», хоть и является графом, но благодаря поддержке принципа «каждый узел главный», не нужно учитывать структуру до узла. Давайте сравним:
/* Код на redux */ const Cat = ({ cat }) => <div>{cat.ribbon.color}</div>; const mapStateToProps = (state, ownProps) => ({cat: state.cats[ownProps.name]}); export default connect(Cat, mapStateToProps); /* Код на линзах */ export const Cat = ({ lens }) => { const [ cat ] = useLens(lens); return <div>{cat.ribbon.color}</div>; }
Из кода видно, что Redux, хоть и позволил нам выбирать конкретного котика, но жёстко привязал компонент к одному списку. Для попытки взять котика из другого списка, нам пришлось бы вводить второе измерение и т. д. Линзы же ожидают другой подход, где необходимый котик будет взят в компоненте родителя, т. е. вектор данных определяется адаптивно, а не хардкодом, а это делает «линзовый» компонент более универсальным и пригодным к работе на других формах, без необходимости его переделывать.
В отличие от Redux, линза вызывает перерисовку только тех компонентов, которые были связанны с изменившимся узлом. Это работает даже, если узел хранит объект. С одной стороны, линза тратит ресурсы на просчёт изменений, с другой — пересчёт виртуального DOM и манипуляции с DOM браузера более тяжёлые.
Сравнение с MobX/MST
MST позволяет использовать MobX в едином дереве, органично реализуя работу над всем состоянием приложения. Каждый узел — это многофункциональный контроллер, который позволяет организовывать разноплановую работу с данными узла, делать фоновые запросы и… и так далее…
Использование MST похоже на ход «E2-E4», на большую пивную кружку для эспрессо. Если действительно есть необходимость так упиться кофем, то нет никаких претензий. В остальных случаях это кажется очень нагромождённым.
Между «Линзой» и MST можно разглядеть общие моменты. Но есть же и отличия. Первое из них — это отсутствие необходимости создавать каждый контроллер отдельно, т. е. часть процесса построения дерева «Линза» берёт на себя.
/* Код на MST */ export const Ribbon = type .model("Ribbon", { color: type.string }) .actions(self => ({ setColor(value) { self.color = value; }, })); export const Cat = type .model("Cat", { ribbon: type.reference(Ribbon) }); /* Код на линзе (для такого же функционала) */ const cat = cats.go('murzik');
Разумеется, для разных технологий сложно определить точный эквивалент в исходном тексте, вне контекста конкретного приложения, но… это весьма близко…
Весь фокус в том, что «линза» будет ориентироваться относительно структуры данных, а не передаваемого типа. Потому, нет необходимости этот тип указывать явно. Например, если у котика появится ещё и шапочка, то появится и возможность с ней работать.
/* Шапочки ещё нет, но мы хотим узнать, когда она появиться */ cat.go('cap').attach(() => console.log('Cap is here!')); /* Теперь оденем шапочку */ cat.go('cap').set({ color: 'black' }); /* Увидим в консоли */ > Cap is here!
С другой стороны, если мы определили, что у котика просто не может быть шляпки, то «Линза» не даст ничего сделать.
interface Cat { ribbon: { color: string }; } const getCats = (): Lens<Cat[]> => ... /* Как-то получили котиков */ const cat = getCats().go(0); const cap = cat.go('cap'); // Выдаст ошибку
Недостатком контроллера по умолчанию, в данном случае, будут отсутствие следующих возможностей:
-
Расширять контроллер в каждом конкретном случае. Однако и тут срабатывает принцип статистичности. Из множества методов для работы с состоянием есть много похожих, например «геттеры» и «сеттеры» могут покрыть значительную часть функциональности. Это «линза» и учитывает.
-
Обеспечение инкапсуляции. Стоит отметить, что инкапсуляция это не инструмент защиты исполняемого кода, а подход к проектированию, т.е. можно просто договориться, какие поля не трогать =)
Стоит отметить, что «линзы», всё же, имеют возможность расширятся в парадигме ООП и приближаться к функциональности к MST. Подробнее можно прочесть тут.
Резюме
…ну наконец то!
Давайте подведём итоги, добавим ещё несколько общих плюсов и составим список причин, всё же, использовать «Линзы».
-
«Линзы» весьма компактны по сравнению с Redux и MobX/MST. Проект на «линзах» не способствует разрастанию архитектуры, которую нужно ещё поддерживать и передавать коллегам.
-
«Линза» сохраняет (по крайней мере, пытается) производительность приложения.
-
«Линза» позволяет создавать универсальные компоненты, независящие от конкретных форм и, даже, технологий. Вы смело можете сделать свой фреймворк — линзы будут работать и там.
-
«Линза» умеет в ООП. Может работать с типами данных, а также масштабироваться.
-
«Линза» не тянет за собой зависимости (за исключением обёрток). Это очень небольшая и монолитная библиотека.
Постой! Ну есть же «скелеты в шкафу»?
Ох, думал не спросите…
У «линзы» есть два существенных недостатка, на данный момент.
-
Отсутствие большого числа надстроек, промежуточного ПО и смежных библиотек. Однако выходом их этой ситуации может стать комбинирование подходов к организации состояния. Например, в качестве состояния форм можно использовать Redux, а компоненты сделать на «Линзах». Это позволит использовать промежуточное ПО (Saga, например), избавит от избыточности и сделает код более универсальным и переносимым. Мега-комбо!
-
Ограничение при серверном «рендеринге». «Линза» потребует специальной оснастки, которую придётся писать самостоятельно. Стоит отметить, что речь идёт только о библиотеке lens-js, а не о всех «линзах» в целом.
Удачи в экспериментах, друзья!
ссылка на оригинал статьи https://habr.com/ru/post/565930/
Добавить комментарий