С точки зрения разработки покупка и оформление заказа — это синонимы, и называются они одним словом «чекаут». В него входят разные параметры: от списка товаров и способа доставки до банковских реквизитов получателя. Чтобы отображать в интерфейсе актуальное состояние чекаута для каждого пользователя, нужно много данных от бэкенда.
Мы опирались на четыре сущности бизнес-логики:
- Посылки
- Опции доставки
- Опции оплаты
- Саммари по заказу
У нас была задача: написать код, который сможет правильно всё это отобразить. Вот какие три подхода мы использовали.
MVC
Суть подхода. Использовали базовый паттерн iOS-разработки. Это простая и понятная схема для любого iOS-разработчика, и мы посчитали её подходящей для нашей задачи.
Когда-то мы в Яндексе создали сервис CAPI, основная роль которого — актуализировать состояние чекаута, которое передано от приложения. Единственный момент: когда пользователь начинал взаимодействовать с приложением, у чекаута не было состояния. Именно поэтому сервису было нечего актуализировать и передавать.
Чтобы решить эту проблему, мы сделали так, чтобы приложение отправляло в CAPI пустой запрос за всеми доступными опциями, а бэкенд в свою очередь возвращал информацию по каждой из них. Оставалось только выбрать нужную и актуализировать состояние по ней.
Когда пользователь оформлял заказ и, например, менял адрес, это считалось изменением состояния. Приложение запрашивало информацию по всем опциям, среди которых был адрес, а потом актуальная информация появлялась в нужном поле.
Мы немного изменили базовый паттерн MVC, потому что у нас была одна активная модель — чекаут. Внутри неё были зашиты:
- Обращения к клиенту и бэкенду
- Сетевые клиенты и вспомогательные зависимости — например, менеджер адресов
- Вся бизнес-логика
Что пошло не так. Нам казалось, что MVC — это очень простая архитектура, которая не может сломаться. Но мы столкнулись с несколькими проблемами:
- Модель разрослась, поэтому вносить изменения стало сложно. Мы не могли предсказать, что перестанет работать после очередных экспериментов.
- UI стал слишком много знать о бизнес-логике, поэтому мы не могли проводить продуктовые тесты в том же интерфейсе. Например, приходилось создавать новую ячейку адреса рядом со старой, чтобы ничего не сломалось.
- Высокий порог вхождения. Код был весь намешан в одном классе, и во всём этом стало сложно разбираться. Вместо четырёх человек в команде стало 40, фичи делали долго.
Redux
Суть подхода. Использовали архитектуру с однонаправленным потоком данных и тремя концепциями:
- State — состояние всего приложения, описывается внутри него в отдельной структуре данных
- Views — подписываются на состояния и актуализируют его сами на основе изменений
- State Changes — отображает изменения состояний; состоит из Action — объектов, которые описывают изменения, и Reducer — обработчиков этих изменений
Мы взяли готовую библиотеку ReSwift с GitHub, потому что у Яндекс Маркета были похожие связи между концепциями State, Views, Action и Reducer.
Дополнительно мы реализовали кастомный Action — Thunk. Он был нужен, чтобы обойти однопоточный и последовательный процесс в Redux и выполнять асинхронные операции к бэкенду.
Чекаут изменился и распался на две сущности:
- State — наша базовая сущность из бизнес-логики.
- UserInput — структура, в которой сохранялись данные, введённые или изменённые пользователем.
Action превратились в enum, в котором каждый кейс содержит смысловую нагрузку в зависимости от того, что пользователь выбрал и куда он кликнул.
Вся бизнес-логика происходила в редьюсерах — это чистые функции, которые подходят для критически важных частей бизнес-логики. Ниже — пример в коде.
У нас в подобных редьюсерах был сброс пользовательского ввода. Там же находилась логика по приоритизации предвыбранной опции оплаты.
После реализации Redux-подхода у нас появилась возможность писать тесты на бизнес-логику, потому что UI оказался от неё отделён. Также мы сепарировали куски бизнес-логики друг от друга, потому что появилось много чистых функций, которые проще тестировать и в которые разработчикам проще вникать. Это помогло решить проблему с командой и понизило порог входа в код, что ускорило запуск фич.
Что пошло не так. Всё работало хорошо, пока не пришёл новый продуктовый запрос. Нужно было изменить сценарий взаимодействия с пользователем и научиться переключаться между двумя разными товарными предложениями: «доставка по клику» и «экспресс-доставка сегодня».
Чтобы это сделать, на старте мы добавили ещё один запрос к бэкенду, после которого получали массив альтернатив и актуализировали данные. В результате работа усложнилась и замедлилась.
Всю эту сложную реализацию мы раскатали на iOS, Android и web, но не были уверены, что на всех платформах сценарии будут работать одинаково хорошо.
BDUI
Суть подхода. Внедрили API, через который нам с сервера приходит вёрстка, готовая для отображения в интерфейсе. Благодаря BDUI мы теперь можем добавлять фичи без дополнительного ревью со стороны App Store и других сторов.
Мы начали реализовывать этот подход полтора года назад и создали свой движок Flex. Он умеет отображать контент на UIkit, DivKit, Compose, SwiftUI. Это даёт больше возможностей для работы на разных платформах, при этом мы тратим меньше сил и времени на адаптацию.
Чтобы внедрить BDUI в Яндекс Маркете, нужно было полностью переделать бэкенд. Мы перенесли на него состояние чекаута из приложения для хранения данных и интеграции с другими микросервисами. Так мы уменьшили количество запросов чекаута с трёх до одного.
Вот как упростилась схема:
- На старте — проверяем /getState и получаем готовый UI для отрисовки
- При изменении данных — отправляем запрос /patchState, в котором передаётся тип изменений и параметр полезной нагрузки payload
Подход BDUI у нас состоит из трёх основных компонентов:
- DivKit — вёрстка на переменных, чтобы быстрее выбирать интерактивные элементы.
- Actions — взаимодействия пользователей.
- MAPI — мобильный бэкенд для BDUI, в котором мы подготавливаем вёрстку.
BDUI хорошо работает для экранов со статической информацией. Чтобы получить быстрый отклик при прямом взаимодействии с пользователем, мы добавили несколько лайфхаков для чекаута. Например, фейковый документ на время загрузки. Если пользователь выбирает экран адресов, мы показываем скелетон по дополнительному запросу, пока страница загружается. Это уменьшает время ожидания и приближает интерфейс к нативному.
В результате удалось ускорить открытие чекаута с 5 300 мс до 1 600 мс, а ещё работать одновременно на iOS и Android и писать фичи без отдельных релизов.
Что можно улучшить. Несмотря на то что новая архитектура нас устраивает, есть мелочи, которые можно улучшить. У нас их две:
- При релизах бэкенда невозможно провести регресс всей функциональности, поэтому бывает, что релизы приходится откатывать из-за внесённых ошибок.
- Ломается обратная совместимость в бэкендах вёрстки и актуализатора данных из-за несинхронного релизного цикла — без версий приложения.