На данный момент я работаю с весьма развесистыми проектами (один из них состоит из почти 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

  • Сложности конфигурирования из-за весьма топорных способов запуска проекта/модуля

А теперь к плюсам:

  • Я еще ни разу не встречал штуку, которую было бы сложно внедрять в проект на такой архитектуре

  • Всегда понятно, где и что нужно искать

  • Обычно не возникает вопросов, что куда нужно положить

  • Благодаря общему следованию принципу частное зависит от общего , крайне редко возникают какие-то циклические зависимости и прочие схожие проблемы

В следующих статьях я постараюсь рассмотреть всё описанное на примерах

Комментарии (8)


  1. sshikov
    23.04.2024 18:51

    Как по мне, тут не хватает одной вещи - оценки масштаба проектов с какой-то другой точки зрения. Ну т.е. вот у вас 120 модулей получилось - у вас в проекте скажем сколько LOC? Как понять, много это модулей, или мало, как оценить объективно?


    1. InsanusMokrassar Автор
      23.04.2024 18:51

      В открытых проектах я почти не вижу даже 30 модулей, хотя бы пустых. У меня в проекте как минимум 100 модулей с наполнением - то есть таких, которые реально имеют код и отвечают за что-то свое :) писать про проект-пустышку, где половина модулей была бы без контента вовсе - как-то неспортивно, что ли :)


      1. sshikov
        23.04.2024 18:51
        +1

        Не, ну спортивно или нет - это другая история. Ну вот последний проект, что я смотрел - это был apache kerby, который является открытой java реализацией Kerberos - сервера и клиента. Ну т.е. это проект достаточно крупный, и в тоже время это проект, который можно охватить целиком (пусть и не за 15 минут). И там, для сравнения, всего 43 pom.xml (я посчитал). Вот мне поэтому и интересно - у вас проект, условно, в три раза сложнее? Или ваши модули по какой-то причине наполненные, но мелкие? Ну или еще проще - у вас очевидно есть проблемы с управлением этими сотнями модулей - вы про них статью написали, так? А вот где профит от таких мелких модулей в большом количестве? Ну раз вы их в таком количестве создаете - значит это чем-то удобно?


        1. InsanusMokrassar Автор
          23.04.2024 18:51

          Я так понимаю, Kerby - это проект с биндингами к системе Kerberos + обвязки вокруг. Такие проекты у меня тоже есть, но их невыгодно делать на описанной в статье архитектуре - там не всегда есть ярковыраженные client/common/server куски, а когда есть - их нетрудно внедрить. У нас проекты в основном клиент-серверные и по-сути то, что в Kerby умещается в модуль, у нас умещается в common каждой фичи и потом добавляются обвязки в client и server. Мелкие модули у нас есть, но они обычно появляются на старте фичи, бОльшая часть из них в итоге разрастаются, что тоже легко поддерживается этой архитектурой. Банальный пример - у нас была фича работы с файлами, где были репозитории, мультиплатформенные абстракции, для каждого таргета были нужные обвязки, а на клиенте/сервере были биндинги для отправки с клиента на сервер и загрузки с сервера на клиент на каждую платформу в том виде, в каком это было нужно. Понятно, что о мелкости тут можно поспорить, но в целом выглядит как самодостаточная фича


          1. sshikov
            23.04.2024 18:51

            Ну мне оценить сложно, насколько это может быть полезно, но идею я понял.


        1. InsanusMokrassar Автор
          23.04.2024 18:51

          Ну и я полагаю, Apache Kerby в основном создан на Java для JVM в основном для серверов или клиентов на JVM, у нас же поддерживаются нативный Android и Web(Kotlin/JS, html, css), при желании можно будет нативные таргеты добавить и это не потребует больших усилий


          1. sshikov
            23.04.2024 18:51

            Ну то есть это скорее особенность андроида, нежели ваших проектов?


            1. InsanusMokrassar Автор
              23.04.2024 18:51

              Что - особенность андроида? Вообще, в моем комментарии фигурировал не только он и я сказал, что благодаря этой архитектуре мы можем поддерживать почти любой таргет и не испытывать при этом боли, в частности - андроид и котлин/жс (веб)