- PVSM.RU - https://www.pvsm.ru -

За последние два года Android-разработчики в Badoo прошли длинный тернистый путь от MVP к совершенно иному подходу к архитектуре приложений. Мы с ANublo [1] хотим поделиться переводом статьи нашего коллеги Zsolt Kocsi [2], описывающую проблемы, с которыми мы столкнулись, и их решение.
Это первая из двух статей, посвящённых разработке современной MVI-архитектуры на Kotlin.
В каждый момент времени у приложения есть определённое состояние, которое задаёт его поведение и то, что видит пользователь. Если сфокусироваться лишь на паре классов, это состояние включает в себя все значения переменных — от простых флагов до отдельных объектов. Каждая из этих переменных живёт своей жизнью и управляется различными частями кода. Определить текущее состояние приложения можно, лишь проверив их одну за другой.
Работая над кодом, мы создаём уже существующую у нас в голове модель работы системы. Мы легко реализуем идеальные случаи, когда всё идет по плану, но совершенно неспособны просчитать все возможные проблемы и состояния приложения. И рано или поздно одно из не предусмотренных нами состояний нас настигнет, и мы столкнёмся с багом.
Изначально код пишется в соответствии с нашими представлениями о том, как система должна работать. Но в дальнейшем, проходя через пять стадий отладки [3], приходится мучительно всё переделывать, попутно меняя сложившуюся в голове модель уже созданной системы. Остаётся надеяться, что рано или поздно к нам придёт понимание того, что же пошло не так, и баг будет исправлен.
Но так везёт далеко не всегда. Чем сложнее система, тем больше шансов столкнуться с каким-нибудь непредвиденным состоянием, отладка которого ещё долго будет сниться в ночных кошмарах.
В Badoo все приложения асинхронны — не только из-за обширного функционала, доступного пользователю через UI, но и из-за возможности односторонней отправки данных сервером. На состояние и поведение приложения оказывает влияние многое — от изменения статуса оплаты до новых совпадений и запросов верификации.
В результате в нашем чат-модуле нам попалось несколько странных и трудновоспроизводимых багов, попортивших всем немало крови. Иногда тестировщикам удавалось их записать, но на устройстве разработчика они не повторялись. Из-за асинхронности кода повторение в полном объёме той или иной цепочки событий было крайне маловероятно. И поскольку приложение не падало, у нас даже не было стек-трейса, который показал бы, откуда начинать поиски.
Clean Architecture (чистая архитектура [4]) тоже не смогла нам помочь. Даже после того как мы переписали чат-модуль, A/B-тесты выявляли небольшие, но значимые несоответствия в количестве сообщений пользователей, использовавших новый и старый модули. Мы решили, что это связано с трудновоспроизводимостью багов и состоянием гонки. Несоответствие сохранялось и после проверки всех остальных факторов. Интересы компании страдали, разработчикам было тяжело поддерживать код.
Нельзя выпускать новый компонент, если он работает хуже существующего, но не выпускать его тоже нельзя — раз уж потребовалось обновление, значит, на то была причина. Итак, необходимо разобраться, почему в системе, которая выглядит совершенно нормально и не вылетает, падает число сообщений.
Откуда же начинать поиски?
Спойлер: это не вина Clean Architecture — виноват, как всегда, человеческий фактор. В конечном итоге мы, конечно, исправили эти баги, но потратили на это много времени и сил. Тогда мы задумались: а нет ли более простого способа избежать возникновения этих проблем?
Модные термины вроде Model-View-Intent и «однонаправленный поток данных» нам хорошо знакомы. Если в вашем случае это не так, советую их загуглить — в Интернете полно статей на эти темы. Android-разработчикам особенно рекомендую материал Ханнеса Дорфмана в восьми частях [5].
Мы начали играть с этими взятыми из веб-разработки идеями ещё в начале 2017 года. Подходы наподобие Flux и Redux оказались очень полезны — они помогали нам справиться со многими проблемами.
Прежде всего, очень полезно содержать все элементы состояния (переменные, влияющие на UI и запускающие различные действия) в одном объекте — State. Когда всё хранится в одном месте, лучше видна общая картина. Например, если вы хотите представить загрузку данных с использованием такого подхода, то вам потребуются поля payload и isLoading. Взглянув на них, вы увидите, когда данные получены (payload) и показывается ли при этом пользователю анимация (isLoading).
Далее, если мы отойдём от параллельного выполнения кода с колбеками и выразим изменения состояния приложения в виде серии транзакций, мы получим единую точку входа. Представляем вам Reducer, прибывший к нам из функционального программирования. Он берёт текущее состояние и данные о дальнейших действиях (Intent) и создаёт из них новое состояние:
Reducer = (State, Intent) -> State
Продолжая предыдущий пример с загрузкой данных, мы получаем следующие действия:
Тогда можно создать Reducer со следующими правилами:
Если мы выведем получившуюся серию State в лог, мы увидим следующее:
Подключив эти состояния к UI, вы увидите все стадии процесса: сначала пустой экран, затем экран загрузки и, наконец, нужные данные.
У такого подхода есть множество плюсов.
Или нельзя?
Одного Reducer явно недостаточно. Как быть с асинхронными задачами с различными результатами? Как реагировать на пуши с сервера? Как быть с запуском дополнительных задач (например, очистки кеша или загрузки данных из локальной базы) после изменения состояния? Выходит, что либо мы не включаем всю эту логику в Reducer (то есть добрая половина бизнес-логики окажется не охвачена, и о ней придётся позаботиться тем, кто решит воспользоваться нашим компонентом), либо заставляем Reducer заниматься всем сразу.
Нам, конечно, хотелось бы заключить всю бизнес-логику отдельной фичи в самостоятельный компонент, с которым разработчики из других команд могли бы легко работать, просто создав его инстанс и подписавшись на его состояние.
Кроме того:
Мы не сразу перешли от Reducer к решению, которое используем сегодня. Каждая команда сталкивалась с проблемами при использовании различных подходов, и разработка универсального решения, которое устроило бы всех, казалась маловероятной.
И все же, текущее положение вещей устраивает всех. Рады представить вам MVICore! Исходный код библиотеки открыт и доступен на GitHub [6].
Поскольку на GitHub уже выложена пошаговая инструкция, я опущу подробные примеры и остановлюсь на основных составляющих фреймворка.
Feature — центральный элемент фреймворка, содержащий всю бизнес-логику компонента. Feature определяется тремя параметрами: interface Feature<Wish, State, News>
Wish соответствует Intent из Model-View-Intent — это те изменения, которые мы хотим видеть в модели (поскольку термин Intent имеет своё значение в среде Android-разработчиков, нам пришлось найти другое название). Wish — это точка входа для Feature.
State — это, как вы уже поняли, состояние компонента. State не изменяем (immutable): мы не можем менять его внутренние значения, но можем создавать новые States. Это и выходные данные: всякий раз, создавая новое состояние, мы передаём его в Rx-стрим.
News — компонент для обработки сигналов, которых не должно быть в State; News используется один раз при создании (проблема SingleLiveEvent [7]). Использование News необязательно (в сигнатуре Feature можно использовать Nothing из Kotlin).
Также в Feature обязательно должен присутствовать Reducer.
Feature может содержать следующие компоненты:
Схема может выглядеть просто:

