Разработка
Рефакторинг кодовой базы в Slack: Стабилизация, Модуляризация и Модернизация
В целом, статья Slack является отличным уроком для многих в сообществе мобильных разработчиков по масштабированию мобильной разработки.
Недавно Slack поделился своей историей о том, как они улучшают свою устаревшую кодовую базу мобильных приложений. Интересное чтение на полчаса.
- Стабилизация базы мобильного кода — 16 минут
- Модуляризация базы мобильного кода — 13 минут
- Модернизация мобильной кодовой базы — 13 минут
Я резюмирую свои выводы в этой статье.
Зачем нужно было серьезное улучшение кодовой базы?
Код писался с 2015 года на Android и с 2013 года на iOS. На момент, когда команда занялась улучшением кода (в 2020 году), ему исполнилось 5 и 7 лет соответственно. Капитальная переделка была необходима, потому что:
- код в основном оставался монолитным, а набор функций рос экспоненциально, как и команда, а методы работы не масштабировались.
- код имел широко распространенную несогласованность и устаревшие шаблоны с отсутствием разделения проблем.
- появились новые технологии как для Android (Kotlin, корутины и т.д.), так и для iOS (Swift).
- было множество миграций, на которых терялась скорость киосков (например, с Obj-C на Swift, AutoValueGson на Android вместо Kotlin Moshi)
- сложность влияла на дорожную карту продукта
- команда увидела значительные улучшения в своем веб-приложении после его переработки
Что можно было сделать
- Полностью переписать? Это слишком рискованно, так как многие основные функции все еще не были затронуты.
- Использовать кроссплатформенный подход (React Native, Flutter)? Пробовали раньше и сталкивались с различными проблемами, например, производительностью.
- Решил использовать многоэтапный подход к рефакторингу — стабилизировать, выделить модули и модернизировать.
Как это было спланировано
Чтобы все это заработало, нужно было не просто начать. Нужно было предпринять шаги, чтобы спланировать все:
- Они назвали проект Project Duplo. Именование важно, чтобы проект можно было легко идентифицировать.
- Начали со списка намеченных целей. Нужно знать, какова цель проекта.
- Некоторые отдельные участники возглавили инициативы по раннему изучению возможностей и составлению более подробных планов.
- Поделились этим с заинтересованными сторонами, которые станут все реализовывать, учитывая, что это потребует времени и инвестиций (компромисса с некоторыми другими вещами). Сообщили им, что они получат взамен.
- Четко расписали, что нужно решить, как измерить прогресс и каков риск с планом смягчения последствий.
Чего они хотели достичь
- Им нужно было позволить разработчикам работать независимо.
- Им нужно было более короткое время сборки для инкрементальных изменений.
- Хотелось более современных технологий разработки.
- Требовалось больше согласованности в кодовой базе.
- Хотелось, чтобы это работало в течение следующих 5 лет
Фаза стабилизации
Избавление от наихудшего технического долга и завершение важных миграций. Общая продолжительность — 6 месяцев.
iOS
- Перенос оставшегося кода Obj-C на Swift, чтобы обеспечить более согласованную кодовую базу. Это также позволило им позже использовать такие функции, как, например, Combine.
- Завершение миграции библиотек доступа к данным — чтобы обернуть CoreData и избежать прямого доступа к ней.
- Завершение миграции на нативную сетевую библиотеку — чтобы в кодовой базе не использовался другой подход (сторонняя библиотека) и чтобы сделать все более согласованным.
Android
- Разделение монолитного API сетевых вызовов по функциональным областям — это позволило выделить интерфейсы для нужд отдельных функций.
- Разделение монолитного интерфейса доступа к базе данных на несколько объектов доступа к данным (DAO).
- Перенос оставшихся запросов к базе данных в SQLDelight — это также помогло сделать все более согласованным.
- Стандартизация шаблона Репозиторий для доступа к сетевым/дисковым данным — это также сделает вещи более согласованными.
- Удаление оставшегося использования шины событий Otto — замена его на RxJava, который лучше работает.
Метрики
- Отображение процента устаревшего кода в базе с течением времени
- Отображение количества строк Obj-C кода с течением времени
Фаза модуляризации
Разделение целевого приложения на более мелкие компоненты, чтобы уменьшить взаимозависимости, сократить время сборки и обеспечить независимую разработку.
Очевидно, что это самая важная часть улучшения кодовой базы для ускорения разработки, как написала команда Slack:
Никакая другая часть проекта, кроме модульности, не оказала большего влияния на наши цели по ускорению мобильной разработки и очистке нашей кодовой базы.
Весь этап Модульности совместно с Модернизацией занял в общей сложности год.
Преимущества модульного подхода
- Более быстрое время компиляции как для локальной версии, так и для непрерывной интеграции.
- Разработчики могут самостоятельно создавать и тестировать свой код.
- Меньшая кодовая база для работы, повышение скорости доставки
- Более четкое владение и разделение ответственности.
- Сведен к минимуму бесхозный код и распутаны зависимости.
- Легче отслеживать ход работы над каждой фияей.
Модульный подход
Разбит на 3 разные категории
- Модули функций — это функции, ориентированные на пользователя (связанные с пользовательским интерфейсом). Интерфейс и реализация разделены для упрощения связи между функциями.
- Сервисные модули — это обычная логика, не связанная с пользовательским интерфейсом. Интерфейс и реализация разделены для упрощения связи между службами.
- Модули библиотеки — это структура данных и утилиты. Реализация и интерфейсы здесь общие, следовательно, между библиотечными модулями нет ссылок.
Инструменты
Для успешной модуляризации более важными являются инструменты. Привлекли Команду опыта разработчиков (Developer Experience Team) для поддержки необходимого тулинга.
- iOS перешел с инструмента Xcode CI на использование Bazel
- Android использует Gradle и функцию кэширования конфигурации в стадии разработки
- Появился сценария генерации кода для создания шаблонов новых модулей, создания моделей CoreData и флагов функций
- Начали использовать функцию Anvil от Square, чтобы улучшить производительность Dagger DI благодаря умному использованию Anvil плагинов компилятора Kotlin
- Провели миграция с KAPT на KSP, где Anvil также помог снизить потребность в KAPT. Подробнее об использовании Anvil здесь.
- Удалили AutoValue и AutoValueGson в пользу классов данных Kotlin, Moshi и Moshi-IR (экспериментально).
- Начали использовали Gradle Enterprise не только в качестве распределенного кэша сборки, но и для проверки того, где разработчики сталкиваются с задержками.
Фаза модернизации
На этом этапе в Slack внедряли более перспективные технологии и шаблоны проектирования, чтобы поддерживать кодовую базу в соответствии с отраслевыми тенденциями и подготовиться к другим технологиям, которые могут быть интересны в будущем.
Совместно с модульностью процесс занял 1 год.
Модернизация iOS
- Переход от функциональной архитектуры MVVM+C к проприетарной VIPER — использование Swift с дженериками и шаблонами для создания базовой реализации.
- Наложение более строгого линтинга поверх уже работающего Swiftlint при каждом коммите, чтобы гарантировать отсутствие анти-шаблонов, чтобы избегать глобальных синглетонов, использовать Combine и использовать проприетарный SlackKit вместо компонентов UIKit.
- Обеспечение внедрения реактивной структуры Apple Combine вместо других, например, RxSwift, что готовит команду к будущему внедрению SwiftUI.
- Расширение использования SlackKit для компонентов пользовательского интерфейса вместо использования нативных компонентов пользовательского интерфейса iOS (на данный момент UIKit и SwiftUI в будущем).
Модернизация Android
- Замена Gson на Moshi, переход к сериализации Kotlin.
- Замена Android Priority Job Queue на WorkManager из-за устаревания APJQ.
- Замена сетевых вызовов API внутренним проектом под названием Guinness, созданным с использованием Retrofit, Moshi, Okio и OkHttp. Исходный код большей части фрагментов Guinness находится в открытом доступе под именем EitherNet.
- Внедрение корутин в качестве еще одного варианта RxJava, но без принуждения к их глобальному внедрению.
- Эксперименты с Jetpack Compose, так как он все еще стабилизируется. Поверьте, он будет определять будущее.
Результаты, достижения
В целом команда добилась того, к чему стремилась, за 1.5 года. Хотя проект еще не считается завершенным (и я думаю, что никогда не будет завершенным), он многого достиг:
- модульность кодовой базы достигает 81% для iOS и 92% для Android, модернизирована большая часть кодовой базы — 68% для iOS
- добавлено больше модулей — 280 для iOS и 330 для Android
- улучшено время CI для тестов слияния, сокращено время тестирования на 63% для iOS и на 30% для Android
- развертывание прототипирования на мобильных устройствах происходит быстрее
- улучшение общего настроения разработчиков
Появившиеся проблемы и что нужно решить
Несмотря на множество преимуществ, есть некоторые проблемы.
Более длительное время локальной сборки
Это привело к увеличению времени локальной сборки из-за Bazel. Однако этому противодействуют с помощью
- Лучший машин M1 MacBook Pro
- Продолжающихся улучшений развертывания Bazel
Сложные отношения между модулями
С модулями «Фичи», «Сервисы» и «Библиотеки» все еще недостаточно проработано, поскольку Фичи зависят от Фич, а Сервисы зависят от Сервисов, что может привести к более глубоким иерархическим и циклическим отношениям. Требуется дополнительная доработка.
Для Android необходимо оптимизировать слишком много межмодульных зависимостей, так как это замедляет компиляцию. Необходимо разделить все модули на модули API и реализации, чтобы распараллелить компиляцию проекта.
Задача внедрения зависимостей
Зависимости должны проходить через множество уровней кода, чтобы определить, какие зависимости используются на самом деле.
С модульностью это теперь обрабатывается с помощью стандартных инъекций инициализаторов, которые:
- Проще использовать и которые знакомы разработчикам
- Есть гарантированная доступность зависимостей во время компиляции
- Они избегают глобального состояния или синглтонов
Кроме того, разработчики изучают Needle (Dagger для Swift) в качестве платформы зависимостей, чтобы модули могли явно определять свои зависимости и создавать их без необходимости связывать все реализации в целевом приложении.
Нужно больше улучшений в кодовой базе Android
- Продолжается миграция на Kotlin, в настоящее время 92% кодовой базы на нем
- Большее принятие Jetpack Compose по мере улучшения производительности
- Принятие корутин Kotlin для асинхронной обработки
Мой личный взгляд
Инициатива Slack по рефакторингу действительно хорошо скоординирована с желаемым результатом. Здорово, что управленческая команда оказывает большую поддержку такой инициативе и позволяет всем разработчикам участвовать если не в большей части этого проекта Duplo, то хотя бы частично.
Что мне действительно нравится в инициативе:
- Разделение монолита на модульные части позволяет сократить время сборки, улучшить владение, и каждая команда может работать независимо.
- Нацеливание на модернизацию кодовой базы с помощью новейшего стека технологий, например, перенос Obj-C на Swift и Java на Kotlin, использование Combine и корутин и т.д.
- Целенаправленное участие некоторых членов команды, а также вклад всех разработчиков в инициативу с помощью команды Developer Experience.
- Различные измеримые показатели результативности, например, улучшение времени CI, % принятия, настроение разработчиков и т.д.
Вещи, которые, по моему мнению, могли бы быть лучше (с оговоркой, что это всего лишь моя личная ограниченная точка зрения):
- Миграция выполняется слишком рано, до того, как SwiftUI и Jetpack Compose будут признаны полезными для команды. В ближайшие 2–3 года мобильный мир будет полон SwiftUI и Jetpack Compose, поэтому команда Slack, планирующая, что инициатива Duplo продлится 5 лет, может и ошибиться. Возможно, с помощью SlackKit они смогут скрыть влияние этих изменений.
- Сотни модулей и многоуровневая модульная структура могут усложнить ситуацию в будущем. Для обеспечения масштабируемости в долгосрочной перспективе может потребоваться более точное разделение модулей, позволяющее избежать непрерывного роста их количества.
- Инициатива iOS и Android выполняется относительно по-разному, хотя фазы высокого уровня (стабилизация, модульность и модернизация) считаются одинаковыми. Возможно, на этапе модульности могут быть общие библиотеки или службы, которые можно использовать на обеих платформах.
- Наиболее предпочтительное согласование шаблонов между командами может ограничить модульную команду в большей автономии, в изучении новых шаблонов и использовании шаблонов, которые лучше подходят для конкретного функционального модуля. Принудительное выравнивание — это балансировка, когда чрезмерное выравнивание может задушить эволюционное развитие базы кода.
В целом, статья Slack является отличным уроком для многих в сообществе мобильных разработчиков по масштабированию мобильной разработки. Большое спасибо команде Slack за то, что поделились ей! Отличный прогресс и так держать!