5 причин использовать LensJs для организации состояния приложения

от автора

В данной статье будут рассмотрены: библиотека lens-js и её обёртки lens-ts и react-lens. Мы попробуем сравнить их с популярными библиотеками (Redux и MobX/MST), а также объясним преимущества и недостатки.

Стоит отметить, что «Линзы» — это только концепция. Мы же будем рассматривать конкретную реализацию. Поэтому, нужно понимать, что другие библиотеки, реализующие «Линзы» могут оказаться лучше или чуть-чуть похуже.

Что такое линзы

«Линзы» — это концепция функционного программирования, позволяющая решать задачи связанные с организацией данных. Внешне, это большой ориентированный граф, где каждый из узлов является интерфейсом (или контроллером) своего сегмента данных, а сам граф — это правило (или агрегатор), описывающее полное состояние (например, вашего приложения).

«Линзы» реализуются при помощи «геттеров» и «сеттеров», которые хранят процедуры того, каким образом нужно присоединить отдельный узел ко графу.

Более подробнее о линзах и библиотеке lens-js можно узнать в Wiki

Сравнение с Redux

Redux прекрасная и функциональная библиотека с низким порогом вхождения, имеет большое комьюнити и пользуется огромной популярностью. Что же, на этом всё, статья закончена, выводы… до свидания…

Ну нет, мы собрались не для того, чтобы просто «сложить лапки». Давайте сначала обратим внимание и на слабые стороны Redux, а именно:

  1. Гиперизбыточность.

  2. Сложность адаптации новых сотрудников на проекте.

  3. Плохая переносимость форм (неуниверсальность)

  4. Производительность

Архитектурно, Redux реализует работу с состоянием в отдельных агрегаторах (reducer), управляемыми особыми действиями — action. Этот код расположен отдельно и обрабатывается независимо от места и времени вызова. Всё это формирует большой аспект применимый ко всему состоянию приложения. Не АОП, но недостатки, свойственные такому подходу имеются (мы же тут, чтобы немного покритиковать?).

Во-первых, аспект не может обладать полной информацией о контексте изменений, в связи с чем, для малоразличающихся действий приходится создавать отдельные обработчики. Иногда, различающиеся только одной строчкой.

Часто приходилось видеть, что для асинхронной работы страницы использовались аж целых 5 действий: INIT, LOADING, LOADED, SECCSES, ERROR. Для всего этого создавались «редьюсеры».

Во-вторых, аспект формирует отдельную подархитектуру, со своими особенностями, знания о которой нужно хранить и передавать. Другими словами, умения (даже хорошо) работать с Redux крайне недостаточно.

В-третьих, чем «больше» аспект, тем сильнее он способствует сильной зацеплённости и низкой связности сущностей. Это, к примеру, может повредить общей архитектуре приложения (см. GRASP).

А как с этим справляются «линзы»?

Давайте вспомним, что по сути, «линзы» — это граф, который является правилом агрегации состояния. Если в Redux мы эти правила описывали в «редьюсерах», то «линза» создаёт их автоматически. Работает это по двум принципам:

  1. Статистический — с высокой вероятностью, мы не будем хаотично менять типы данных и/или способы их обработки, а напротив — постараемся их придерживаться.

  2. Структурный — если сущность 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'); // Выдаст ошибку

Недостатком контроллера по умолчанию, в данном случае, будут отсутствие следующих возможностей:

  1. Расширять контроллер в каждом конкретном случае. Однако и тут срабатывает принцип статистичности. Из множества методов для работы с состоянием есть много похожих, например «геттеры» и «сеттеры» могут покрыть значительную часть функциональности. Это «линза» и учитывает.

  2. Обеспечение инкапсуляции. Стоит отметить, что инкапсуляция это не инструмент защиты исполняемого кода, а подход к проектированию, т.е. можно просто договориться, какие поля не трогать =)

Стоит отметить, что «линзы», всё же, имеют возможность расширятся в парадигме ООП и приближаться к функциональности к MST. Подробнее можно прочесть тут.

Резюме

…ну наконец то!

Давайте подведём итоги, добавим ещё несколько общих плюсов и составим список причин, всё же, использовать «Линзы».

  1. «Линзы» весьма компактны по сравнению с Redux и MobX/MST. Проект на «линзах» не способствует разрастанию архитектуры, которую нужно ещё поддерживать и передавать коллегам.

  2. «Линза» сохраняет (по крайней мере, пытается) производительность приложения.

  3. «Линза» позволяет создавать универсальные компоненты, независящие от конкретных форм и, даже, технологий. Вы смело можете сделать свой фреймворк — линзы будут работать и там.

  4. «Линза» умеет в ООП. Может работать с типами данных, а также масштабироваться.

  5. «Линза» не тянет за собой зависимости (за исключением обёрток). Это очень небольшая и монолитная библиотека.

Постой! Ну есть же «скелеты в шкафу»?

Ох, думал не спросите…

У «линзы» есть два существенных недостатка, на данный момент.

  1. Отсутствие большого числа надстроек, промежуточного ПО и смежных библиотек. Однако выходом их этой ситуации может стать комбинирование подходов к организации состояния. Например, в качестве состояния форм можно использовать Redux, а компоненты сделать на «Линзах». Это позволит использовать промежуточное ПО (Saga, например), избавит от избыточности и сделает код более универсальным и переносимым. Мега-комбо!

  2. Ограничение при серверном «рендеринге». «Линза» потребует специальной оснастки, которую придётся писать самостоятельно. Стоит отметить, что речь идёт только о библиотеке lens-js, а не о всех «линзах» в целом.

Удачи в экспериментах, друзья!

ссылка на оригинал статьи https://habr.com/ru/post/565930/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *