Управление состоянием Angular — не бойтесь шаблонов

Бойтесь сильного связывания!

Управление состоянием Angular — не бойтесь шаблонов

Бойтесь сильного связывания!

Оригинал

Многие люди рассуждают о шаблоне, который создается при внедрении библиотеки управления состоянием на основе redux в приложении Angular. Все, что я могу ответить на это:

Не бойся шаблона. Бойся сильного связывания!

Тем не менее, я могу понять таких людей, они в некотором смысле правы. Управление состоянием может очень быстро стать запутанным. Особенно, если вы работаете в большой команде с разным уровнем опыта.

Я думаю, что больше всего боли причиняют шаблоны. Иногда это просто отсутствие четкого разделения интересов. Отсутствие четко определенных умных и глупых компонентов. Состояние, которое не является хорошо структурированным или просто слишком большим, чтобы обрабатываться одним компонентом контейнера. Сложные подписки внутри компонентов, которые могут объединять несколько наблюдаемых потоков(observables).
Добавьте сверху некоторые побочные эффекты. Добавьте информацию о маршрутизации в состояние. И внезапно:


Redux архитектура стала странной

Код больше не поддерживаемый. Архитектура Redux обещала решить проблемы сокрытия источника состояния приложения, но теперь стало ещё сложнее следить за потоком данных внутри вашего приложения. Тебя бомбит. Ты винишь того, кто принял решение применить управление состоянием или того, кто его реализовал.

Пожалуйста, потерпи немного :)

В этом посте я покажу как вы можете «управлять» вашей библиотекой управления состоянием. Я буду использовать ngrx в моих примерах, но на самом деле не имеет значения, какую библиотеку на основе редукса вы используете. Концепция остается прежней. Даже простые старые сервисы могут быть использованы для удержания вашего состояния.

1. Используй Typescript

Это лёгкая задача для разработчиков Angular. Стоит к нему немного привыкнуть, и вы уже не сможете без него обходиться. Мы будем использовать его силу, чтобы определять наши типизированные действия и использовать их в редукторах (reducer) и эффектах. Таким образом, ошибки кодирования возникают во время компиляции по мере разработки, а не во время выполнения. Давайте прямо сейчас погрузимся в наши типы действий:

Интерфейсы действий

Это довольно прямое определение стандартного типа Action , объединяющего типы Type и Payload. Таким образом, фактически каждый объект в форме {type, payload} подтверждает этот тип.

Мы могли бы использовать это сейчас, чтобы создать функцию, которая возвращает определенное действие и затем отправляет его через ngrx store.dispatch:

функции LoadCommits() ActionCreator()

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

Давайте улучшим наш дизайн с помощью функций-фабрик. Они помогают нам немного урезать шаблон:

Фабрики

Теперь мы можем использовать createType и createActionCreator для определения наших CommitActions:

Действия

Это альтернатива подходу определения действий на основе классов, который широко применяется пользователями ngrx. Теперь мы готовы отправить действие из компонента контейнера:

Отправка действия через ngrx хранилище

Следующим шагом является настройка редуктора. Самое интересное в редукторах и их эффектах заключается в том, что они поддерживают проверку типов также во время компиляции.

Создание редуктора

Функция isAction не только проверяет соответствие одного из ожидаемых действий, но и позволяет нам использовать типизированную полезную нагрузку внутри операторов is благодаря Typescript Type Guards.

Проверка типов действий

Тип TypedActionCreator — это функция, которая также сохраняет тип действия как свойство. Это выглядит немного странно, но позволяет нам выполнять проверку isAction без создания дополнительного шаблона.

Конструктор действия

Полный исходный код на github.

2. Подумайте о внедрении зависимостей (Dependency Injection)

Внедрение зависимостей является одной из основных особенностей Angular. Это очень упрощает многое для нас, разработчиков, а именно тестирование и вложенные зависимости. Но за всё надо платить.

На первый взгляд, внедрение чего угодно куда угодно кажется великим благом. Однако, я тут вижу проблему, которая заключается в том, что разработчики перестают думать о зависимостях. Вот что не даёт мне покоя:

Действительно ли я хочу иметь прямую зависимость от сторонней библиотеки в моем компоненте?

Это подводит нас к сильному связыванию. Связывание — это плохо.

Поэтому мы не хотим иметь прямую зависимость в библиотеке управления состоянием наших компонентов-контейнеров.

Нет:

— встряхните головой —

Сильное связывание

К сожалению, так делается в каждом онлайн примере ngrx . Если вам сказали, что контейнерные компоненты являются умными, то это не обязательно означает, что они должны импортировать каждую стороннюю библиотеку в качестве зависимости. Есть способ получше.

Да:

— кивните одобрительно —

Слабое связывание

Теперь вы даже не можете сказать, что за ним стоит ngrx или что мы применяем шаблон redux. Только имена переменных и имена типов позволяют догадаться, что мы используем redux. Но я мог бы переименовать их:

Угадай шаблон redux

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

Когда мы говорим на языке redux, и находимся в его контексте, термин «действие» (action), безусловно, корректен. Но когда мы в наших контейнерных компонентах говорим на языке бизнеса, то действие больше похоже на команду (например, LOAD_COMMITS). Когда команда завершена, происходит событие (например, LOAD_COMMITS_SUCCESS, LOAD_COMMITS_FAILED), основанное на ее результате.

Выделение зависимостей также помогает нам повысить стабильность тестов. Правило тестов № 1 (по крайней мере для меня) гласит: не макетируй (mock) то, чем не владеешь. Зачем? Потому что это вне вашего контроля. Если в сторонней библиотеке изменяется что-то, о чем мы не знаем, то это в конечном итоге ломает все наши тесты. Тесты ломаются по неправильной причине. Они должны сломаться, если кто-то изменит поведение в логике нашего приложения, а не если инструмент управления состоянием изменился.

У нас должны быть специальные тесты контрактов для проверки интеграции с вашей библиотекой управления состоянием. Тесты должны потерпеть неудачу, если библиотека изменяется неожиданным способом.

Что мы выиграли?

  • Слабая связность
  • Чистый код
  • Поддерживаемость
  • Хотите заменить свою реализацию redux или текущее решение по управлению состоянием, не сходя с ума? Теперь нет проблем :)

Но как это вообще работает? Где находится код отправки (dispatch)? Позвольте мне представить вам Связанные действия (Bound Actions). Это не новая концепция, просто она не получила широкого распространения в сообществе Angular. На это определенно стоит посмотреть.

Конструктор действий

CreateBoundActionCreator связывает ActionCreator с функцией отправки (dispatch). Он возвращает функцию, которая принимает полезную нагрузку, затем создает действие и напрямую отправляет его с помощью данной функции отправки. Таким образом, мы можем скрыть зависимость ngrx от вызывающего действия.

Давайте определим связанное действие в нашем уже существующем классе CommitActions. Для этого класс CommitActions должен стать сервисом Angular, который внедряет хранилище ngrx и затем связывает функцию отправки с функцией действия:

Связывание действий

Теперь компонент контейнера больше не имеет прямой зависимости от ngrx. Это то, чего мы хотели достичь.

Слабое связывание компонента

3. Используй селекторы (selectors)

До сих пор мы только посылали команду, чтобы запустить побочный эффект (отправка HTTP запроса) и, в конце концов, загрузить коммиты в хранилище. Но мы все равно должны показать их пользователю. Для этого мы должны выбрать данные из хранилища Redux в нашем компоненте CommitsContainer. Это можно сделать напрямую, если мы внедрим в него сервис хранилища из ngrx.

К сожалению, при таком подходе мы снова получаем нежелательную зависимость от нашей библиотеки управления состоянием. Для этого мы создаем сервис CommitSelector, который содержит наши селекторы и может быть внедрен в компонент контейнера, чтобы эффективно скрыть от него зависимость ngrx.

Сервис селекторов

В CommitsContainer мы теперь можем использовать этот сервис, выбрать данные, и передать их нашему презентационному компоненту в шаблоне:

Внедрение сервиса в компонент
Шаблон компонента коммитов

Еще одна действительно приятная особенность селекторов — возможность их комбинировать. Таким образом, несколько простых селекторов могут быть объединены в один сложный. Ngrx поставляется с модифицируемыми функциями createSelector. Но вы также можете использовать библиотеку reselect.

Давайте посмотрим на пример. В приведенном выше коде мы захардкодили имя пользователя. Теперь мы хотели бы передать его динамически через параметр url-маршрута.

Я расширил пример использования ngrx/router-store для синхронизации текущего состояния маршрутизатора (angular/router) с нашим хранилищем ngrx. Это позволяет нам использовать мощность комбинированных селекторов, где routeSelector извлекает состояние маршрутизатора из хранилища, а routeParamSelector считывает из него переданный параметр маршрута.

Комбинирование селекторов

Теперь мы можем использовать селектор в нашем сервисе CommitSelectors:

Сервис селекторов

CommitsContainer описывает наблюдаемое (observable) имя пользователя и при каждом изменении выполняет действие loadCommits:

Подписка на изменяемое имя пользователя

Мы разработали компонент, который не имеет прямой зависимости ни от маршрутизатора, ни от библиотеки управления состоянием. Это важный шаг для получения обслуживаемого, тестируемого и более чистого кода, который более чётко выражает сферу его деятельности.

В следующем посте я, вероятно, сделаю еще один шаг и представлю ConnectMixin, который эффективно превратит наши контейнерные компоненты в глупые, которые просто используют @Input() и @Output() для подключения к хранилищу.

Посмотрите полный исходный код на github.

Подпишись на меня в Twitter :)

Примечание переводчика: примеры кода дополнены комментариями, исправлены незначительные ошибки, текст незначительно сокращён для более литературно красивого перевода.