Введение в монорепозиторий с Kotlin
Современная разработка сложных приложений часто сталкивается с проблемой управления кодом. Выбор между монорепозиторием и мультирепозиторием — одно из ключевых архитектурных решений. В этой статье мы разберем, как организовать монорепозиторий с Kotlin, используя лучшие практики и современные инструменты.
Монорепозиторий (monorepo) — это подход, при котором все проекты, библиотеки и сервисы хранятся в одном репозитории. Для Kotlin-разработчиков это особенно актуально благодаря мощной системе сборки Gradle и возможности использовать общий код между различными модулями.
Преимущества монорепозитория для Kotlin-проектов
Перед тем как перейти к организации, рассмотрим ключевые преимущества монорепозитория для Kotlin:
- Единая версия зависимостей — все модули используют одну версию Kotlin, библиотек и плагинов, что устраняет конфликты версий.
- Упрощенный рефакторинг — изменения в общей библиотеке автоматически проверяются всеми зависимыми модулями.
- Централизованная конфигурация — общие настройки Gradle, линтеры и CI/CD конфигурируются один раз.
- Атомарные коммиты — связанные изменения в разных модулях можно делать в одном коммите.
- Повторное использование кода — общие утилиты, DTO и бизнес-логика легко доступны всем проектам.
Структура монорепозитория с Kotlin и Gradle
Рассмотрим оптимальную структуру директорий для Kotlin-монорепозитория с использованием Gradle Multi-Module:
my-monorepo/
├── build.gradle.kts # Корневой build-файл
├── settings.gradle.kts # Настройка модулей
├── gradle.properties # Общие свойства
├── gradle/
│ └── libs.versions.toml # Version Catalog
├── modules/
│ ├── common/ # Общие утилиты и модели
│ ├── domain/ # Бизнес-логика
│ ├── data/ # Слой данных (API, БД)
│ ├── service-a/ # Микросервис A
│ ├── service-b/ # Микросервис B
│ └── shared-test/ # Общие тестовые утилиты
├── apps/
│ ├── backend/ # Основное приложение
│ └── cli-tool/ # CLI инструмент
└── config/
├── checkstyle/ # Конфигурация линтеров
└── docker/ # Dockerfile для сервисов
Настройка settings.gradle.kts
Файл settings.gradle.kts определяет все модули проекта:
rootProject.name = "my-monorepo"
// Включаем все модули
include(
":modules:common",
":modules:domain",
":modules:data",
":modules:service-a",
":modules:service-b",
":apps:backend",
":apps:cli-tool"
)
// Плагин для управления версиями
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
Использование Version Catalog (libs.versions.toml)
Для централизованного управления зависимостями используем gradle/libs.versions.toml:
[versions]
kotlin = "1.9.22"
coroutines = "1.7.3"
ktor = "2.3.7"
exposed = "0.44.1"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Пример build.gradle.kts для модуля common
plugins {
alias(libs.plugins.kotlin.jvm)
}
dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)
testImplementation(kotlin("test"))
}
kotlin {
jvmToolchain(17)
}
Организация кода: модули и зависимости
Правильная организация кода в монорепозитории критична для поддержки масштабируемости. Рассмотрим основные принципы:
Принцип разделения ответственности
Каждый модуль должен иметь четкую зону ответственности. Рекомендуется следующая иерархия:
- common — базовые утилиты, расширения, общие типы данных (не содержит бизнес-логики).
- domain — бизнес-логика, модели предметной области, порты для репозиториев.
- data — реализация репозиториев, работа с API, БД, кэширование.
- service-* — конкретные микросервисы или приложения.
Пример общего кода в модуле common
// modules/common/src/main/kotlin/com/example/common/Result.kt
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val exception: Throwable? = null) : Result<Nothing>()
}
// modules/common/src/main/kotlin/com/example/common/extensions.kt
fun String.isValidEmail(): Boolean =
this.matches(Regex("^[A-Za-z0-9+_.-]+@(.+)$"))
Зависимости между модулями
Управление зависимостями должно быть направлено от конкретного к общему:
// modules/service-a/build.gradle.kts
dependencies {
implementation(project(":modules:domain"))
implementation(project(":modules:data"))
implementation(libs.ktor.server.core)
}
// modules/domain/build.gradle.kts
dependencies {
implementation(project(":modules:common"))
}
// modules/data/build.gradle.kts
dependencies {
implementation(project(":modules:domain"))
implementation(libs.exposed.core