[Перевод] Архитектура Redux. Да или нет?

Автор материала, перевод которого мы сегодня публикуем, говорит, что входит в команду мессенджера Hike, которая занимается новыми возможностями приложения. Цель этой команды заключается в том, чтобы воплощать в реальность и исследовать идеи, которые могут понравиться пользователям. Это означает, что действовать разработчикам нужно оперативно, и что им приходится часто вносить изменения в исследуемые ими новшества, которые направлены на то, чтобы сделать работу пользователей как можно более удобной и приятной. Они предпочитают проводить свои эксперименты с применением React Native, так как эта библиотека ускоряет разработку и позволяет использовать один и тот же код на разных платформах. Кроме того, они пользуются библиотекой Redux.

ac19ea87996279f729fd56933e24c42f.png
Когда разработчики из Hike начинают работу над чем-то новым, то, при обсуждении архитектуры исследуемого решения, у них возникает несколько вопросов:

  • Это — экспериментальная возможность, которая может, что называется, «не взлететь», и от неё придётся отказаться. Нужно ли, учитывая это, тратить время на проектирование архитектуры приложения?
  • Экспериментальное приложение — это всего лишь MVP, минимально жизнеспособный продукт, в котором имеется 1–2 экрана и который надо создать как можно быстрее. Стоит ли, учитывая это, связываться с Redux?
  • Как оправдать перед менеджерами по продукту время, необходимое на подготовку вспомогательной инфраструктуры экспериментального приложения?


Собственно говоря, найти правильные ответы на все эти вопросы помогает Redux. Архитектура Redux способствует отделению состояния приложения от React. Она позволяет создать глобальное хранилище, находящееся на верхнем уровне приложения и предоставляющее доступ к состоянию для всех остальных компонентов.

Разделение ответственностей


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

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

aa4b4fca367a26eeaa6d4affecc0eb95.png
Архитектура Redux

Вот краткая характеристика этих блоков:

  • Представления или компоненты пользовательского интерфейса (UI Components) напоминают чистые функции (то есть такие функции, которые не изменяют переданные им данные и обладают некоторыми другими свойствами), которые ответственны за вывод информации на экран на основе данных, переданных им из хранилища. Они не меняют данные напрямую. При возникновении какого-либо события, или если с ними взаимодействует пользователь, они обращаются к создателям действий.
  • Создатели действий (Action Creators) ответственны за создание и диспетчеризацию действий.
  • Редьюсеры (Reducers) получают диспетчеризованные действия и обновляют состояние хранилища.
  • Хранилище (Data Store) ответственно за хранение данных приложения.


Рассмотрим архитектуру Redux на примере.

Что делать, если разным компонентам нужны одни и те же данные?


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

4680741943d6d145443ba5ab97bddb3f.png
Экран с информацией о друзьях в приложении Hike

Здесь имеется 3 React-компонента:

  • FriendRow — компонент, содержащий имя друга пользователя и некоторые другие сведения о нём.
  • FriendsHeader — компонент, который выводит надпись «MY FRIENDS» и сведения о количестве друзей.
  • ContainerView — компонент-контейнер, который объединяет заголовок экрана, представленный компонентом FriendsHeader, и список друзей, полученный путём обхода массива, содержащего сведения о друзьях пользователя, каждый элемент которого оказывается представленным на экране компонентом FriendRow.


Вот код файла friendsContainer.js, иллюстрирующий вышесказанное:

class Container extends React.Component {

    constructor(props) {
      super(props);
      this.state = {
        friends: []
      };
    }

    componentDidMount() {
      FriendsService.fetchFriends().then((data) => {
        this.setState({
          friends: data
        });
      });
    }

    render() {
      const { friends } = this.state;
      return (
        
        
        {friends.map((friend) => ()) }
        
      );
    }
}


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

Подумаем теперь о том, что эти данные о друзьях могут понадобиться и в каких-то других компонентах, используемых в приложении.

6507b05997159a11bcc609a90cf7a9b2.png
Экран чатов в приложении Hike

Предположим, в приложении имеется экран чатов, который также содержит список друзей. Видно, что и на экране со списком друзей, и на экране чатов используются одни и те же данные. Как поступить в подобной ситуации? У нас есть два варианта:

  • Можно загрузить данные о друзьях снова, в компоненте ComposeChat, ответственном за вывод списков чатов. Однако такой подход не особенно хорош, так как его применение будет означать дублирование данных и может привести к проблемам с синхронизацией.
  • Можно загрузить данные о друзьях в компоненте высшего уровня (главный контейнер приложения) и передать эти данные компонентам, ответственным за вывод списка друзей и вывод списка чатов. Кроме того, нам надо передать этим компонентам функции для обновления данных о друзьях, что необходимо для поддержки синхронизации данных между компонентами. Такой подход приведёт к тому, что компонент высшего уровня будет буквально набит методами и данными, которые он сам напрямую не использует.


Оба эти варианта не так уж и привлекательны. Посмотрим теперь на то, как нашу проблему можно решить с использованием архитектуры Redux.

Использование Redux


Здесь речь идёт об организации работы с данными с использованием хранилища, создателей действий, редьюсеров и двух компонентов пользовательского интерфейса.

▍1. Хранилище данных


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

▍2. Создатели действий


В данном случае создатель действия используется для диспетчеризации событий, направленных на сохранение и обновление данных о друзьях. Вот код файла friendsActions.js:

export const onFriendsFetch = (friendsData) => {
  return {
    type: 'FRIENDS_FETCHED',
    payload: friendsData
  };
};


▍3. Редьюсеры


Редьюсеры ожидают поступления событий, представляющих диспетчеризованные действия, и обновляют данные о друзьях. Вот код файла friendsReducer.js:

const INITIAL_STATE = {
       friends: [],
    friendsFetched: false
};

function(state = INITIAL_STATE, action) {
    switch(action.type) {
    case 'FRIENDS_FETCHED':
        return {
            ...state,
            friends: action.payload,
            friendsFetched: true
        };
    }
}


▍4. Компонент, выводящий список друзей


Этот компонент-контейнер просматривает данные о друзьях и обновляет интерфейс при их изменении. Кроме того, он ответственен за загрузку данных из хранилища в том случае, если их у него нет. Вот код файла friendsContainer.js:

class Container extends React.Component {

    constructor(props) {
      super(props);
    }

    componentDidMount() {
      if(!this.props.friendsFetched) {
        FriendsService.fetchFriends().then((data) => {
          this.props.onFriendsFetch(data);
        });
      }
    }

    render() {
      const { friends } = this.props;
      return (
        
        
        {friends.map((friend) => ()) }
        
      );
    }
}

const mapStateToProps = (state) => ({
  ...state.friendsReducer
});

const mapActionToProps = (dispatch) => ({
  onFriendsFetch: (data) => {
    dispatch(FriendActions.onFriendsFetch(data)); 
  }
});

export default connect(mapStateToProps, mapActionToProps)(Container);


▍5. Компонент, выводящий список чатов


Этот компонент-контейнер так же пользуется данными из хранилища и реагирует на их обновление.

О реализации архитектуры Redux


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

Тестирование


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

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

Надо отметить, что независимому тестированию могут быть подвергнуты не только компоненты, ответственные за визуализацию данных, но и редьюсеры, и создатели действий.

Redux — это замечательно, но используя эту технологию мы столкнулись с некоторыми трудностями.

Трудности при использовании Redux


▍Избыток шаблонного кода


Для того чтобы реализовать в приложении архитектуру Redux, приходится потратить немало времени, сталкиваясь при этом со всяческими странными понятиями и сущностями.

Это так называемые санки (thunks), редьюсеры (reducers), действия (actions), промежуточные программные слои (middlewares), это функции mapStateToProps и mapDispatchToProps, а также многое другое. На то, чтобы всё это изучить, нужно время, а для того, чтобы научиться правильно этим пользоваться, требуется практика. В проекте оказывается очень много файлов, и, например, одно незначительное изменение компонента для визуализации данных может привести к необходимости вносить правки в четыре файла.

▍Хранилище Redux — это синглтон


В Redux хранилище данных построено с использованием паттерна «синглтон», хотя компоненты могут иметь несколько экземпляров. Чаще всего это не проблема, но в определённых ситуациях подобный подход к хранению данных может создавать некоторые сложности. Например, представим себе, что существуют два экземпляра некоего компонента. Когда в любом из этих экземпляров меняются данные, эти изменения сказываются и на другом экземпляре. В определённых случаях такое поведение может оказаться нежелательным, может понадобиться, чтобы каждый экземпляр компонента пользовался бы собственной копией данных.

Итоги


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

Уважаемые читатели! Пользуетесь ли вы Redux в своих проектах?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru