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

в 14:47, , рубрики: android, mvp, ribs, viper, Блог компании Яндекс, Проектирование и рефакторинг, разработка мобильных приложений, Разработка под android

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ссылки:
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

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

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

Автор: Эдуард

Источник


* - обязательные к заполнению поля