Кроссплатформенная мобильная архитектура RIBs от Uber

https://github.com/uber/RIBs/wiki
  • Перевод
20 декабря 2016 года ребята из Uber Engineering опубликовали статью про новую архитектуру (вот перевод этой статьи на хабре). Представляю вашему вниманию перевод основной части документации.

Для чего вообще нужна архитектура RIBs?


RIBs — кроссплатформенный архитектурный фреймворк от Uber. Он был разработан для больших мобильных приложений с большим количеством вложенных состояний.

При разработке этой структуры инженеры Uber придерживались следующих принципов:

  • Поддержка сотрудничества между людьми, разрабатывающими на разных платформах: подавляющее большинство сложных частей приложений Uber аналогичны на iOS и Android. RIBs обеспечивает общие паттерны разработки на Android и iOS. При использовании RIBs, инженеры как на iOS, так и на Android могут совместно использовать одну совместно разработанную архитектуру для своих функций.
  • Минимизация глобальных состояний и решений: глобальные изменения состояния могут привести к непредсказуемому поведению и могут сделать невозможным знание того, к чему приведут те или иные изменения в программном коде. Архитектура на основе RIBs поощряет инкапсулированные состояния в глубокой иерархии хорошо изолированных RIB, что позволяет избежать проблем с глобальными состояниями.
  • Тестируемость и изоляция: классы должны быть простыми для того, чтобы можно было написать unit-тесты, а также иметь причину для того, чтобы быть изолированными (отсылка к SRP). Отдельные классы RIB имеют разные обязанности (например, маршрутизация, бизнес-логика, логика представления, создание других классов RIB). Кроме того, логика родительского RIB, в основном, отделена от логики дочернего RIB. Это позволяет легко тестировать классы RIB и снижать зависимость между компонентами системы.
  • Инструменты для продуктивной разработки: заимствование нетривиальных архитектурных паттернов может привести к проблемам при росте приложения, если не будет надежных инструментов для поддержки архитектуры. Архитектура RIBs поставляется с инструментами IDE для создания кода, статического анализа и интеграции во время выполнения, что повышает производительность разработчиков в больших и малых командах.
  • Принцип открытости-закрытости: разработчики, по возможности, должны добавлять новые функции без изменения существующего кода. При использовании RIBs, выполнение этого правила можно увидеть в ряде мест. Например, вы можете присоединить или создать сложный дочерний RIB, который требует зависимостей от своего родительского RIB, практически без изменений в родительском RIB.
  • Структурирование вокруг бизнес-логики: структура бизнес-логики приложения не должна строго отражать структуру пользовательского интерфейса. Например, чтобы облегчить анимацию и производительность представления, иерархия представлений может быть более мелкой, чем иерархия RIB. Или, одна функция RIB может управлять появлением трех представлений, которые отображаются в разных местах пользовательского интерфейса.
  • Точные контракты: требования должны быть объявлены с помощью контрактов, которые проверяются во время компиляции. Класс не должен компилироваться, если его собственные зависимости, а также гостевые зависимости не удовлетворены. В архитектуре RIBs используется ReactiveX для представления гостевых зависимостей, типобезопасные системы внедрения зависимостей (DI) для представления зависимостей классов, а также многие другие возможности DI для того, чтобы способствовать созданию инвариантов данных.

Составляющие элементы RIBs


Если вы ранее работали с архитектурой VIPER, тогда классы, которые входят в состав RIB, будут выглядеть вам знакомыми. RIB обычно состоят из следующих элементов, каждый из которых реализован в своем классе:



Interactor


Interactor содержит бизнес-логику. В этом классе происходит подписка на Rx уведомления, принимаются решения об изменении состояния, хранении данных и прикреплении дочерних RIB.

Все операции, выполняемые в Interactor'е, должны быть ограничены его жизненным циклом. В Uber создали инструментарий для обеспечения того, чтобы бизнес-логика выполнялась только при активном взаимодействии. Это предотвращает дезактивацию Interactor'ов, но Rx подписки по-прежнему срабатывают и вызывают нежелательные обновления бизнес-логики или состояния пользовательского интерфейса.

Router


Router отслеживает события от Interactor'а и преобразует эти события в прикрепление и открепление дочерних RIB. Router существует по трем простым причинам:

  • Router существует как пассивный объект, что упрощает тестирование сложной логики Interactor'а без необходимости создавать заглушки для дочерних Interactor'ов или каким-то другим способом заботиться об их существовании.
  • Router'ы создают дополнительный уровень абстракции между родительским и дочерними Interactor'ами. Это делает синхронную связь между Interactor'ами немного сложнее и стимулирует использование Rx связи вместо прямой связи между RIB.
  • Router'ы содержат простую и повторяющуюся логику маршрутизации, которая в противном случае была бы реализована в Interactor'ах. Перенос этого шаблонного кода в Router'ы помогает Interactor'ам быть небольшими и более сосредоточенными на основной бизнес-логике RIB.

Builder


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

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

Presenter


Presenter это класс без состояния, который транслирует бизнес-модель в модель представления и наоборот. Он может использоваться для облегчения тестирования преобразований модели представления. Однако часто этот перевод настолько тривиален, что он не оправдывает создание отдельного класса Presenter. Если Presenter не сделан, то трансляция моделей представления становится обязанностью View (Controller) или Interactor'а.

View(Controller)


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

Component


Component используется для управления зависимостями RIB. Он помогает Builder'у создавать экземпляры других классов, из которых состоит RIB. Component обеспечивает доступ к внешним зависимостям, необходимым для создания RIB, а также к собственным зависимостям, созданными самим RIB, и контролируют доступ к ним из других RIB. Component родительского RIB обычно внедряется в дочерний RIB-Builder, чтобы предоставить дочернему RIB доступ к зависимостям родительского RIB.

Управление состоянием


Состояние приложения, в основном, управляется и представлено RIBs, которые в настоящее время подключены к дереву RIB. Например, по мере того, как пользователь переходит через разные состояния в упрощенном приложении для совместных поездок, приложение присоединяет и отделяет следующие RIBs:



RIBs только принимают решения о состоянии в пределах своей компетенции. Например, LoggedIn RIB только принимает решение для перехода между такими состояниями, как Request и OnTrip. Он не принимает никаких решений о том, каким должно быть поведение системы когда мы находимся на экране OnTrip.

Не все состояния могут быть сохранены путем добавления или удаления RIB. Например, когда настройки профиля пользователя изменяются, RIB не привязывается или не отсоединяется. Как правило, мы сохраняем это состояние внутри потоков неизменяемых моделей, которые заново отправляют значения при изменении деталей. Например, имя пользователя может быть сохранено в файле ProfileDataStream, который находится в компетенции LoggedIn. Только сетевые ответы имеют доступ на запись к этому потоку. Мы передаем интерфейс, который обеспечивает доступ на чтение к этим потокам вниз по DI графу.

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

Взаимодействие между RIBs


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

Как правило, если связь идет вниз к дочернему RIB, то мы передаем эту информацию как события в Rx потоке. Или данные могут быть включены как параметр в метод build() дочернего RIB, и в этом случае этот параметр становится инвариантом для времени жизни дочернего элемента.



Если связь идет вверх по дереву RIB к родительскому RIB Interactor'у, то эта связь сделана через интерфейс слушателя, так как родительский RIB может иметь более длинный жизненный цикл чем дочерний RIB. Родительский RIB, или некоторый объект на его DI графе, реализует интерфейс слушателя и помещает его на свой DI граф, чтобы его дочерние RIB могли его вызывать. Использование этого шаблона для передачи данных вверх вместо того, чтобы родительские RIBs напрямую подписались на Rx потоки своих дочерних RIBs, имеет несколько преимуществ. Он предотвращает утечку памяти, позволяет писать, тестировать и поддерживать родительские RIBs без знания того, какие дочерние RIBs к ним прикреплены, а также уменьшает количество возни, необходимой для прикрепления/отсоединения дочернего RIB. Rx потокам или слушателям не нужно отменять регистрацию или заново регистрироваться при таком методе прикрепления дочернего RIB.



RIB инструментарий


Чтобы обеспечить плавное внедрение архитектуры RIB в приложениях, инженеры Uber создали инструментарий для упрощения использования RIB и использования инвариантов, созданных путем внедрения архитектуры RIB. Исходный код этого инструментария частично был открыт и упоминается в примерах (см.правую часть — прим.пер.).

Инструментарий, который на данный момент имеет открытый исходный код, включает в себя:

  • Кодогенератор: плагины IDE для создания новых RIB и сопутствующих тестов.

  • Статический анализатор NPE (Android): NullAway это инструмент статического анализа, который позволяет вам забыть про NullPointerExceptions.
  • Статический анализатор автоматического размещения (Android): предотвращает наиболее распространенные утечки памяти в RIB.

Инструментарий, у которого Uber планирует открыть исходный код в будущем:

  • Статический анализатор, предотвращающий различные утечки памяти в RIB
  • Интеграция RIB с детектором утечек памяти во время выполнения программы
  • (Android) Процессоры аннотаций для упрощения тестирования
  • (Android) Статический анализатор RxJava, который обеспечивает RIB'ы неизменяемыми из основного потока представлениями(views)

P.S.


Нам в sports.ru очень понравился подход инженеров Uber, т.к. мы много раз сталкивались со всеми архитектурными проблемами, которые описывала статья. Несмотря на продуманность, у RIB есть ряд недостатков, например достаточно высокий порог вхождения в архитектуру. Мы разберем более подробно плюсы и минусы архитектуры в следующих статьях, их планируется как минимум две — для iOS и для Android. Для тех, кто хочет погрузиться в RIB прямо сейчас, на странице вики справа есть колонка, в которой есть уроки на английском. От себя замечу, что архитектура явно рождалась в долгих технических дискуссиях и собрала в себе лучшие практики построения архитектур для мобильных приложений, которые есть на данный момент. Ну и напоследок немного пиара — мы в sports.ru тоже любим технические дискуссии, часто проводим технические мастер-классы для коллег, регулярно изучаем новые технологии и в целом у нас классная атмосфера. Так что если вы хотите стать частью нашей команды — welcome!
  • +12
  • 2,7k
  • 7
Sports.ru
53,00
Компания
Поделиться публикацией

Комментарии 7

    +1
    Новый монстр — мутант?
    «Создание кнопки» — прочитайте документацию стр.100 — 150, если не получилось «Не беда!,
    велкам на форум сообщества, чуть-чуть упорства и через неделю у вас все получится».

    -«Почему сложно?»
    -«Да вы просто не понимаете, у нас же самые лучшие индийские разработчики, они плохого не придумают»

    Так мне видится вся эта кросплатформенность, основываясь на своем опыте
      0
      Монстр совсем не новый, ему уже года 2, если не больше :-)

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

      Так что я призываю вас к конструктиву — ознакомиться с инструментом получше, составить его плюсы и минусы для себя, ну и далее я с удовольствием пообщаюсь с вами на эту тему. Также было бы здорово узнать какой подход в разработке применяете лично вы, возможно у вас есть какая-то хорошая альтернатива, о которой я не знаю :-)
        –1
        Я выбрал для себя натив для каждой из платформ, т.к мой сегмент корпоративный софт.
        По времени разработки не сильно дольше, зато 100% свобода, выше надежность и прогнозируемость, меньше багов. Не агитирую… для меня это best way

          0
          Так RIB это как раз нативный подход, приложения пишутся на родных для платформ языках. У Uber в репозитории это Swift и Java. Просто Uber постарались разработать архитектуру так, чтобы она была максимально похожа на обеих платформах. Возможно, слово «кроссплатформенность» в заголовке ввело вас в заблуждение, но в оригинальной статье написано именно оно.

          В плане натива я с вами полностью согласен.
      0
      Вот архитектура, которая может быть легко перенесена на любую платформу github.com/shishkin1966/CleanArchitecture5. Уровень вхождения в архитектуру почти нулевой.
        +1
        Всем привет! Давно ждал обзора RIBs на хабре. Мы (один из крупнейших банков в Австралии) уже успешно построили более 5 корпоративных приложений на RIBs и отзывы от разработчиков (разных категорий) исключительно положительные. Да, в самом начале есть потеря времени на вхождение в процесс и усвоение основ этой архитектуры. Но это время в разы меньше по сравнению с тем же VIPER-ом и тп. Конечно если лениво вникать, то можно продолжаться строить Massive View Controllers ну или запилить свой велосипед (очень, кстати, понимаю, когда хочется выразить весь свой опыт в своей мега архитектуре). Ребята из Убер реально вложились в проект, думая о различных аспектах современной мобильной разработки в команде. Респект им!

        Кто хочет попробовать эту архитектуру, но сталкивается с вопросами, мы создали сообщество в слэке: uber-ribs-invite-automation.herokuapp.com Там всегда рады помочь и разобраться со спорными вопросами.

        А также есть пример использования для основных задач в большинстве мобильных приложений: github.com/dev4jam/ToDo

          0
          Приятно видеть обзор архитектуры, к которой приложил руку (только в плане документации на гитхабе ;)).

          Про нашу мотивацию при создании RIBs можно посмотреть небольшое видео:



          И, да, это не кроссплатформенный подход, все исключительно нативное.

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

          Самое читаемое