Владелец кода, отзовись! Как построить и применить систему владения кодом

от автора

Привет, Хабр! Меня зовут Марат, я работаю Android-инженером в большом проекте в приложении СберИвестиции. Над ним трудится около 30 разработчиков и множество кроссфункциональных команд, написано более миллиона строк кода, расположенного в более чем 300 модулях. И на подобных масштабах начинают проявляться неочевидные проблемы, связанные с владением кодом, а именно:

  1. Внесли правки в модули вашей команды — и всё поломали, потому что были не в курсе каких‑то нюансов.

  2. Непонятно, кому задавать вопросы по коду. Git-blame тут не панацея, потому что, возможно, вопросы надо задавать аналитику, а не разработчику. А может, автор кода уже уволился.

  3. Кто-то подключил твой модуль без твоего ведома, и если это был временный код, который планировалось удалить или рефакторить, то теперь у тебя проблемы.

  4. Были изменения в твоём модули, а потребители кода это пропустили и потом получают баги в регрессе или даже проде.

Code review решает такие проблемы частично, всегда присутствует человеческий фактор, и раз за разом подобные проблемы проходят через проверки. Но решение есть — это концепция Code Ownership, которую мы применили в нашем проекте.

Как это работает?

Мы используем многомодульную API/IMPL-архитектуру: API-модули предоставляют данные и методы другим модулям, а IMPL-модули делают большую часть работы и к ним нельзя подключаться напрямую. Ещё мы написали плагин для Gradle, который позволяет задать владельца модуля в build.gradle.kts. Все наши самописные плагины находятся в композитной сборке внутри проекта:

Наши плагины для Gradle

Наши плагины для Gradle

Подробнее про композитные сборки можно почитать тут.

В нашем случае, каждый владелец — это кроссфункциональная команда, отвечающая за какую‑либо функциональность в проекте (например, портфель или рынок).

description = "Модуль плагинов для определения владельцев кода" group = "investor.buildlogic"  gradlePlugin {     plugins {         create("inv.ownership") {             id = "inv.ownership"             implementationClass = "inv.ownership.OwnershipPlugin"         }     } }
class OwnershipPlugin : Plugin<Project> {      override fun apply(target: Project) {         if (target == target.rootProject) {             target.subprojects                 .filter { it.projectDir.resolve("build.gradle.kts").exists() }                 .forEach { subProject ->                     subProject.extensions.create("ownership", OwnershipExtension::class.java)                 }         }     } }
ownership {     owner.set(Team.FINANCIAL_INSTRUMENTS) }

Другие Gradle-плагины могут распарсить эту информацию и сделать с ней что-нибудь полезное.

val extension = project.extensions.findByType(OwnershipExtension::class.java) val owner: Owner = extension?.owner?.orNull ?: emptyOwnerError(project)

Как мы это используем

Автоматическое добавление владельцев кода на проверку pull request’ов

Используется в связке с ещё одним Gradle-плагином, который был написан для impact-анализа и умеет отслеживать зависимости от внешних библиотек и между модулям проекта.

private fun fillDependencies(rootProject: Project) {     rootProject.subprojects.filter { it.hasBuildGradleKts() }.forEach { subProject ->          val module = modules.first { it.name == subProject.fullName() }         subProject.configurations.forEach { configuration ->             configuration.dependencies.forEach { dependency ->                 val targetName = "${dependency.group?.removePrefix("${rootProject.name}.")}:${dependency.name}"                 val dependencyModule =                     modules.find {                         it.name.endsWith(targetName)                                  && dependency.group?.startsWith(rootProject.name) == true                     }                 if (dependencyModule != null) {                     // Тестовые плагины добавляют модуль в зависимый classpath к себе самому(debugAndroidTestCompileClasspath/debugUnitTestCompileClasspath/releaseUnitTestRuntimeClasspath...)                     if (module.name != dependencyModule.name) {                         val dependencies = moduleDependencies[module] ?: HashSet()                         dependencies.add(dependencyModule)                         moduleDependencies[module] = dependencies                     }                 } else {                     val library = LibraryDependency(                         dependency.group ?: "unspecified",                         dependency.name,                         dependency.version ?: "unspecified"                     )                     val dependencies = libraryDependencies[module] ?: HashSet()                     dependencies.add(library)                     libraryDependencies[module] = dependencies                 }             }         }     } }

Когда в pull request’е изменяется код, мы автоматически добавляем владельцев этого кода на проверку и без их одобрений этот pull request влить не получится. Также мы отслеживаем добавление зависимостей от API-модулей и добавляем их владельцев на проверку. Это позволяет командам отслеживать нагрузку на их функциональность и понимать, как её используют (чтобы знать, когда нужно добавить пару серверов для микросервиса для удержания нагрузки).

Для этого мы собираем в pull request’е граф зависимостей, сравниваем его с графом зависимостей целевой ветки в pull request’е и собираем дифф:

private fun calculateDiff(     currentDependencies: Map<String, List<String>>,     developDependencies: Map<String, List<String>> ): DependencyGraphDiff {     val addedDependencies = HashMap<String, List<String>>()     val removedDependencies = HashMap<String, List<String>>()     currentDependencies.keys.forEach { key ->         val current = currentDependencies[key].orEmpty()         val inDevelop = developDependencies[key].orEmpty()         val addedDiff = current - inDevelop.toSet()         if (addedDiff.isNotEmpty()) {             addedDependencies[key] = addedDiff         }         val removedDiff = inDevelop - current.toSet()         if (removedDiff.isNotEmpty()) {             removedDependencies[key] = removedDiff         }     }     return DependencyGraphDiff(addedDependencies, removedDependencies) }

После этого по диффу изменений мы находим владельцев и записываем в файл:

val ownerLabels = (dependencyDiff.addedDependencies.values.asSequence() + dependencyDiff.removedDependencies.values.asSequence())     .flatten()     .toSet()     .map { moduleName ->         var result: Owner = UNKNOWN         val module = service.findModule(project, moduleName)         if (module != null) {             val extension = module.project.extensions.findByType(OwnershipExtension::class.java)             val owner: Owner = extension?.owner?.orNull ?: emptyOwnerError(project)             result = owner         }         result     }     .toSet() val reportFile = project.buildDir.resolve(NEW_API_DEPENDENCIES_OWNERS_REPORT_PATH) reportFile.parentFile.mkdirs() reportFile.writeText(ownerLabels.map { it.prHandlerLabel() }.filter { it.isNotEmpty() }.joinToString(separator = ","))

После этого стейджинговая среда в JenkinsFile читает файл с владельцами и добавляет их на проверку через Rest API BitBucket’а.

Загрузка документации в Confluence

Gradlе-плагин, который собирает всю Javadoc/KDoc-информацию в API-модулях, генерирует HTML-страницы с помощью Dokka и загружает в Confluence через Rest API.

Владение кодом используется для того, чтобы разложить документацию по владельцам. Очень полезная вещь для аналитиков и владельцев продукта, помогает найти, где взять необходимую функциональность без возвращения в исходный код проекта.

Уведомление всех потребителей изменённого модуля

Прямо сейчас мы разрабатываем механизм уведомления об изменениях библиотечных модулей для тестировщиков в командах, которые этот модуль используют. Это делается для того, чтобы во время регресса ребята обратили более пристальное внимание на эту функциональность.

Также владельцы кода широко используются в учёте техдолга, но об этом я расскажу позже в отдельной статье.

Вариантов использования концепции владения кодом очень много. К примеру, сбор статистики по проекту позволяет посмотреть, кто отвечает за какую часть проекта, и не пора ли добавить рабочую силу. Вдобавок можно быстро найти, кому назначить тот или иной дефект.

На этом у меня все, надеюсь статья оказалась полезной и до новых встреч.


ссылка на оригинал статьи https://habr.com/ru/articles/834672/