Мне надоело писать один и тот же код. Поэтому я сделал Featuregen

Привет, Хабр. Я уже не тот беззаботный парнишка, что собирал 486-й компьютер. Время бежит, я скуф успел поработать с C++, C#, Java и Kotlin, последние 10 лет занимаюсь мобильной разработкой. Программирование я до сих пор люблю потому что больше ничего не умею. Но чем дольше работаешь, тем чаще ловишь себя на мысли, что от некоторых вещей начинает подгорать. Сегодня я расскажу, как я попытался избавиться хотя бы от части таких вещей.

Немного о себе. Я начал изучать программирование в 2005 году, когда пошел в восьмой класс. Один друг поделился со мной диском, на котором были записаны Flash-игры и эмуляторы, и я подумал, что раз кто-то в принципе готов играть на компьютере не только в Far Cry, Doom 3, NFS Most Wanted, а ещё и в это, у меня тоже есть шанс запилить игрушку и воплотить в жизнь какие-то свои идеи. Спойлер: не запилил. Для старта я выбрал Delphi. Во-первых, язык был достаточно понятным для новичка, а во-вторых, для него был DelphiX — обёртка над DirectX, которая позволяла относительно просто делать игры. На тот момент мы даже ещё не проходили тригонометрию в школе, поэтому я тратил часы, меняя цифры в непонятных скопипащенных формулах, пытаясь разобраться, почему персонаж когда-то прыгает нормально, а иногда вылетает за экран. Так почему же тогда я проводил за монитором дни напролёт и получал от этого кайф, а сейчас, когда я только начинаю проект, всё идёт хорошо, но постепенно добавление каждой новой фичи, каждого нового экрана начинает вызывать почти физическую боль?

Со временем я понял, в чем дело. Когда трава была зеленее и я пилил свой клон Марио на DelphiX, мне было достаточно одной функции onTimerTick() на всё. Весь код представлял из себя огромный файл и я не видел в этом проблемы. Теперь у нас есть промышленные стандарты, Архитектура. Теперь у нас не программы, а приложения, и в приложение нельзя просто добавить строчку без того, чтобы объявить юзкейс, интерфейс юзкейса, интерфейс репозитория, сам репозиторий, domain-модель, модель данных, конвертер из модели данных в domain-модель, интерфейс конвертера из модели данных в domain-модель, дата-соурс, провайдер для дата-сорса, di модуль, ну вы поняли… На одном проекте я насчитал 20+ обязательных сущностей для каждого экрана. И нет, я не против архитектуры. Архитектура решает реальные проблемы. Но вместе с этим она приносит огромное количество повторяющегося кода. А вот каждый раз писать руками тонны бойлерплейта уже утомительно. Да, на крупных проектах иногда есть какие-то тулзы для разворачивания новых фич, но, во-первых, лично вам о них могут просто забыть сказать, во-вторых, ежели это сделано в виде плагина к нашей любимой Android Studio, нередка ситуация, когда AS обновилась, а плагин еще нет, и вот и усё, карачун тебе, Церетели. Да и для моего Pet-проекта никто кроме меня самого такое решение не напишет.

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

Featuregen - это генератор файлов на основе шаблонов, написанный на POSIX Shell. По сути это небольшой DSL, который может быть использован для генерации исходников, конфигураций и других файлов. Вы можете задать вполне валидный вопрос почему было не взять что-нибудь из уже готовых генераторов, например cookiecutter или нечто подобное. Ну, главная причина в том что мне хотелось попробовать сделать всё самому. Когда я думал о том, как мог бы выглядеть идеальный результат этой работы, я представлял себе кусок кода, который можно скинуть в личку знакомому или коллеге, и он, просто скопипастив одну команду в терминал, получал бы полностью готовое окружение, без необходимости заниматься установкой ноды или пайтона. А это как раз может обеспечить шелл. Но проблема в том, что я не какой-то суперзнаток шелла. Да, я знал как запустить докер в CI/CD пайплайне, но давайте начистоту - шелл обладает устаревшим синтаксисом, к которому в наше время не то чтобы приятно прикасаться. Как я выкрутился? Решил, что буду работать по TDD, как пишут в умных книжках. Набросал простенький движок для тестов на основе JUnit. Движок берет шаблон, передает необходимые аргументы, прогоняет скрипт, сравнивает структуру и содержимое файлов на выходе. AI сгенерировал каркас скрипта, по мере необходимости я добавлял новые тесты и спустя какое-то время уже мог сам что-то править в коде скрипта. Я смирился с тем, что вряд ли у меня хватит терпения полностью разобраться в каждом символе матчинга строк на шелл, но если я в принципе хотя бы на верхнем уровне могу сказать про каждую строчку кода для чего она нужна, и все тесты проходят, и инструмент делает своё дело, меня это устраивает.

Вот пример простого шаблона Featuregen:

@expect name

@file path=Hello~<<name>>.txt
@@fstart
Hello, ~<<name>>!
@@fend

Здесь:

Теперь, если запустить генератор с параметром name=World, результатом будет файл HelloWorld.txt с содержимым:

Hello, World!

Для реальных задач одной подстановки переменных мало, поэтому в Featuregen есть несколько встроенных функций преобразования строк:

camel(value)

Конвертирует текст в camelCase

snake(value)

Конвертирует текст в snake_case

screamingSnake(value)

Конвертирует текст в SCREAMING_SNAKE_CASE

pathFromPackage(value)

Конвертирует com.example.app вcom/example/app

extractIgnoringCase(value, remove)

Удаляет подстроку без учёта регистра

Пример шаблона для Android ViewModel
@expect package
@expect fullName

@let shortName = extractIgnoringCase(fullName, Feature)
@let snakeCaseName = snake(fullName)
@let packagePath = pathFromPackage(package)

@file path=~<<packagePath>>/~<<snakeCaseName>>/presentation/~<<shortName>>ViewModel.kt
@@fstart
package ~<<package>>.~<<snakeCaseName>>.presentation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

internal class ~<<shortName>>ViewModel(
    screenMapper: ~<<shortName>>ScreenMapper,
) : ViewModel() {

    private val internalState = MutableStateFlow(~<<shortName>>ScreenInternalState.empty)

    private val _sideEffects = Channel<~<<shortName>>ScreenSideEffect>()
    val sideEffects = _sideEffects.receiveAsFlow()

    val uiState: StateFlow<~<<shortName>>ScreenViewState> =
        internalState
            .map(screenMapper::toViewState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = screenMapper.toViewState(internalState.value),
            )
}
@@fend

Вызываем генератор так:

./featuregen template.featuregen \
    package=foo.bar \
    fullName=FeatureTest

На выходе получим:

package foo.bar.feature_test.presentation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

internal class TestViewModel(
    screenMapper: TestScreenMapper,
) : ViewModel() {

    private val internalState = MutableStateFlow(TestScreenInternalState.empty)

    private val _sideEffects = Channel<TestScreenSideEffect>()
    val sideEffects = _sideEffects.receiveAsFlow()

    val uiState: StateFlow<TestScreenViewState> =
        internalState
            .map(screenMapper::toViewState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = screenMapper.toViewState(internalState.value),
            )
}

Конечно, создавать шаблоны вручную было бы немного иронично: мы пытаемся избавиться от рутины, а в итоге создаём новую рутину. Поэтому я сделал Telegram-бота для генерации шаблона по образцу кода или описанию.

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

А пока вы тоже можете попробовать сгенерировать шаблон через бота: @featuregen_bot.

У бота есть два режима работы:

Также можно написать гадость оставить фидбэк.

Получился ли идеальный инструмент? Пока нет. Сейчас Featuregen умеет только базовые вещи, и ему ещё есть куда расти. Но главное — он уже избавляет меня от части рутины и сохраняет некоторое количество нервных клеток. Поэтому проект не заброшен: я буду продолжать его развивать.

@AWE64
09.07.2026 23:23 UTC
Первоисточник

Комментарии

@
10.07.2026 03:15 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@shashurup
10.07.2026 18:32 UTC
0

А вот мне непонятно, если 20+ обязательных сущностей тупо генерится из шаблона, там точно есть какая-то логика чтобы ее вообще тестировать?
Выглядит так, что можно или один раз это всё универсально написать для большинства случаев, или тупо не городить эти 20 сущностей и срезать все углы. Уж больно это какой-то карго культ уже напоминает.

@georgiy08
11.07.2026 06:39 UTC
+2

После прочтения вашего комментария (и других, которые вы писали под другими статьями) у меня складывается впечатление, что ваш текст - это ИИ резюмирование.

@vcKomm
10.07.2026 07:51 UTC
+5

Велосипедный велосипед. Вы пишете про Android Studio — там есть Templates, которые могут это все и даже больше. Куча функций, переменных, Apache velocity, возможность встраивать шаблон в шаблон и прочее

@AWE64
10.07.2026 13:51 UTC
+1

Можно пользоваться тем, что даёт Android Studio, если нравится. Но мне хотелось именно CLI-инструмент, а не генерацию через меню IDE.
К тому же, мир не ограничивается Android Studio — есть Xcode, есть просто терминал. На мой взгляд, удобнее пользоваться одним инструментом для всего.

@Lucifurry
10.07.2026 08:32 UTC
0

Hygen?

@genmargen
10.07.2026 08:56 UTC
0

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