Быстрый, простой, сложный: как мы выпилили Realm

в 9:10, , рубрики: dodo engineering, dodopizzaengineering, Realm, swift, Администрирование баз данных, базы данных, Блог компании Dodo Engineering, мобильная разработка, разработка мобильных приложений, разработка под iOS
Быстрый, простой, сложный: как мы выпилили Realm - 1

Когда на собеседованиях я спрашиваю у кандидата с какими базами он работал и что делал, то чаще всего слышу про Realm. Типичный ответ: он быстрее и с ним проще работать, создал конфиг, описал модель, готово. Но за все удобства Realm придётся расплачиваться: он вроде бы решает одну проблему, но тут же добавляет пару своих. А последствия, недостатки и проблемы обычно обсудить не получается: нужен опыт использования в пару лет, а его обычно нет. 

Мы тоже использовали Realm — 3 года подряд. Сначала он помогал, потом раздражал, пару раз выстрелил и в конце чуть не вогнал команду в депрессию. В итоге мы удалили Realm из проекта, потому что это сложный инструмент, который нужно правильно обслуживать, а простота интеграции обманчива. 

Примечание. Realm читается как «рэлм», не «реалм»

Зачем нужна база данных для заказа пиццы?

Кратко — незачем. База данных сначала прикрывала плохое API.

В 2017 году Dodo Pizza решила написать свое приложение. Серверная часть уже работала 6 лет и обслуживала 250+ пиццерий (на начало 2021 почти 700). Много работы было сделано для бизнеса, а для клиентов был только сайт — нужно делать приложение.

Чтобы подключить приложение —  нужно новое API и срочно, ребята торопились. Чтобы ускориться, часть работы разделили: API было больше похоже на прокси для базы данных, а часть логики решало приложение. 

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

Весь жизненный путь приложения сложно угадать на старте, база данных могла как сильно помочь, так и оказаться избыточной. Немного промахнулись, оказалось, что проще без неё.

Realm vs Core Data

Сложно вспомнить почему выбрали Realm, а не Core Data. Скорее всего, так было проще: схему базы рисовать не нужно, объекты создаются сразу в коде, работает быстрее, да и опыт работы с ней был. Так и поехало. 

Как работало

Первую версию приложения сделала команда на аутсорсе. Уже тогда понимали, что проект будет жить долго, стран будет много, фич навалом. Тогда казалось важным заложить поддержку работы в офлайне. Если нет, то, хотя бы, настроить восстановление между сессиями работы приложения, чтобы меню появлялось сразу на запуске, без скачивания. Архитектурно это заложили с первой строки. 

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

  • получили данные из сети;

  • положили в базу, разметили связи между таблицами;

  • прочитали из базы, связанные объекты подтянулись сами;

  • переложили данные во view-модели, а дальше уже MVVM. 

Realm требователен к переключению потоков, поэтому вся работа с базой архитектурно заворачивалась в один фоновый поток. Это было правильное и удачное решение (без шуток), но к нему мы ещё вернемся. 

Недостатки Realm

После релиза проект перешел на поддержку от аутсорса к внутренней команде, её для этого только собрали. Новым разработчикам, конечно, не нравятся все старые решения, но понимания проекта не было, поэтому особо ничего не меняли — рефакторили только рядом с бизнес-фичами. С Realm работать не нравилось, но это был лишь вопрос вкуса. 

Realm накладывает ряд ограничений сам по себе.

Хранит только сырые данные. Enum надо перекладывать в String или Int, Optional в RealmOptional, массивы в List, обратные ссылки в LinkedList. Чтобы превращать это в нормальные объекты надо писать какие-то конвертеры. В итоге кода становится сильно больше, модели дублируются, проект становится хрупче.

По всему коду размазано обращение к Realm: он импортируется в файл, передается в качестве параметра, из базы тянутся объекты. Мы активно заворачивали всё в репозитории, чтобы скрыть работу с базой, а интерфейсом выходил доменный объект. Но это дополнительный код и слой в архитектуру.

Работа с базой превратилась в целый слой, который надо поддерживать: писать маперы, обертки. Добавить новую сущность — это слишком много ручной работы: создать Entity, переложить из DTO в нее, потом из Entity в доменную модель. Это всё ещё и протестировать надо, а мы даже на UI выводить ничего не начали.

Realm-объект должен быть классом. Все его свойства надо пометить как динамичные, при этом нельзя их сделать немутабельными, а конструктор Entity не имеет смысла, он всегда пустой. В итоге легко добавить свойство, но забыть поставить его из всех нужных мест.

Realm — большая и очень тяжелая зависимость. Наш проект весил 55 Мб, Realm занимал 7 — и очень долго билдился. Мы решили проблему пребилдом — перенесли билд на этап pod install, стало реже и легче. Но плагин компиляции стал влиять и на другие поды, например, он не работал с XCFramework и мы не могли обновить поды, которые перешли на него. Убрать пребилд мы уже не могли, потому что привыкли к нормальной скорости сборки. 

Быстрый, простой, сложный: как мы выпилили Realm - 2

Ну и Realm мог бы и складывать свои файлы в одну папку!

По умолчанию Realm складывает всё в папку Documents
По умолчанию Realm складывает всё в папку Documents

Проблемы в проекте из-за недостатков

Это не критика Realm, а взгляд на то, к чему может привести недальновидность в начале разработки. 

Realm стал целым слоем обработки данных, все операции проходили через него. При этом, вся архитектура с бекапом в Realm не работает, если на девайсе мало места. Из-за размера фреймворка не получится переиспользовать код и написать, например, аппклипс: из 10 доступных мегабайт он займет все 10.

Страдает производительность. Обратные связи могут порождать очень большие и сложные деревья, сохранение и запись могут растягиваться. Мы столкнулись с этим в меню, когда появились изменяемые комбо. В комбо были слоты, каждый мог содержать десятки ссылок на продукты. При получении меню запись и чтение из базы занимало 2/3 времени: сетевой запрос проходил за полсекунды, а ещё одну мы просто разбирались с базой в приложении.

Офлайн-режим нашему приложению совсем не нужен, а персистентость нужна лишь частичная. Нет смысла хранить всю корзину, если можно обновить её с сервера по ID. Даже если захотим хранить её, то это проще сделать другим способом: на уровне кеша сети, ручным кешем оригинального JSON или конвертацией доменной модели в файл через Codable.

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

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

Сложно писать тесты. Непонятны зависимости, часто есть только одна, на базу. Что он из неё читает? Города, профиль, корзину? Иногда нужные записи находятся в нескольких таблицах, для теста мучительно ищешь их по дебагеру. Из интерфейса функции совершенно ничего не понятно:

// К каким таблицам пойдёт запись? От чего зависит работа функции?
public func saveOrder(_ order: Order, to realm: Realm) 

При обновлении Xcode каждый раз ломался CI. Обновление Realm его быстро чинило, но это лишние нервы каждый год.

Всё вместе это приводило к тому, что весь код вокруг Realm превращался в легаси:

  • его сложно рефакторить;

  • надо помнить про миграции;

  • могли быть неожиданные ошибки.

Это всё неприятно, но не критично: чуть больше кода, чуть меньше контроля, но работает.

Реально бесил лишь перформанс меню, но это можно было решить, стоило только сфокусироваться и попрофилировать. Коллеги на Android столкнулись с отсутствием каскадного удаления, но на iOS мы достаточно хорошо обработали это вручную, когда перед добавлением удаляли все прошлые объекты одного типа. Это же спасало и от разбухания базы.

Многие проблемы можно было решить инфраструктурно, но это только усложняло код. Например, чтобы не работать с объектами базы напрямую мы завели репозитории, которые конвертировали Realm Object в доменные объекты. Но это всё дополнительный код и усложнение.

При этом надо постоянно иметь дело с сырыми типами в Realm. Особенно сложно было с объектами большой вложенности, где для вложенных объектов тоже пришлось создавать репозитории, конвертеры и всё такое. Со всем этим легко промахнуться в перформансе: читаешь и конвертируешь огромную модель, а потом берешь от неё только одно свойство.

Почему решили удалить — две «последние капли»

Совсем плохо стало дважды: когда мы застряли на множественной миграции и когда случайно откатились на прошлую версию. 

Миграция. У нас была одна база, которую мы не разделили на обязательные данные и временный кеш, поэтому мигрировать приходилось вообще всё.

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

Разные пользователи оказались на разных этапах миграции. Мы получили пачку крешей, потратили неделю чтобы каждый день катить хотфиксы, которые пытаются правильно смигрировать данные разных версий. За пять фиксов идеально сделать не получилось, шестым мы просто снесли все миграции. 

Нам повезло, за месяц до этого мы перенесли самые критичные данные в UserDefaults. В худшем случае пользователям пришлось бы выбрать город второй раз, а авторизация и текущая корзина сохранялись и без миграций.

Ситуация всех вымотала, но мы чувствовали, что проблема была в нашем коде. Мы думали, что виноват не Realm, поэтому снова ничего не делали, но затаили обиду. 

Realm мигрирует без учета версии схемы. Могут быть сложности при повторном переименовании Property.

Откат. Через два месяца мы столкнулись в непонятным крешем «Realm accessed in incorrect thread». Это было очень странно, потому что мы были точно уверены, что работаем с потоком правильно: вся работа с базой велась строго в отдельном потоке. Креш случался в самых разных местах, стабильности не было. Искали его неделю: у нас был pull request на версию с ошибкой, мы отревьювили 700 файлов 3 раза, но не смогли найти проблему.

Миграций базы уже не было, поэтому в качестве быстрого решения мы откатились на прошлую версию приложения. Это была ошибка. С откатом всё стало только хуже: Realm не мог прочитать свой файл из-за разницы версий самого Realm. Повезло, что мы обновили только 1% пользователей и вовремя остановили. Откат обошелся в 3000 крешей. 

Креши нам показали место, на которое мы не обратили внимание: версия Realm обновилась, а откатом мы ее понизили. На устранение проблемы ушла ещё неделя: написали удаление файла Realm в случае проблем с его чтением, дождались ревью и раскатки до пользователей. 

Стало ясно, что так выкатывать приложение нельзя, каждый раз что-то случается, в этом каждый раз задействован Realm. Конечно, на ошибках мы учились, но так подставлять нельзя ни пользователей, ни бизнес. Каждый новый релиз стал восприниматься как смертельное решение, страшно было катнуть даже маленький фикс с переводами. Есть ли креши? Никто не знает, он рандомный: UI-тесты иногда показывали, а иногда и по 5 раз проходили без проблем. 

Команда устала, давление огромное, такое отношение к релизам терпеть нельзя. Стало понятно, что вылечить не получается, надо резать. 

Можно ли было решить каждую из проблем? Да, конечно. Можно ли было угадать что-то наперед? Наверно да, вопрос миграций стоял давно. Надо ли это было делать срочно? Нууу, риски были… но 3 года же нормально работало, что случится? 

Краткий итог критичных проблем:

  • проблемы с несколькими миграциями одного поля;

  • проблемы многопоточности в новой версии Realm.

Примечание. Забегая вперед скажу, что ошибку в Realm поправили в версии 5.3.5 20-го августа, а столкнулись мы 6-го. Фикс Realm вышел через две недели после наших проблем, но брейкинчедж появился 16 мая — проблему починили только спустя 3 месяца. Нам просто повезло, что мы не обновились раньше.

Как «продали» бизнесу удаление Realm

В итоге за 3 месяца мы трижды столкнулись с крупными проблемами на релизе, каждый раз с новыми. На починку суммарно ушло 20 дней. Это время кажется какой-то бесполезной антиработой. 

iOS команда не нашла аргументов за то, чтобы оставить Realm. При этом он мог нам заблокировать любой релиз новым неожиданным образом.

Увы, тут было не до продажи — просто поставили перед фактом.

Выпиливание Realm не та задача, которую можно сделать в фоне, да ещё и в конечный период. Пришлось ставить ультиматум, что релизить в таком состоянии мы не можем, надо остановить разработку на какое-то время и выпилить целый слой в приложении. За дело берутся все команды, на тот момент это было 4 iOS-разработчика. 

Быстрый, простой, сложный: как мы выпилили Realm - 4

Естественно, первый вопрос от бизнеса — на сколько времени останавливаемся. Ответ — примерно месяц. Офигели все.

