company_banner

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

    Когда на собеседованиях я спрашиваю у кандидата с какими базами он работал и что делал, то чаще всего слышу про 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 мог бы и складывать свои файлы в одну папку!

    По умолчанию 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 используется в проекте. Это могло бы быть мерилом качества инкапсуляции Realm. Нашли 3300 мест. Погнали выпиливать.

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

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

    • меню;

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

    • профиль;

    • адреса;

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

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

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

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

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

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

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

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

    Многие домены «очистились» меньше чем за день. Это было шоком: мы пару лет бесились с того, как сложно работает корзина, а оказалось, что отказаться от кеширования в 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 несвязанных домена: города, оценка заказа и очередь продуктов в корзине. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Миграция

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

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

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

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

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

    Чистка после 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 сложный инструмент и его надо уметь обслуживать. Простота интеграции бывает обманчива.

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

    Интересное по теме:

    Как заблокировать приложение с помощью runBlocking. Актуально для Андроид-разработки.
    — О том, над чем в целом мы тут работаем: монолит, монолит, опять монолит.
    — Кратко об истории Open Source — просто развлечься (да и статья хорошая).

    А если хочешь присоединиться к нам в Dodo Engineering, то будем рады  — сейчас у нас открыты вакансии для iOS-разработчиков (а ещё для Android, frontend, SRE и других). Присоединяйся, будем рады!

    Dodo Engineering
    О том, как разработчики строят IT в Dodo

    Комментарии 46

      +12
      Шикарная статья, с реальной проблемой и в довольно редком формате — «Удаление технологии из проекта». Когда обычно пишут про то как втащить в проект новую свистелку.

      Пожалуйста, продолжайте писать про реальные кейсы.
        +3

        Спасибо, писать буду :-)

          0
          Плюсую!
            0
            уже вторая статья за последние дни, как выпиливают Realm)) Но реально круто смотреть, как к такому процессу подходят системно.
            +2
            В очередной раз задумаешься – стоит ли тащить в проект third party библиотеки
              +1
              Практически любой относительно крупный проект состоит из «third party библиотек». Это нормально.
              Надо просто оценивать риски и понимать цену этого, так как есть часть кода, которая не контролируется как свой.
                –1
                Он может состоять из них только по большой глупости. Как раз таки в большие проекты затаскивать сторонние библиотеки очень рискованно, при этом оправданность из затаскивания будет чрезвычайно мала. Ну, либо, как писал автор статьи — надо сразу закладывать время на их выпил
                  0
                  Похоже, мы совсем о разном.
                  Если писать всё самому, то такой проект имеет риск никогда не запуститься из-за высокой сложности.
                  Да и до какого уровня можно спуститься? Взять работу с БД. Отказаться от библиотеки и напрямую работать с sqlite файлами или полностью писать свою реализацию протокола работы с MySQL?
                    –1

                    sqlite – это уже и есть библиотека

                      0
                      Не совсем, это главным образом БД. Просто так случилось, что библиотека для работы с ней так же называется.
                      Но главное не это, главное — что это «3rd party». Затаскивать ли в свой проект стороннюю библиотеку?
                        0

                        sqlite уже затащен в iOS с самой первой версии, нужно только импортировать sqlite.h

                          0
                          Верно. Там много что уже «затащено», но всё равно это «3rd party». Я об этом.
              0

              Тоже бодаемся с реалмом. Удалить его есть задача. Но никак руки не доходят.

                0
                на что планируете заменить, если не секрет?
                  0

                  Пока это серьезно не обсуждалось. В нашем случае нам сперва тоже все не связанное напрямую с хранилищем надо перетащить в UserDefaults. А для остальных случаем нам подойдет вполне просто база данных закрытая каким нить Repository паттерном.

                +2
                RX-ы будет выпиливать сложнее…
                  +3

                  Не добавляли, поэтому и выпиливать не придется

                    +1
                    К слову, в Rx проблема совсем другого характера — сам фреймворк весьма стабилен (за все несколько лет, что я с ним работал, столкнулся с проблемой только в Rx 1 — refCount в нескольких тредах могли устроить deadlock), и единственным камнем преткновения может стать его сложность для новых разработчиков. Ну и дикая кривая сложности написания своих операторов, которые работают с Backpressure (но это весьма специфичный случай).
                      0
                      Просто не надо делать Rx-лапшу, тогда и выпиливать не прийдётся. Используем Rx только в имплементации сервисов/вьюх. Наружу в протоколах Rx не торчит. Так что выпилить или заменить на тот же Combine задача на один день.
                      +2

                      Сколько лет работаю с Realm одно удовольствие. Каскадное удаление уже давно завезли кстати. Это правда что есть много нюансов и легко выстрелить в ногу, но когда имеешь хороший опыт не вижу проблем. У Room их намного больше...

                        0

                        Вполне может быть.


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

                          0
                          А с какой версии завезли каскадное удаление? Недавно пытался искать инфу, и натыкался только на то, что «нет и пока не предвидтся»
                            0
                            посмотрите Embedded objects, это и есть каскадное удаление.
                            Версию посмотрите от 10.0.0 и выше.
                            0
                            Согласен, большинство минусов Realm (из начала статьи) точно так же применимы к CoreData. Статью можно было назвать «Как мы отказались от БД в пользу UserDefaults и сериализации JSON в файлы». Но за рефакторинг подкреплённый тестами и метриками :like:
                            +1
                            Спасибо, очень интересная статья!
                            Особенно методология и организация работ по рефакторингу.

                            Я так понимаю что ошибку с миграций (которая была первым звоночком) не получилось поймать в CI тестах на апдейт приложения, поскольку она случалась не между текущей и предыдущей версией а между текущей и какой-то древней?
                              0

                              Ага, там разница времени была года полтора

                              +2
                              Года четыре-пять назад, помню, ходила шутка: «Realm используют те, кому лень разбираться с CoreData», он позиционировался чуть ли не как серебряная пуля на все проблемы с хранилищем в iOS. На деле уже тогда, тем кто знал Core Data было понятно, что невозможно «просто» решить комплексные вещи.
                                0

                                Прям с языка снял!

                                0
                                После аутсорсеров всегда переписывать приходится.
                                  0

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

                                    0

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

                                    0
                                    Крутая статья, спасибо, прочитал с удовольствием)

                                    Но мне кажется есть какая-то излишняя щепетильность.
                                    Типа камон, это просто приложение чтоб заказать пиццу, без обид.
                                    Тут нет важных данных. Это не ПО для жизнеобеспечения)
                                    Если бы приложение раз в год предложило условно снова выбрать город я бы глазом не моргнул. Думаю как и остальные люди.
                                    Человек не обновлялся полтора года и тут обновился. Вам так сильно важно чтоб у этого плюс минус единственного человека все сработало идеально? Стерли бы все к чертям и дело с концом, пусть на чистую все создается грузится
                                      0

                                      Человек может заказывать и несколько раз в месяц. Почему у него буквально между двумя запусками должны пропасть какие-то настройки?

                                        0
                                        Ну, не каждый же день реалм выпиливаете из проекта)
                                        И естественно я не о чем то критичном, типа слетевшей авторизации.

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

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

                                          Включите автобновление на телефоне :-)

                                            0
                                            Люблю почитать чейнджлоги)
                                            И ненавижу ежедневные апдейты с шаблонным тестом :(
                                              0
                                              Раз уж разговариваю с таким важным человеком
                                              Позвольте немножко фидбека оставить
                                              Статья заставила установить ваше приложение на телефон (купил новый пару месяцев назад, а так стояло)

                                              Первый запуск.

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

                                              Тыкаю в серч контроллер у него сразу отступ появляется и он съезжает вниз. Скролом не возвращается на место :) Наверное как-то связано с largeTitle

                                              Кстати истории заказов почему-то нет. Они хранятся определенное время? Вполне возможно что заказывал года полтора назад

                                              На главной банер с акцией по Apple Pay
                                              По нажатию всплывает карточка с описанием и кнопкой показать.
                                              Кнопка просто скрывает карточку. Немного сбивает с толку. Что я должен был увидеть то :)

                                              У всех «питательных» товаров кнопка i в карточке продукта показывает всплывающее окно с пищевой ценностью. Но есть товары типа «Додо Клюв»
                                              И эта кнопка не делает ничего. Так если нет доп. информации может стоит кнопку скрыть?

                                              Карточки с пиццами мое почтение. Очень красиво и прикольно :)
                                              Особенно та где половинки выбирать
                                      0
                                      А удалось посчитать реальную разницу в размере кода? Как я понял, одним из недостатков Realm был увеличенный объем. Realm убрали, заменили репозиториями — и какой эффект в этом отношении получился в числах? Скажем, убрали 1000 строк кода, а добавили 10. Не считали?
                                        0

                                        Рилм занимал 8 МБ кода.


                                        По самому проекту значительной разницы нет в размере, пару тысяч строк наверно. А вот в чистоте и типизированности кода стало значительно лучше

                                          0
                                          Да, меня интересовала разница в количестве кода исключительно, не объем бандла.
                                        0
                                        Еще относительно UserDefaults. Как решили вопрос миграций в них?
                                          0

                                          Храним в файле, в UserDefaults только пара айдишников.


                                          Пока не мегрировали, но как предполагаю может быть.


                                          1. Заводим новую модель, старую оставляем рядом.
                                          2. Даем репозиторию пачку модейле что он может прочитать.
                                          3. Если не смог самую новую прочитать, то читает предыдущую. Если смог, то должна вызваться функция миграции из одной модели в другую. Все типизировано, легко тестировать.
                                          4. После миграции сохраняет данные.

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

                                            0
                                            Понятно. Вопрос появился из-за того, что проблема с миграциями — не от их наличия, а от обращения с ними. Сам-то инструмент полезный. Собственно, если переехать на UserDefaults — то вопрос с миграциями останется. Вот только, по моему мнению, он проигрывает Realm: если у Realm миграции предусмотрены штатно, то в UserDefaults их к тому же и нет (то есть проблем становится больше: и завести, и правильно использовать).
                                            Ну и, кроме того, Realm имеет настройку удалять БД при миграциях: можно их не писать, а просто чистить базу. Как я понял из ответа, примерно к тому же и пришли в UserDefaults.
                                              0

                                              Можно чистить при миграции.


                                              А еще откат версии надо обрабатывать в другом месте, если файл не прочитался.


                                              А еще если места на диске нет. Много этих «еще»

                                                0
                                                Я в данном случае не спорю и не высказываю свое мнение ни в одну из сторон, цель — разобрать с непредвзятой точки зрения плюсы и минусы обоих способов. Если беспристрастно — отсутствие места на диске аналогично разрушительно и для Realm, и для UserDefaults.
                                                А вот про «много еще» было бы интересно послушать — правда, желательно, с обеих сторон. Если это возможно, конечно. На этом примере открывается одна из сравнительно редких возможностей решить одну задачу двумя разными способами и сравнить их на практике. Как правило бывает, что разработчики выбирают одно из решений, реализуют его. И видят все плюсы/минусы полученного решения, а плюсы/минусы других решений можно оценить только умозрительно. А в вашем случае можно сесть, составить таблицу параметров и расставить оценки кандидатам. Получится объективный счет.

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое