Привет-привет! Не будем мять бока и начнем максимально быстро.
Но для начала представлюсь. Меня зовут Таскаев Евгений — я Android-разработчик в фичевой команде hh.ru. Пилю всякие интересные фичи, которыми вы пользуетесь каждый день*.
В статье я расскажу, как у нас в Android-приложении прошел глобальный ренейминг фичей и пакетов и их структуризация. Что у нас получилось, а что нет. А стоит ли вам делать так же, вы решите уже сами.
*
* - если вы каждый день открываете приложение "hh работа"
Для тех, кто не любит читать
У меня для вас есть выход! Эту статью можно глянуть в формате видео на ютубе в одном из эпизодов «Охэхэнных историй» ;)
Представим, что сейчас 2018 год. Представили? А теперь перестаньте плакать. В 2018 тоже было полно проблем.
У нас есть шикарный проект, в котором, по законам жанра, огромный монолит с 1 app gradle модулем и весь код в активити и несколько флэйворов. Основные из них - applicant и employer — приложения соискателя и работодателя соответственно.
В этом нет ничего страшного ровно до тех пор
пока команда не слишком разрослась и не стало трудно пилить фичи
пока старые разработчики не ушли, а новые знают не всё
пока не пришел хайптрейн с многомодульностью
пока рак на горе не свистнул
Подчеркните нужное.
В нашем случае произошло так: были разного рода лики и баги, фиксить которые было сложно, к тому же старые разработчики разбежались, а новым было трудно ориентироваться в том, что осталось. Из-за этого было трудно декомпозировать задачи и разработка стала “дорогой”.
А какие у вас были проблемы на проектах пишите в комментариях)
Структура проекта до ренейминга
Спустя некоторое время, когда многое было зарефачено и отлажено, мы пришли к новому виду структуры проекта. Это как раз та структура, которая была у нас до недавнего времени.
За всё это время в плане структуры произошло следующее:
мы избавились от flavors и разделили приложения на два app gradle-модуля — модули которые зависят от плагина "com.android.application".
разбили монолит на отдельные library gradle-модули (и в дальнейшем просто gradle-модули) — модули которые зависят от плагина "com.android.library".
Также прошло несколько этапов формирования структуры. И перед последним, о котором я расскажу в конце, структура была такая:
-
Часть фичей лежали в папке feature в корне проекта, туда складывались gradle-модули фичей соискательского приложения с префиксом “feature-” в именовании.
Например, фича резюме называлась feature-resume. Это рудиментарное решение, которое появилось почти в самом начале рефакторинга. -
Некоторые фичи состояли из нескольких сабфичей, по аналогии с 1 пунктом, но создавался не gradle-модуль, а папка также с префиксом, куда уже складывались нужные фичи. Слишком сложно? Сейчас приведу пример. Фича резюме, feature-resume, внутри нее лежали gradle-модули: feature-resume-profile, feature-resume-network и тд.
-
Общие gradle-модули, которые использовались в обоих приложениях, лежали в папочке shared. Помимо этого, фичи внутри shared делились на core и feature. Соответственно, в какой папке лежал gradle-модуль, то имя папки добавлялось префиксом к названию модуля. Как можете заметить на картинке, с core что-то было не так. По правилам префикс у названия фичей должен быть “shared-core-”.
-
Основная логика приложений лежала в папках соответствующих названиям приложений applicant (соискательское) и employer (работодательское).
Структура была аналогична папке shared и фичи в них именовались так:
Проблемы с именованием
Расположение модулей не мэтчилось с их названием, что затрудняло подключение модулей, а иногда и путало, особенно новых разработчиков
Бардак в названии пакетов, конечно были какие то правила, но за 3 года, они тоже менялись, а пакеты могли называться по-разному, иногда даже совпадали у некоторых разных фич
И выход из всего этого был — РЕНЕЙМИНГ!
Ретроспектива ренейминга
А теперь самое интересное. Расскажу про новую структуру проекта.
Начнем с небольшой ретроспективы и разберемся, с какими проблемами мы столкнулись.
До начала рефакторинга мы определились какой хотим видеть новую структуру.
Во время праздничных выходных в феврале мы начали переносить файлы по новым папкам и менять имена пакетов с помощью разного рода скриптов.
Время было выбрано специально, чтобы никто параллельно ничего не делал.Но работа затянулась. Тогда мы плохо понимали масштаб возложенной на нас задачи.
И поэтому решили для начала вручную составить гигантский чеклист того, что предстоит сделать, формата "как модуль назывался -> как будет называться". А также за одно переименовать пакеты в соответсвии с названием модулей
Он выглядел примерно так
#### feature-chat --> chat
#### feature-chat/core
- [x] applicant-feature-chat-core-data
— [x] rename module: applicant-feature-chat-core-data -> :applicant:feature:chat:core:data
— [x] rename package: ru.hh.applicant.feature.chat.screen_core_data -> ru.hh.applicant.feature.chat.core.data
- [x] applicant-feature-chat-core-network
— [x] rename module: applicant-feature-chat-core-network -> :applicant:feature:chat:core:network
— [x] rename package: ru.hh.feature_chat_network -> ru.hh.applicant.feature.chat.core.network
#### feature-chat/feature
- [x] applicant-feature-chat-list
— [x] rename module: applicant-feature-chat-list -> :applicant:feature:chat:chat-list
— [x] rename package: ru.hh.feature_chat_list -> ru.hh.applicant.feature.chat.list
и так далее
И даже после этого мы не до конца осознали, что нора, в которую мы залезли — не кроличья.
Поэтому первые модули было решено перенести просто руками, в Android Studio. После 10 модулей стало понятно, что git-история этих файлов исчезла. Терять историю нам однозначно не хотелось, поэтому мы вспомнили про такую команду как git mv, которая позволяет без потери истории перенести файлы из одной папки в другую.
Попытавшись перенести несколько модулей с использованием git mv, мы сильно взгрустнули. Потому что писать руками эти команды было очень долго и муторно.
Нужно было искать какое-то автоматическое решение. Поэтому мы написали простой bash-скрипт, который для двух заданных папок генерил тонну команд git mv, которые можно было скопировать и сразу запустить.
А вот и сам скриптик на генерацию git mv команд
#!/bin/bash
readonly LOCAL_PATH=$1
readonly START_FOLDER=$2
readonly START_PACKAGE=$3
readonly TARGET_FOLDER=$4
readonly TARGET_PACKAGE_NAME="$5"
echo "===== MV COMMANDS GENERATOR ====="
echo "LOCAL_PATH: ${LOCAL_PATH}"
echo "START_FOLDER: ${START_FOLDER}"
echo "START_PACKAGE: ${START_PACKAGE}"
echo "TARGET_FOLDER: ${TARGET_FOLDER}"
echo "TARGET_PACKAGE_NAME: ${TARGET_PACKAGE_NAME}"
readonly SPLITTED_TARGET_PACKAGE=(${TARGET_PACKAGE_NAME//./ })
TARGET_PATH_PARTS=""
for package_part in "${SPLITTED_TARGET_PACKAGE[@]}"; do
TARGET_PATH_PARTS="${TARGET_PATH_PARTS}/${package_part}"
done
readonly SOURCE_SET_FOLDER="/src/main/java"
readonly SOURCE_KOTLIN_SET_FOLDER="/src/main/kotlin"
readonly DESIRED_CODE_ROOT_FOLDER="${START_FOLDER}${SOURCE_SET_FOLDER}${TARGET_PATH_PARTS}"
echo "DESIRED_CODE_ROOT_FOLDER: ${DESIRED_CODE_ROOT_FOLDER}"
readonly SPLITTED_START_PACKAGE=(${START_PACKAGE//./ })
START_PATH_PARTS=""
for package_part in "${SPLITTED_START_PACKAGE[@]}"; do
START_PATH_PARTS="${START_PATH_PARTS}/${package_part}"
done
START_CODE_ROOT_FOLDER="${START_FOLDER}${SOURCE_SET_FOLDER}${START_PATH_PARTS}"
if [ ! -d "${START_CODE_ROOT_FOLDER}" ]; then
echo "... there is no /java source set --> /kotlin source set exists"
START_CODE_ROOT_FOLDER="${START_FOLDER}${SOURCE_KOTLIN_SET_FOLDER}${START_PATH_PARTS}"
fi
readonly FULL_START_PATH="$(cd "$(dirname "${START_CODE_ROOT_FOLDER}")"; pwd)/$(basename "${START_CODE_ROOT_FOLDER}")"
echo "START_CODE_ROOT_FOLDER: ${START_CODE_ROOT_FOLDER}"
echo "FULL_START_PATH: ${FULL_START_PATH}"
echo ""
echo "======= Generation result ======"
echo ""
# Первая команда — создаём директорию для переноса кода
echo "mkdir -p ${DESIRED_CODE_ROOT_FOLDER} && \\"
# Перечисляем команды для аккуратного переноса кода
for code_directory in ${FULL_START_PATH}/* ; do
NAME="${code_directory/${LOCAL_PATH}/.}"
echo "git mv ${NAME} ${DESIRED_CODE_ROOT_FOLDER} && \\"
done
# Последняя команда — перенос кода в target_folder
echo "git mv ${START_FOLDER} ${TARGET_FOLDER}"
echo ""
echo "================================="
echo ""
Дело пошло чуть веселее. Но перенос папок — это всего лишь одна часть истории. Вторая часть заключалась в том, что мы, помимо простого переноса папок, захотели ещё и ПЕРЕИМЕНОВАТЬ некоторые модули, о чем я писал выше, добавив структуры не только в иерархию папок, но и в иерархию package-ей.
Чтобы было вот так:
ru.hh.feature_chat_list -> ru.hh.applicant.feature.chat.list
ru.hh.feature_chat_network -> ru.hh.applicant.feature.chat.core.network
ru.hh.applicant.feature.chat.screen_core_data -> ru.hh.applicant.feature.chat.core.data
Поэтому помимо генерации команд git mv, нужно было ещё сгенерить команды для переименования одних package-ей в другие. Для этого мы тоже написали дополнительный скрипт, который генерил команды для вызова скрипта переименования.
Скрип запуска скрипта переименования
readonly START_FOLDER=$1
readonly START_PACKAGE=$2
readonly REPLACE_PACKAGE=$3
echo "START_FOLDER = ${START_FOLDER}"
echo "START_PACKAGE = ${START_PACKAGE}"
echo "REPLACE_PACKAGE = ${REPLACE_PACKAGE}"
echo ""
echo "========="
echo ""
for filename in ${START_FOLDER}/*; do
withoutPath=$(basename -- "$filename")
fff="${withoutPath%.*}"
echo "sh global_rename.sh ${START_PACKAGE}.${fff} ${REPLACE_PACKAGE}.${fff} && \\"
done
echo ""
echo "========="
Сам скрипт переименования пакетов
#!/bin/bash
readonly OLD_PACKAGE=$1
readonly NEW_PACKAGE=$2
find . -type d \( \
-name 'firebase' -o \
-name 'gradle' -o \
-name 'hooks' -o \
-name 'lint' -o \
-name 'profiling' -o \
-name 'scripts' -o \
-name 'detekt' -o \
-name '.git' -o \
-name '.gradle' -o \
-name '.mainframer' -o \
-name 'build' -o \
-name '.idea' -o \
-name 'android-style-guide' -o \
-name 'ci' \
\) -prune -o \
-type f \( \
-name '*.java' -o \
-name '*.kt' -o \
-name '*.gradle' -o \
-name '*.xml' -o \
-name '*.txt' \
\) \
-print0 | xargs -0 sed -i '' "s/${OLD_PACKAGE}/${NEW_PACKAGE}/g"
echo "done rename for ( ${OLD_PACKAGE} / ${NEW_PACKAGE} )"
В итоге мы просидели все выходные, по очереди генерируя команды для каждого модуля и проверяя, собирается ли приложение и запускается ли оно вообще.
Так прошло 3 дня и две ночи...
Ответственные за это люди ушли отдыхать, а остальные разработчики продолжили, но у нас ничего не вышло.
А не получилось потому, что:
в больших фиче ветках разработчиков было много изменений и таких веток было несколько, если бы каждый мержил себе сам, то он с большей вероятностью кто-то мог ошибиться где-то
также некоторые ветки пересекались по изменениям и было трудно потом все это смержить еще и с ренеймингом
Подумав, мы решили что было бы хорошо, если кто-то один замержит ренейминг везде!
Поэтому решили фиче ветки смержить сразу в develop, а те кто не хочет мержить сейчас, а хотят еще поработать, будут потом сами разруливать конфликты с новым develop.
И мы не до конца понимали, чем нам это грозит...
Смержив все, что можно было в develop, в пятницу мы объявили кодфриз и избранный занялся мержем ренейминга в develop.
За неделю работы команд накопилось кучу изменений, и при мерже вылезло много конфликтов.
Вот например
После мержа ветки с ренеймингом в старый develop (недельной давности), старые фичи сменили имя и пакет, по факту это означало, что они отправились в другую папку.
Обратите внимание на файлы, все хорошо, они лежат в новом месте.
Но при мерже ренейминга в новый develop после недельной работы, появились такие фантомные структуры:
По старым путям пакетов лежали файлы, которые были изменены разработчиками, но изменены они были в старой структуре, и таких мест было много... голова шла кругом.
Естественно, все это править руками будет долго... Чтобы ускорить процесс, в качестве вспомогательного инструмента, была выбрана утилита rsync, потому что она умеет рекурсивно мержить папки друг с другом, и ей можно указать стратегию разрешения конфликтов (перезапись, оставить новое, оставить старое, etc.).
В консоли с ее помощью рекурсивно переносили папки фичей. Из папки со старым названием в папку с новым.
Затем с помощью волшебных настроек Android Studio — Optimize imports on fly и Add unambiguous imports on the fly, были поправлены проблемы с импортами. Да, мы заходили ручками в каждый файл.
По-хорошему, надо было идти сначала от корневых модулей (shared/core) и делать регулярный синк проекта в IDE. Так пришлось бы гораздо меньше страдать потом с импортами при переносах файлов, ибо Android Studio автоматически бы их сразу переименовывала во всех местах, куда дотянется.
Но эта мысль пришла в наши светлые головы уже после проделанной работы и полученного опыта.
Спустя пару дней мучений мержа, develop был актуализирован и содержал новую структуру папок и новые имена пакетов.
А ребятам, которые не стали мержить свои ветки в develop, была предоставлена инструкция, как безболезненно влить в себе новый develop.
Но все было не так просто, как хотелось бы.
Приведу список основных пунктов, если вдруг вам понадобится:
Вмержить к себе в ветку develop и сохранить лог конфликтов,
на памятьчтобы понимать поле работыКонфликты типа modified — modified разрешить самим, руками, но таких конфликтов было минимум
Остальные конфликты нужно разрешить в свою пользу
Нужно перевести все созданные вами папки на новую структуру
Каждый перенос лучше делать отдельным коммитом, чтобы ничего не потерять и чтобы лучше контролировать процесс
Разрулить силами Android Studio все неправильные импорты
Удалить все неиспользуемые папки
В теории звучит просто, но в реальных условиях:
если переносить модуль/модули у себя в ветке, то рефакторинг будет применен к модулю, который располагался в старом месте. Приходилось повторить все, что уже сделал, но для перенесенного модуля (получилась двойная работа) + удалить старый модуль, причем сделать аккуратно, чтобы осталась история гита
если переносить файлы у себя в ветке, то рефакторинг будет применен к старым файлам, естественно появлялась неактуальная фантомная структура файлов, которые были уже перенесены, поэтому приходилось аккуратно их объединить с новыми
Всего файлов с конфликтами было ~500 в ~50 модулях.
Итоги ренейминга
-
Названия всех модулей соответствуют структуре папок.
И теперь включать gradle-модуль в settings.gradle можно через
include(':shared:feature:location'), поскольку путь совпадает с неймингом.
Для примера, раньше это делалось вот так: -
Все модули из папки feature из корня проекта (о чем я писал вначале), переместились на свои законные места в папку applicant/feature.
Имена пакетов стали соотноситься с расположением фичи.
Например, фича геолокации :shared:feature:location получила пакет ru.hh.shared.feature.location.Gradle-модули избавились от префиксов feature, shared, core и т.д.
Но префиксы сабфичей решено было оставить... А потом и их решили не писать :)Появилась возможность статической валидация подключения модулей.
В будущем будем проверять нейминг модулей и пакетов, а также корректность связей между модулями разных типов.
Наши рекомендации
Прежде чем идти в эту историю, надо написать скрипты, которые автоматизируют большую часть работы. Можно воспользоваться нашими наработками, но сначала нужно проверить их на валидность вашему проекту.
И основной совет — не делайте руками, делайте сразу через скрипты. Это сэкономит кучу нервов и времени.
Также нужно составить чеклист переноса модулей/файлов. И после каждого этапа переноса по чеклисту нужно проверять: собирается ли проект. Да, это долго и замедляет процесс, но сильно упрощает жизнь в дальнейшем. По крайней мере будет понимание, что “вот из-за этого у меня проект развалился“.
Для подобных глобальных изменений нужно обязательно уведомлять всю команду и заранее договариваться, как будет идти разработка в это время, чтобы минимизировать конфликты. Самый радикальный инструмент для этого — фичефриз/кодфриз etc. Если вы проводите такой рефакторинг, поддерживайте регулярную связь с командой, сразу же сообщайте о проблемах и потенциально сложных для мерджа местах.
И не стоит недооценивать эту задачу, если у вас большая кодовая база. Она стопудово займёт больше времени, чем вам кажется.
На этом все, если у вас остались какие-либо вопросы или вы можете поделиться собственным опытом, пишите в комментариях.
Спасибо за внимание ;)
Маленький опрос для большого исследования
Мы проводим ежегодное исследование технобренда крупнейших IT-компаний России. Нам очень нужно знать, что вы думаете про популярных (и не очень) работодателей. Опрос займет всего 10 минут.
Пройти опрос можно здесь.
Комментарии (9)
Power
07.10.2021 23:38+1Кажется, скрипт для
git mv
не нужен. Достаточно перенести/переименовать файлы любым способом, а потом сделатьgit add -A
. Ведь в гите нет такого понятия, как переименование, он просто видит, что файл исчез в одном месте и появился в другом месте и содержимое очень похоже.
Calc
у меня вопрос про опрос в подписи. Это такой неявный хантинг? Там в первые 20 вопросов выявляются сильные/слабые стороны текущего места работы, а после выбора "где бы вы хотели работать" при выборе "нигде из выше перечисленного" идет завершение (хотя там скрыто еще 22 вопроса)
schnitzer
На самом деле нет, мы каждый год проводим подобные опросы по технобренду разных компаний IT-рынка. Совсем скоро мы подведем итоги по этому опросу и опубликуем результаты :)
Xanderblinov
@Calcв прошлом году было аналогичное исследование, вот тут можно посмотреть результат
Calc
Статистика интересная, спасибо. А то с перегретостью рынка непонятно что ты делаешь. Отдаешь свое мнение или видоизмененное резюме.