Хабрахабр

Как мы внедряли архитектуру RIBs. Доклад Яндекс.Такси

Привет, меня зовут Алексей Валякин, я пишу приложения для Андроида. Несколько месяцев назад я выступил на встрече команды Яндекс.Такси с мобильными разработчиками. Мой доклад был посвящен переходу на архитектуру RIBs в Такси (RIB означает тройку Router, Interactor, Builder). Вот видео, а под катом — конспект:

— Настало время немножко запрыгнуть на паровозик с хайпом. Это классическая тема про архитектуру в Андроиде.

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

Уже в тот момент у нас возникало огромное количество сложностей. Когда я пришел в компанию, наша команда состояла из четырех человек. Там было собрано довольно много технических проблем, одна из которых — неправильно выстроенный СI, большая вариативность в подходах, тесты, которые покрывают не всё. Проект был старый, он стартовал в 2012 году. И в целом было большое количество сложностей и merge-конфликтов.

Следовательно, стало еще больше merge-конфликтов, и при большой связанности кода вы понимаете, к чему это может привести. За два года мы выросли до 12 человек, а это означает, что у нас повысилась параллелизация разработки фич. Часть из этих проблем решал точечный рефакторинг, часть — библиотека компонентов, про которую стоит рассказывать в отдельном докладе. В какой-то момент мы просто начали тонуть, и нужно было как-то разобраться с этим.

Красивой архитектуры, гибкости разработки, простоты добавления фич, и, конечно, снижения сложности мержей — потому что в основном они порождают какие-то баги, которые могут всплыть на этапе релиза, когда фичи в изолированности протестированы и работают хорошо. Чего хотят все разработчики? Это примерная картина, к которой мы хотели прийти. А когда смержили и попали в релиз — хоп, все развалилось.

Понятно, что вариантов, как сделать что-то хорошо, довольно много. Как к ней можно идти? Конечно, есть и другие решения. Я расскажу об основных подходах и их минусах.

С какими трудностями мы сталкиваемся в классическом MVP? Классический MVP. И получается очень большая вариативность в том, что нужно добавить. Если рассматривать на примере нашего проекта, у нас получилось, что есть MVP Activity, MVP Fragment, MVP View. Потом оказывается, что добавить какую-то маленькую фичу, с которой к тебе приходит менеджер, довольно сложно, потому что это вообще находится в отдельном MVP Activity. В каких-то случаях ты думаешь, что нужно добавить вьюху и фрагмент.

Вам хочется гибко подключать детей и чтобы у вас была для этого какая-то сущность. Вторая проблема, которая есть у MVP, связана с тем, что напрашивается роутер. Да и view driven-подход — довольно большой минус. Поэтому обычно MVP приходят к какому-либо самописному роутеру либо еще к чему-то. В очень многих MVP-паттернах именно презентер инжектится во вьюху, это уже делает ее менее пассивной и нарушает clean architecture.

У него есть такая сущность, как роутер, он более абстрагирован, и все же у него есть ряд минусов. Viper получше. Cлой View тоже обязательный, от него нельзя избавиться. У него все еще остается view driven-логика, у него обязательный слой presenter, через который проходит бизнес-логика, и это не всегда верно.

Я видел, что есть какие-то адаптации, и некоторые из них даже ничего, но есть минусы. Главная проблема — эта архитектура пришла к нам из мира iOS, поэтому ее нужно определенным образом адаптировать под Андроид.

У RIBs тоже есть минусы. Понятно, что в мире архитектуры нет silver bullet, у каждой архитектуры есть свои плюсы и минусы. У них довольно мало открытых классов, нет сложных примеров. В целом, Uber представила эту архитектуру по большей части на уровне концепта. И при переходе на любую архитектуру следует большое количество рефакторинга, который вам нужно совершить, но этот минус есть не только у RIBs. Есть какие-то простенькие туториалы, которые вы можете пройти.

Она использует компоненты Dagger. Из чего же состоит архитектура RIBs? Presenter(View) — отдельный слой, иногда он может присутствовать, иногда — отсутствовать. У нее основной класс Builder собирает воедино весь этот компонент, который состоит из следующих частей: Router, Interactor. При этом Presenter(View) может быть как слита в один класс, так и разделена, если у вас ложная логика презентации.

Поскольку Presenter(View) являются опциональными, вы добавляете новые экраны примерно так же, как и новые бизнес-фичи. Что здесь еще есть из крутого? Ребенок ничего не знает о родителе, а родитель знает о детях. Ваша структура получается более чистой и понятной. Посмотрим, как это выглядит на примере упрощенной структуры.

Это корневой RIB. У вас всегда есть какой-то корень. Посмотрим на примере нашего приложения. Он решает, что в себя подключать, в зависимости от state вашего приложения: это либо авторизованное, либо неавторизованное состояние. Может быть, вы на заказе или не на заказе.

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

В данный момент мы как раз задумались о том, чтобы разбить наше приложение на модули. Примерно так может выглядеть структура модулей. На самом деле все реализовано довольно классически. У нас он был один. У вас есть какой-то core API, может быть network, базы данных и т. У вас есть какой-то модуль Common, он может быть разбит на еще более мелкие модули в зависимости от того, что вам требуется. И в нашей системе координат конкретный RIB — отдельный модуль, он подключает в себя все Common и т. д. д., то, что ему требуется, включая дочерние RIBs.

Если какие-то вещи нужно объединить между несколькими RIBs, здесь есть примеры с Shared feature classes, которые выделяются просто в отдельные модули.

Простота тестирования, высокая изоляция кода, single activity-подход, нет боли с фрагментами (кто работал, тот поймет), и единообразие. Какие плюсы есть у RIBs? И если у вас две команды, это большой плюс, потому что они будут говорить на одном языке. Это кроссплатформенная архитектура, есть подход и для iOS, и для Андроида.

Хотите небольшой лайфхак про внедрение RIBs? Здесь важный момент. В итоге вы просто берете и переносите их в свои классы. Предположим, вы переносите себе зависимости, потом начинаете дописывать extension-функции наследников и понимаете, что вам всего этого не хватает, нужно адаптировать это под себя. А есть другой путь — когда вы сразу переносите их в свои классы, уже не тратя время на первый вариант, и адаптируете это под себя.

Настало время немножко посмотреть на код, на то, как все это выглядит.

Он создает четыре основных класса — Builder, Interactor, Router и View, про которые я детальнее поговорю на следующих слайдах. У них есть удобный плагин, позволяющий генерировать классы, которые нужны для RIB, не тратя время на их создание. Естественно, он их за вас не напишет и вам придется написать их самим, но тем не менее, это довольно приятно. Также он генерирует тесты. Этот плагин сразу подключал бы все необходимые зависимости, и на настройку модуля тратилось бы меньше времени. Сейчас мы думаем о том, чтобы создать плагин, который позволит упростить создание новых модулей с RIBs.

Обычно View собирается простым вызовом конструктора, ничего сложного там нет. Итак, Builder — классический glue code component, основная задача которого — собрать все воедино, собрать Dagger-компонент и View. В некоторых случаях это может быть inflate.

Вторая часть, которая находится в Builder — она про зависимости, то есть про то, как ребенок получает какие-либо зависимости извне.

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

Только в него разрешены инжекты. Interactor — по сути, самый главный класс, который является бизнес-логикой. Он получает событие от UI layer с помощью Stream RX events. Это практически самая главная вещь, которая тестируется. Presenter — интерфейс, определяющий методы, которые предоставляет мое событие.

Тем, что на слое Interactor и Presenter вы можете организовать то взаимодействие, которое вам нравится. Чем еще удобен RIBs? Тут каждый волен выбирать то, что ему нравится. Это может быть и MVP, и MVVM, и MVVI. Примерно так может выглядеть подписка на события Presenter.

А вот как может выглядеть обработка этих событий.

У него нет никакой бизнес-логики, сам он не вызывает подключение детей. Router — класс, который отвечает за подключение детей. По сути, здесь я привожу упрощенный пример того, как это происходит. Это делает Interactor в такой концепции. Чаще всего эту логику можно инкапсулировать в отдельный transition, можно настроить анимации — все зависит от ваших потребностей. По факту у Builder просто вызывается метод Build, который собирает дочерний RIB и подключает ребенка напрямую с помощью attach child, а также добавлением вьюхи.

Она ничего в себя не инжектит, практически ни о чем не знает. View максимально пассивна в этой архитектуре. В более сложных вариантах эта логика разносится на два класса. В простейших случаях она может имплементировать интерфейс Presenter, если у вас нет никакой сложности представления. То есть у вас есть отдельный класс Presenter, который как раз маппит бизнес-данные — например, во view модели.

Обзёрвится Rx stream. Вот пример про то, каким образом Interactor получает UI события.

Когда вы так делаете, особенно в большом проекте, начинаются определенные сложности. Нельзя просто построить новую архитектуру. Вся эта логика была очень разрозненная. Нужно понимать, что у нас огромный проект: около 20 Activity, если не больше, и около 60 фрагментов. Нужно было все это как-то слить воедино.

Никто не позволит вам целый день внедрять новую архитектуру и останавливать бизнес. В первую очередь вам нужно слить все в единую точку навигации, сделать сначала god object — некий Activity Router, где вы будете управлять еще и стеком фрагментов, потому что у вас останется много старого кода. У RIBs тоже, естественно, есть стек — он доступен из-под капота. При этом вам нужно будет подружить его со стеком RIBs. Довольно много кода придется дописывать самим. Но что здесь важно? Поэтому первое, что мне пришлось сделать, когда я начал изучать эту архитектуру, — дописать наследника над роутером, который поддерживает восстановление иерархии RIBs и всего state приложения. Uber не поддерживает повороты экранов, поэтому он практически не парится о восстановлении state.

Ни один большой проект не может без этого обойтись. Вам понадобится поддерживать Feature toggling. Если кто-то смотрел Mobius 2016, на нем мы рассказывали про Plugin Factory, которая позволяет динамически подключать и отключать определенные блоки логики — не обязательно куски с экранами. Сейчас один из наших разработчиков разрабатывает концепт. Все делается абстрагированно, и взаимодействие упрощается. Она может действовать, например, в зависимости от экспериментов, которые приходят с сервера.

Это когда у вас есть несколько RIBs, которые ничего не знают друг о друге, находятся примерно на одном уровне, но при этом вам нужно запустить процесс с данными на входе, а на выходе в конце вы должны собрать все воедино. RIBs workflow — тоже интересная концепция, которая может вам понадобиться.

У нас суперкастомный дизайн, поэтому почти не осталось никаких классических диалогов. И, например, модальные экраны. Все самописное, нам все приходится реализовывать самим.

Изолированность кода, понятную простую архитектуру, легкий путь к модуляризации, избавление от фрагментов, Single activity-подход и удобство параллельной разработки фич. Что вы можете получить, используя RIBs?

Ссылки:
— github.com/uber/RIBs
— github.com/uber/RIBs/tree/master/android/tutorials
— habr.com/ru/company/livetyping/blog/320452
— youtu.be/Q5cTT0M0YXg
— github.com/xzaleksey/Role-Playing-System-V2
— github.com/xzaleksey/DeezerSample

Полгода назад я начал разрабатывать на RIBs, чтобы попробовать эту архитектуру, прежде чем внедрять ее к нам. Последняя ссылка ведет на мой pet project. Я много экспериментировал, поэтому есть и спорные вещи. И там есть более-менее реальные кейсы, которые могут вам помочь. И может быть, вы оттуда возьмете что-то для себя. Но в целом там можно посмотреть, как оно работает.

Будут такие Яндекс-рибы. Также мы думаем о том, чтобы потом выделить все это в отдельную библиотеку, как мы сделали в свое время с библиотекой компонентов. Я все рассказал, спасибо. Но это в будущем.

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть