Основной инструмент любого программиста — язык программирования. Когда начинался проект мы выбрали Swift. Решили идти в ногу со временем, старый, но так горячо любимый Objective-C остался не у дел. Однако у Swift есть небольшая проблема и особенно она становится заметной, когда проект начинает расти – это проблема времени сборки проекта. Для понимания проблемы и размеров проекта, попробуем сравнить среднее время сборки за неделю на всех проектах студии.
Как видно из графика, медиана времени сборки проекта ZenitOnline больше всех представленных в несколько раз. И поверьте, остальные проекты не такие уж и маленькие. Особенный интерес представляет Objective-C проект, который в свою очередь по размерам сопоставим с нашим проектом. Оба содержат 100+ экранов. За полтора года разработки мы успели достичь следующих цифр:
- ≥ 2500 файлов;
- ≥ 600 ресурсов;
- ≥ 160 000 строк кода.
Однако счастье длилось недолго и, примерно, на этапе 1500 файлов xcode решил, что нам больше не нужно собирать проект и просто отказался это делать, аргументируя свое поведение следующей ошибкой:
Unable to spawn process (Argument list too long)
В поисках ответа на SO, Swift Jira и Open Radar, я понял, что решить это в одно действие не получится. Исходя из появившейся ошибки, найти выход из этой ситуации можно несколькими способами:
- Уменьшить количество папок в проекте, тем самым скинуть все в одну кучу, чтобы путь до компилируемых файлов уменьшился. Это решение не зашло из-за любви к порядку в проекте.
- Переместить проект выше в иерархии папок в системе. Это спасло нас на какое-то время, но далее уменьшать путь до директории проекта стало невозможно.
- Последним решением, которое реально могло помочь нам на тот момент, стала модульная архитектура проекта. Собственно, этого способа решать проблемы мы придерживаемся и до сих пор.
Более подробный ресерч проблемы со слишком длинным путем до компилируемых файлов можно найти в моем ответе на SO.
Спустя десятки потраченных часов, нервов и баночек энергетика удалось разбить проект для начала на два модуля, которые покрывали общие переиспользуемые классы и сервисный слой, а потом уже на полноценный концепт модульной архитектуры приложения.
Безусловно, переход на модульную архитектуру дался очень нелегко. На каждом этапе появлялось все больше и больше подводных камней. Связный код не давал с легкостью разбивать проект на слои по ответственностям, но в итоге проект, который изначально полностью разрабатывался в одной области видимости, превратился в проект, разбитый на отдельные модули. У нас получилась схема ответственностей модулей:
3rd Party Dependency – самый нижний слой, отвечает за сторонние зависимости. Все эти сторонние библиотеки подключаются к проекту с помощью CocoaPods. Ничего необычного по сравнению с плоской архитектурой приложения.
Core Frameworks – после слоя со сторонними библиотеками идет основной блок фреймворков, которые в дальнейшем импортируются во все части приложения и используются так или иначе.
- Common – модуль, в котором содержатся основные константные значения. Например, локализация, все картинки, extensions и другие не завязанные на UI и сервисном слое классы и утилиты.
- Services – этот модуль содержит в себе все необходимые классы для работы с сетью и данными. Здесь можно найти модели сервисного слоя, утилиты для работы с сетью, БД и другими данными в приложении.
- Reusable – в этот фреймворк мы решили вынести все переиспользуемые UI-элементы нашего приложения: поля ввода, ячейки, адаптеры и другие вьюшки.
- Analytics – исходя из названия, думаю, легко понять, что все классы аналитики находятся тут.
- Core – фреймворк, в котором не находится ни строчки кода. Единственная его цель – это объединение импортов всех модулей, входящих в Core слой. Это необходимо для дальнейшей удобной линковки переиспользуемых фреймворков в других модулях.
Feature Frameworks – это фреймворки, которые содержат в себе отдельно взятые фичи. Каждый фреймворк из этого слоя импортирует в себя Core уровень и другие зависимые от него фичи. Такой подход позволяет удобно изолировать работу и код разных разработчиков.
Выводы
Какую проблему решает модульность? Как минимум две проблемы были решены благодаря такому подходу:
- Скорость сборки проекта.
- Невозможность скомпилировать проект.
Скорость сборки проекта «на холодную» вообще не изменилась, так как количество необходимых для компиляции файлов не уменьшилось. Они просто стали собираться в разных стадиях, что позволило решить проблему бесконечно длинного пути до компилируемых файлов в основном модуле проекта. Однако уменьшилось время сборки проекта «на горячую», когда мы вносим изменения только в один модуль, который не используется в бесконечном количестве других частей проекта, то xcode собирает только то, что вы изменили. Конечно это не работает в случае, когда вы изменяете переиспользуемый элемент, который задействован в большом количестве мест в приложении.
Также, немаловажно — мы наконец-то смогли собрать проект. Проект начал билдиться без танцев с бубном.
P.S. Xcode 11 решает проблему с ошибкой компиляции слишком большого количества файлов. Конечно мы не отказались от модульной архитектуры и продолжаем развивать этот архитектурный подход в Surf.
В следующей статье я расскажу о проблемах доставки фич, с которыми мы столкнулись по мере того, как наш проект разрастался в огромное банковское приложение.
Кейсы, лучшие практики, новости и вакансии Surf — в телеграм-канале Surf iOS Team. Присоединяйтесь >>