5 причин использовать LesnJs

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В данной статье будут рассмотрены: библиотека 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/


Интересные статьи

Интересные статьи

Если вы не из тех, кто пишет «Мы — одна из лучших компаний в России и поэтому вам стоит купить наш товар», если понимаете, что маркетинг — прежде всего работа с потребнос...
Нахождение скрытого потенциала параллелизации в произвольном алгоритме, построение плана (каркаса) выполнения параллельной программы на гомогенном или гетерогенном поле п...
Недавно на Хабре была опубликована статья Главная причина, почему не Linux, которая наделала много шума в обсуждениях. Данная заметка — это небольшой философский ответ на ту статью, который, как ...
Компании переполнили рынок товаров и услуг предложениями. Разнообразие наблюдается не только в офлайне, но и в интернете. Достаточно вбить в поисковик любой запрос, чтобы получить подтверждение насыще...
Как быстро определить, что на отдельно взятый сайт забили, и им никто не занимается? Если в подвале главной страницы в копирайте стоит не текущий год, а старый, то именно в этом году опека над са...