или включать в себя все перечисленные выше дополнительные компоненты:

Сама же Feature, содержащая всю бизнес-логику и готовая к использованию, выглядит проще некуда:

Feature, краеугольный камень фреймворка, работает на концептуальном уровне. Но библиотека может предложить гораздо больше.
Надеемся, вы попробуете нашу библиотеку [6] и её использование доставит вам столько же радости, сколько нам — её создание!
24 и 25 ноября можно познакомиться с нашей командой мобильной разработки поближе. Мы проведём hiring event: за один день можно будет пройти все этапы отбора и получить оффер. Общаться с кандидатами в Москву приедут мои коллеги из iOS- и Android-команд. Если вы из другого города, расходы на проезд берёт на себя Badoo. Чтобы получить приглашение, пройдите отборочный тест по ссылке [9]. Удачи!
Автор: Андрей Шиков
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/298986
Ссылки в тексте:
[1] ANublo: https://habr.com/users/anublo/
[2] Zsolt Kocsi: https://badootech.badoo.com/@zsolt.kocsi
[3] пять стадий отладки: http://i.imgur.com/qsLx1Ip.jpg
[4] чистая архитектура: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
[5] материал Ханнеса Дорфмана в восьми частях: http://hannesdorfmann.com/android/mosby3-mvi-1
[6] GitHub: https://github.com/badoo/MVICore
[7] проблема SingleLiveEvent: https://github.com/googlesamples/android-architecture-components/issues/63
[8] DebugDrawer: https://github.com/palaima/DebugDrawer
[9] по ссылке: https://events.badoo.com/mvi1
[10] Источник: https://habr.com/post/429728/?utm_source=habrahabr&utm_medium=rss&utm_campaign=429728
Нажмите здесь для печати.