На данный момент я работаю с весьма развесистыми проектами (один из них состоит из почти 120 градл модулей) и уже достаточно давно разные факторы подталкивали меня написать статью о том, как я организую свои проекты: стажеры и коллеги, чтение различных статей и книг. Понятное дело, что не существует серебряной пули, но я надеюсь, что эта статья поможет кому-то в понимании, как можно организовывать проекты. Добро пожаловать в комментарии для обмена опытом :)
Статью планируется разделить от большего к меньшему - от организации всего проекта до организации UI экранов. Все проекты я создаю с помощью gradle
и, соответственно, в рамках статьи слово "модуль" будет обозначать gradle
модуль.
Дисклеймер об оригинальности
В этой статье будет много очевидных (казалось бы) вещей - общие части приложения должны быть в общих модулях и прочее такое. Тем не менее, такие вещи важно проговаривать во избежание недопонимания подхода.
Дисклеймер о библиотеках
В своих проектах я, как правило, работаю со своим семейством библиотек:
MicroUtils для почти всего, от корутин до репозиториев
Krontab для отложенных и периодических задач
KSLog для логгирования
KTgBotAPI для работы с телеграм ботами
Navigation для собственно UI навигации
Это сделано потому, что популярные библиотеки имеют ряд недостатков, часто критичных на моих проектах. Например, если в библиотеке есть баг, который мешает, иногда можно годами ждать его исправления (особенно если он некритичен для большей части пользователей). Аналогично, если нужна какая-то фича в библиотеке - ситуация примерно как с багом, можно хоть PR запилить, он может проваляться в бэклоге неизвестно сколько.
Тем не менее, все эти инструменты базируются на таких понятных и надёжных решениях, как ktor, kotlinx serialization, koin и многих других.
Организация проекта
Проект обычно разделяется на три большие папки: features
, client
, server
. Бывают ситуации, когда нужно разделить типы клиентов и/или типы серверов, и тогда имеет смысл в папках client
/server
создавать соответствующие подмодули. Пример на диаграмме ниже:
Таким образом, клиенты и серверы никак не зависят друг от друга и могут иметь любой собственный код и способы запуска.
Фичи (модули в папке features) всегда зависят от common и тех фич, которые логически им нужны. В таком случае они не будут зависеть (даже косвенно) от тех частей приложения, которые им не нужны.
В итоге получается, что каждый модуль самостоятелен и располагается в логичном месте, не имея привязок к другим модулям, которые ему не нужны. Это же в итоге помогает IDE понимать, что именно в данном модуле может быть доступно.
Организация фичи
Фича состоит из простых модулей: common
, client
и server
common
модуль отвечает за общий код: модели (например, пользователь, токен авторизации и т.д.), базовые инструменты (математика работы с локациями, стандартные преобразования типов). То есть если что-то в рамках фичи будет использоваться и на сервере, и на клиенте, либо не зависит от расположения (та же математика) - оно идёт в common
.
client
и server
зависят от common
и, соответственно, отвечают за свои части: репозитории, алгоритмы, UI (в случае клиента), конфигурации запуска.
Поскольку я работаю в основном с KMP, каждый модуль может опционально иметь платформенные или общие сорссеты, контент которых составляется примерно по тем же принципам, что и данное разделение модулей.
Организация UI
В работе мы используем классический MVVM по нескольким причинам:
Он очень хорошо расширяется
С ним легко следовать общим принципам программирования (общее не зависит от частного и наоборот)
Каждый элемент легко объясним с точки зрения его присутствия в цепочке
Идеально ложится на большинство UI фреймворков и на сырую работу с UI (тем же html)
Принцип при этом очень простой - View
отрисовывает, ViewModel
отвечает за выдачу текущего состояния и логику UI части, Model
- источник данных.
С точки зрения UI, абсолютно не важно, что находится за Model
, поэтому переезды между библиотеками, смена логики какого-то кэширования и прочие не связанные с UI штуки не особо влияют на UI часть как таковую.
Организация DI
В своих проектах мы используем связку Koin + MicroUtils/Startup. Суть очень простая: каждый модуль имеет набор плагинов, каждый из которых может быть тем или иным образом подключен в клиентах и серверах. При этом, как правило, используется следующая диаграмма плагинов:
В данной диаграмме Platform
соответственно заменяется на, например, JVM
/JS
/etc., а Client
можно заменить на Server
для серверных модулей.
При старте сервера, в конфигурации указывается список модулей, которые мы включаем в сервер. Метод старомодный, но работает на 100%, плюс теоретически этот подход легко улучшается с помощью написания соответствующего KSP плагина. Таким же образом работает Client
В плагинах мы имеем две части:
Часть инициализации
Koin
модуля - здесь определяются репозитории,ViewModel
,Model
, фабрикиView
и т.д.Часть старта приложения - здесь запускаются серверные и клиентские сервисы, такие как сервисов автоматизации авторизации на клиентах и сборки мусора на сервере
Подведение итогов, или плюсы/минусы этого безобразия
В двух словах, получается следующая структура:
Есть фичи. В фичах есть общая фича и все остальные, от неё зависящие. Каждая фича делится на common, server и client
Есть клиенты. Модули конечных клиентов зависят от
client
модулей фичей, но каждый клиент зависит только от тех модулей, которые ему нужныЕсть серверы. Модули конечных серверов зависят от
server
модулей фичей, но каждый сервер зависит только от тех модулей, которые ему нужныДля DI используем Koin + MicroUtils/startup
В UI используем
MVVM
.ViewModel
иModel
регистрируются вCommonPlugin
common
модуля,View
регистрируется и встраивается в UI из модулей на платформах
А теперь пришла пора плюсов и минусов, и начнём мы с минусов:
Иногда, особенно в сложных проектах, получается крайне ветвистая структура модулей, что мешает быстро ориентироваться в проекте
Много бойлерплейта. Спасает плагин для идеи SegmentGenerator
Сложности конфигурирования из-за весьма топорных способов запуска проекта/модуля
А теперь к плюсам:
Я еще ни разу не встречал штуку, которую было бы сложно внедрять в проект на такой архитектуре
Всегда понятно, где и что нужно искать
Обычно не возникает вопросов, что куда нужно положить
Благодаря общему следованию принципу
частное зависит от общего
, крайне редко возникают какие-то циклические зависимости и прочие схожие проблемы
В следующих статьях я постараюсь рассмотреть всё описанное на примерах
sshikov
Как по мне, тут не хватает одной вещи - оценки масштаба проектов с какой-то другой точки зрения. Ну т.е. вот у вас 120 модулей получилось - у вас в проекте скажем сколько LOC? Как понять, много это модулей, или мало, как оценить объективно?
InsanusMokrassar Автор
В открытых проектах я почти не вижу даже 30 модулей, хотя бы пустых. У меня в проекте как минимум 100 модулей с наполнением - то есть таких, которые реально имеют код и отвечают за что-то свое :) писать про проект-пустышку, где половина модулей была бы без контента вовсе - как-то неспортивно, что ли :)
sshikov
Не, ну спортивно или нет - это другая история. Ну вот последний проект, что я смотрел - это был apache kerby, который является открытой java реализацией Kerberos - сервера и клиента. Ну т.е. это проект достаточно крупный, и в тоже время это проект, который можно охватить целиком (пусть и не за 15 минут). И там, для сравнения, всего 43 pom.xml (я посчитал). Вот мне поэтому и интересно - у вас проект, условно, в три раза сложнее? Или ваши модули по какой-то причине наполненные, но мелкие? Ну или еще проще - у вас очевидно есть проблемы с управлением этими сотнями модулей - вы про них статью написали, так? А вот где профит от таких мелких модулей в большом количестве? Ну раз вы их в таком количестве создаете - значит это чем-то удобно?
InsanusMokrassar Автор
Я так понимаю, Kerby - это проект с биндингами к системе Kerberos + обвязки вокруг. Такие проекты у меня тоже есть, но их невыгодно делать на описанной в статье архитектуре - там не всегда есть ярковыраженные client/common/server куски, а когда есть - их нетрудно внедрить. У нас проекты в основном клиент-серверные и по-сути то, что в Kerby умещается в модуль, у нас умещается в common каждой фичи и потом добавляются обвязки в client и server. Мелкие модули у нас есть, но они обычно появляются на старте фичи, бОльшая часть из них в итоге разрастаются, что тоже легко поддерживается этой архитектурой. Банальный пример - у нас была фича работы с файлами, где были репозитории, мультиплатформенные абстракции, для каждого таргета были нужные обвязки, а на клиенте/сервере были биндинги для отправки с клиента на сервер и загрузки с сервера на клиент на каждую платформу в том виде, в каком это было нужно. Понятно, что о мелкости тут можно поспорить, но в целом выглядит как самодостаточная фича
sshikov
Ну мне оценить сложно, насколько это может быть полезно, но идею я понял.
InsanusMokrassar Автор
Ну и я полагаю, Apache Kerby в основном создан на Java для JVM в основном для серверов или клиентов на JVM, у нас же поддерживаются нативный Android и Web(Kotlin/JS, html, css), при желании можно будет нативные таргеты добавить и это не потребует больших усилий
sshikov
Ну то есть это скорее особенность андроида, нежели ваших проектов?
InsanusMokrassar Автор
Что - особенность андроида? Вообще, в моем комментарии фигурировал не только он и я сказал, что благодаря этой архитектуре мы можем поддерживать почти любой таргет и не испытывать при этом боли, в частности - андроид и котлин/жс (веб)