План работ по сносу

Делать такую задачу без плана самоубийство. Надо составить план задач и отслеживать прогресс. 

Ключевые строки. Мы выписали ключевые строки, по которым можно отслеживать как много Realm используется в проекте. Это могло бы быть мерилом качества инкапсуляции Realm. Нашли 3300 мест. Погнали выпиливать.

Быстрый, простой, сложный: как мы выпилили Realm - 5

Но такая верхнеуровневая метрика не рассказывает о сложности работы, только её количество. 

Домены. Тогда мы выписали наши домены. За 3 года работы над приложением мы развязали домены, работать над ними можно было параллельно. Получилось так:

  • меню;

  • города и страны;

  • профиль;

  • адреса;

  • активные заказы;

  • корзина и детали заказа;

  • оценка заказа;

  • очередь синхронизации продуктов в корзине.

По каждому домену оценили сколько упоминаний их объектов, а потом всё сложили. Получилось 1500 мест.

Разделить работу оказалось удобно по доменам: одной команде один домен. Начали с самых больших и критичных: меню, корзина, активные заказы. 

Быстрый, простой, сложный: как мы выпилили Realm - 6

Чтобы дать оценку точнее, мы решили работать 5 дней в полную силу, оценить прогресс и сделать по нему прогноз на остаток. Такой план устроил всех, команды взялись за работу. 

Ревизии. Каждый день делали ревизию по количеству упоминаний, строили график нашей скорости. Дольше всего выпиливали Realm из меню, в нём было 26 видов объектов с 852 упоминаниями. Над ним работало 2 человека и потратили 112 человеко-часов.

Быстрый, простой, сложный: как мы выпилили Realm - 7

Многие домены «очистились» меньше чем за день. Это было шоком: мы пару лет бесились с того, как сложно работает корзина, а оказалось, что отказаться от кеширования в Realm можно за несколько часов.

Как удаляли

Простое удаление. Прозвучит странно, но где-то просто оказался лишний код. Например, в корзине у нас есть очередь из продуктов, которые ещё не были отправлены на сервер. Она нужна, чтобы не потерять продукт при сетевой ошибке. Мы сохраняли эту очередь в Realm, чтобы продукты не терялись даже между запусками. Хорошо, что это предусмотрено, но реальный шанс так потерять данные очень низкий. Для скорости выпиливания мы отказались от бэкапа корзины.

Замена объектов. Мы уже начали упрощать работу с Realm оборачивая его в абстракцию репозитория. План был такой: про способ хранения знает только репозиторий, а в приложение он отдает только доменные модели. Переписать успели процентов 30, это сильно помогло при удалении. При переписывании мы весь слой старых репозиториев с Realm заменяли на самописные репозитории, которые конвертировали структуры моделей через Codable и сохраняли в файл. Данных у нас не так много, способ подходит.

Самое сложное в таком случае правильно поменять Realm класс Entity на структуру: нужно поменять способ мутации объекта, ведь теперь он каждый раз копируется. Суммарно ошиблись пару раз в тех местах где тестов ещё не было. 

Обычно работы по замене выглядели так:

  • Убираем наследование от Object, убираем всё @objc dynamic декларации у property, меняем класс на структуру (если надо).

  • Меняем запросы к Realm на обращение в наш репозиторий.

  • Правим «мелочи»: тесты, доступ.

  • Чистим: меняем типы property с сырых на доменные. Больше никаких непонятных Int, только Enum.

Переписывание. Какие-то части было проще переписать — слишком много завязано на логику самого Realm. Так случилось с меню. Домен большой, но у нас было написано довольно много тестов на него, поэтому можно было переписать логику по старым тестам, временами ещё и удаляя легаси.

Ещё проблемы, которые нашли

В процессе выпиливания обнаружили несколько проблем, которые были так или иначе связаны с Realm.

Адреса. Они состоят из 3-х слоев: объект адреса, набор полей, которые его описывают, у каждого поля есть его тип. Например: нужна улица, её значение Ленина и она часть адреса Ленина 25. Простая система, но из-за обратных ссылок в коде можно было ходить по вложенности в любом порядке: не только 1-2-3, но и 1-2-1-2-3-2. Это сильно усложняло код. Написали тесты, поменяли структуру моделей, отрефакторили, теперь можно двигаться только в одном направлении 1-2-3 — читать стало проще.

Города. В нашем домене встречаются две модели городов:

  • короткая — нужна только для списка городов на старте приложения;

  • полная, которая подгружается после того, как выбрали город и нужна для работы приложения. 

Оказалось, что в Realm они были описаны одной полной моделью, а данные могли как оказаться в ней, так и нет. При этом приложение стартовало с простой модели, а потом докачивало данные. Естественно, могло и не докачать, и приложение бы работало как есть. 

На проблемы временами натыкались, но понять причину сходу не смогли. Найти такой баг можно было только зная весь домен городов и держа всю схему их обновления в голове. Так как задач про города почти не было, то домен все представляли плохо и до решения проблемы добраться не получалось. 

При выпиливании мы разделили модели на две и сделали для них нормальный жизненный цикл. Теперь модель города скачивается синхронно перед меню и приложение всегда стартует со всеми нужными данными. 

Пауза: фидбек и переоценка сроков

После недели интенсивного рефакторинга мы взяли паузу: стабилизировали релиз, начали брать бизнес-задачи. За неделю мы сделали очень много — по доменам упоминание снизилось на 60%. В итоге у нас осталось 3 несвязанных домена: города, оценка заказа и очередь продуктов в корзине. 

Релизить всё ещё было страшно, но мы были вдохновлены результатами и даже выпустили релиз.

Быстрый, простой, сложный: как мы выпилили Realm - 8

С новым XCode мы получили новые проблемы с Realm, но и новые пути решений у нас тоже были:

Быстрый, простой, сложный: как мы выпилили Realm - 9

Как нам казалось, до конца проекта оставалось пару недель, поэтому 2 из 3-х команд начали брать бизнес-задачи, а одна продолжила рефакторить проект. 

Не так страшны первые 90% рефакторинга, как вторые 90%

В последнюю очередь мы меняли логику для городов. В городах всё оказалось сильно сложнее:

  • На старте приложения мы берем города из JSON в бандле приложения, чтобы можно было показать список городов без задержек и как можно быстрее перейти к меню.

  • Этот список обновляется на старте, новый список надо сохранять для следующего раза. Сохранить в бандл мы не можем, надо хранить отдельно. 

  • После выбора города скачивалась полная модель — она хранится отдельно. Приложение должно работать только с ней.

  • Информацию о городе получали в десятки мест приложения, каждый раз по ID с опциональным результатом. Это всё надо было унифицировать, а потом понять, что мы ничего не сломали. На всё подряд тесты не напишешь, нужен другой подход.

Мы вынесли весь код городов в отдельный фреймворк, чтобы решить все проблемы за раз. У фреймворка появился контракт, которым он общается с приложением, а вся работа происходит внутри. Простота контракта уменьшила риски, и для этого уже можно было писать разные автотесты. 

Быстрый, простой, сложный: как мы выпилили Realm - 10

Флоу получился такой: мигрируем данные, пользователь выбирает город, докачиваем детали города, кешируем и показываем меню. Внутри есть особенности для разных стран, но сейчас они для нас не важны, да и всё скрыто внутри фреймворка. 

Этот этап оказался сложным технически и занял пару месяцев работы. Сам Realm удалился за неделю, в остальном мы рефакторили и выносили код во фреймворк, чтобы однозначно сказать, что всё работает правильно. 

Раньше в приложении много где дублировался код:

  • Взять текущий идентификатор города.

  • Получить запись из базы по идентификатору.

  • Взять первый объект в ответе, это считаем текущим городом. 

  • Повторить в каждом месте.

Надежность перешла на уровень зависимостей между модулями. У приложения всегда есть текущий город, он строго один. Если города нет, то его надо выбрать пользователю (или получить от сервера при миграции) и только потом стартовать основную часть приложения.  

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

Примечание. О модульности и тестах напишу как-нибудь потом, подписывайтесь на канал.

Миграция

В самом конце нас ждала интересная задача: нужно смигрировать данные так, чтобы ничего не потерять. Но при этом не тащить с собой Realm, чтобы читать из него старые данные. 

В этом нам сильно повезло: когда мы отказались от миграций в Realm, мы перенесли все нужные для работы ID в UserDefaults. Мы знали ID корзины или выбранного города, поэтому на старте нужно было только получить новые данные от API. 

Если бы критичные данные были только в Realm, то пришлось бы удалять базу в несколько апдейтов сильно растягивая переход по времени. Или вообще просить пользователя выбрать город ещё раз.

Храните критичные ID вне базы — пригодятся.

Механизм миграции пригодился и для UI-тестов: можно пропустить выбор городов если сразу передать правильный ID. За счет миграции мы получим все нужные данные с сервера и сразу покажем меню, пропуская выбор страны и города.

Быстрый, простой, сложный: как мы выпилили Realm - 11

Чистка после Realm

С новой версией в приложении нет Realm, но у пользователей оставалась старая база. Мы подчистили за собой, чтобы не занимать место. Этот код останется с нами надолго.

/// Давным давно, когда API был не очень, мы использовали Realm: собирали все ответы в одном базе, а потом читали из неё.
/// Больше такой фигни нет и мы всё аккуратно раскладываем по репозиториям.
/// Теперь на месте Realm вот такой маленький шрам, для того чтобы очистить старых клиентов.
/// Удали этот код, если читаешь это в 2022 году.
internal final class RealmCleaner {
    let fileManager = FileManager.default
 
    /// Remove all realm files
    /// - Returns: total size of removed files
    func removeRealmFiles() {
        let pathes = filePathes()
        fileManager.removeItems(at: pathes)
    }
 
    private func filePathes() -> [URL] {
        let baseURL = fileManager.documentsDirectory().appendingPathComponent("default.realm")
        let realmURLs = [
            baseURL,
            baseURL.appendingPathExtension("lock"),
            baseURL.appendingPathExtension("note"),
            baseURL.appendingPathExtension("management"),
            baseURL.appendingPathExtension("log_a"),
            baseURL.appendingPathExtension("log_b")
        ]
 
        return realmURLs
    }
}

Мы замерили размер удаляемых файлов: в основном меньше 15 МБ, но было и несколько пользователей с размером в 150 и даже 300 МБ. И это не девайсы тестировщиков. 

Новое хранилище

Какие-то данные всё равно хочется хранить. Мы уже избавились от Realm-объектов, перевели все на доменные. Хочется использовать их так, чтобы больше не надо было конвертировать из одного типа в другой только для хранения. Core Data таким образом тоже не подходит. 

Мы собрали требования к хранению:

  • Хотим работать с доменным объектами.

  • Умеет работать с разным количеством объектов: хранит как один объект для типа (профиль пользователя может быть только один), так и коллекцию (список из городов).

  • Хранить можно в памяти или с кешем на диск. Приложение должно работать даже если на диске нет места. Кеш на диске опционален. 

  • Для кеша на диск готовы запариться чуть больше, если надо. 

  • Объемы данных всегда небольшие (меньше мегабайта) и слабо связанные — реляционная БД не нужна.

  • Допускаем, что каждый фреймворк приложения может хранить данные по-своему. Для сообщений база данных нужна, для кеша городов точно нет, а что-то лежит в UserDefaults. Но стандартизируем подходы по возможности.

Мы разделили способ хранения и количество объектов, которое можно хранить. В объявлении репозитория видно ключевые части: 

public class ProfileRepository: SingleRepository<ProfileModel> {
    public init() {
        super.init(storage: InMemoryStorageWithFilePersistance().toAny())
    }
}
  • SingleRepository хранит один объект.

  • Хранит только модель ProfileModel.

  • Хранит объект в памяти и кеширует на диск.

  • Ещё есть InMemoryStorage и FileStorage. Для хранения на диске модель должна реализовать протокол Codable, а для хранения в памяти это не нужно. Для доменной модели это вполне подходит и легко поддерживать. Теперь отдельную модель для записи в базе создавать не нужно. 

Коллекция пиццерий хранится в CollectionRepository: синтаксис похож, только наследуемся от другого класса. 

public class PizzeriaRepository: CollectionRepository<PizzeriaModel> {
    public init() {
        super.init(storage: InMemoryStorageWithFilePersistance().toAny())
    }
}

Примечание. Про устройство рассказывать долго: там и box typing, и работа с асинхронностью. Пишите в комментарии, если интересно узнать как работает внутри.

Конечно, есть и минус: механизм для миграций на уровне данных мы пока не писали, но и потребности не возникло. Зато, можно будет легко заводить хранилища разных версий для прогона тестов миграций. 

Мониторинг

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

Запросы во время миграции. Мы пропустили, что во время миграции может восстановиться push-токен от Firebase и мы отправим его в наше API. Хедеры запроса зависят от текущей страны, а она в процессе миграции. Запрос не проходил, возник фон некритичных ошибок.

Падение профиля. Он падал при одновременном чтении и записи из разных потоков. Мы не сталкивались с этим при разработке, а он оказался единственным классом среди всех моделей в репозиториях, остальные были структурами. Переделали на структуру, проблемы с одновременным доступом ушли. 

Крешрейт к Новому годы мы довели до 99.95%. Можно улучшать ещё, ведь теперь креши не в рандомных местах Realm, а только в нашем продукте и понятно как их чинить.

Результаты

Баги. После выпиливания осталось несколько небольших багов, где домены оказались связаны. С бизнесом решили что это нормально и положили в роадмап. Например, сейчас мы не подписываем удаленные ингредиенты в истории заказов, потому что для названия надо сходить в домен меню. Поправим контракт в API, снова будем получать, тогда и покажем. 

Домены. От изначальной проблемы связанных доменов почти ничего не осталось: всё работает независимо, мы активно разделяем приложение на фреймворки. Работать с такими модулями удобно: быстро компилируются, мало зависимостей и связей, понятная ответственность, легче тестировать. Можно даже из одного модуля создать отдельное приложение-витрину и написать для него UI-тесты. 

Сделали первый полный фича-фреймворк. Мы давно, но не спеша занимаемся распилом, только сейчас получилось дожать целый модуль. Стало понятней как строить архитектуру остальных фича-фреймворков. 

Релизы. В период выпиливания почти не получалось релизить. Последний этап с рефакторингом затянулся, добавилось пару больших задач от бизнеса и релиз получился очень большим. На релизе особо ничего не произошло, но было некомфортно.

Объём. Приложение уменьшилось на 8 МБ от Realm, запустили процесс по ревизии размера и уменьшили ещё на 10 МБ за счет бандла. Начали трекать размер приложения при каждом релизе.

Сроки. От начала проекта до полного выпиливания прошло 3,5 месяца, но все команды остановились только на одну неделю. В остальное время разработка продолжалась. 

Потоки. В середине августа у Realm вышло обновление, которое полностью починило нашу проблему с потоками. Нам повезло, что мы натолкнулись на проблему в июле, уже перед фиксом, но впервые она появилась в мае. Получается, что на исправление у ребят ушло несколько релизов и 3 месяца работы. Всё это время понять статус фикса невозможно: вроде чинят, а оно всё равно вылетает. 

Стало проще. Мы нашли старые баги и заревьювили несколько архитектурных задач. Работать стало проще. Теперь в приложении всегда можно быть уверенным, что город имеет все данные и не нужно возиться с опциональностью. Стало проще разрабатывать: у нас получились нормальные доменные объекты, удалили целый слой приложения и маперы из одного в другое, кое-где переосмыслили архитектуру. Упростилось деление приложения на фреймворки, можно идти в сторону аппклипс. Домен городов уехал в отдельный фреймворк. 

Блокировки. Перестали блокироваться релизами Realm при обновлении Xcode, смогли обновить Cocoapods и поды на XCFramework. 


У каждой зависимости своя цена и её надо прикидывать с самого начала. Нормально стартануть в типовой зависимостью, но надо иметь план на то, как выпиливать, как менять и как это поддерживать.

Realm сложный инструмент и его надо уметь обслуживать. Простота интеграции бывает обманчива.

Быстрый, простой, сложный: как мы выпилили Realm - 12

Больше новостей про разработку в Додо Пицце я пишу в канале Dodo Pizza Mobile. Также подписывайтесь на чат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы.

Автор: Рубанов Михаил

Источник

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js