Уменьшаем написание boilerplate с помощью File Templates

от автора

Всем привет! На связи Дима Котиков, и я все еще люблю разбираться в технологиях, разрабатывать под Android и KMP и пить латте на фундучном молоке 🙂

Рассказываю о генерации файлов с boilerplate-кодом с помощью удобного механизма задания File Templates в средах разработки Intellij. File Templates позволяет в пару кликов создавать несколько файлов с каким-либо boilerplate-кодом. Хоть статья приводит примеры создания File Templates для Android/Kotlin Multiplatform, она может быть полезна всем, кто работает в средах разработки от Intellij.

Шаблонный код

Каждый разработчик в повседневной работе сталкивается с созданием однотипных файлов и шаблонных конструкций, будь то файлы а-ля Controller/Service/Repository в backend-разработке или Fragment-, ViewModel-, State-, SideEffect-, Store-сущности в Android/KMP-разработке. 

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

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

Для примера приведу код одного шаблонного экрана мобильного приложения:

Шаблонный код экрана мобильного приложения
// file: SomeFeatureScreen   @Composable fun SomeFeatureScreen(     viewModel: SomeFeatureViewModel,     router: SomeFeatureRouter, ) {     val lifecycle = LocalLifecycleOwner.current.lifecycle       LaunchedEffect(viewModel) {         viewModel.sideEffectFlow             .flowWithLifecycle(lifecycle)             .onEach { sideEffect: SomeFeatureSideEffect ->                 handleSideEffect(sideEffect, router)             }             .launchIn(this)     }       val state: SomeFeatureState by viewModel.stateFlow.collectAsStateWithLifecycle()       SomeFeatureStateContent(         state = state,         onEvent = { uiEvent -> viewModel.dispatch(uiEvent) }     ) }   private fun handleSideEffect(sideEffect: SomeFeatureSideEffect, router: SomeFeatureRouter) {     // some feature-specific logic }   @Composable private fun SomeFeatureStateContent(     state: SomeFeatureState,     onEvent: (SomeFeatureUiEvent) -> Unit ) {      // some feature-specific logic } 
// file: SomeFeatureViewModel   class SomeFeatureViewModel : ViewModel() {       private val _stateFlow = MutableStateFlow<SomeFeatureState>(SomeFeatureState())     val stateFlow: StateFlow<SomeFeatureState>         get() = _stateFlow.asStateFlow()       private val sideEffectChannel = Channel<SomeFeatureSideEffect>()     val sideEffectFlow: Flow<SomeFeatureSideEffect>         get() = sideEffectChannel.receiveAsFlow().flowOn(Dispatchers.Main.immediate)       fun dispatch(uiEvent: SomeFeatureUiEvent) {         // some feature-specific logic      }       private fun reduceState(reducer: (SomeFeatureState) -> SomeFeatureState) {         _stateFlow.update(reducer)     }       private fun sendSideEffect(sideEffect: SomeFeatureSideEffect) {         viewModelScope.launch {             sideEffectChannel.send(sideEffect)         }     }        // other functions with some feature-specific logic    } 
// file: SomeFeatureState   @Immutable data class SomeFeatureState(     val isLoading: Boolean,     ... // other some feature-specific fields ) 
// file: SomeFeatureSideEffect   @Immutable sealed interface SomeFeatureSideEffect {       data class ShowUnknownError(val error: Throwable) : SomeFeatureSideEffect       // other some feature-specific subclasses } 
// file: SomeFeatureUiEvent   @Immutable sealed interface SomeFeatureUiEvent {       data object NavigateBack : SomeFeatureUiEvent       // other some feature-specific subclasses } 
// file: SomeFeatureRouter   interface SomeFeatureRouter {       fun navigateBack()       // other some feature-specific functions   } 
// file: SomeFeatureRouterImpl   class SomeFeatureRouterImpl : SomeFeatureRouter {       override fun navigateBack() {         // some feature-specific logic     }       // other some feature-specific functions   } 

Код немного утрирован для понимания того, что происходит в представленном примере, в production-коде некоторые конструкции могут быть вынесены в интерфейсы или абстрактные классы.

Если мы внимательно посмотрим на код, то легко сможем выделить некоторые конструкции, которые можно назвать шаблонными:

  • Названия сущностей именуются как «<feature name>» + «<suffix>», где «<feature name>» — специфичное название для фичи или экрана, а «<suffix>» — шаблонный суффикс, обозначающий, что это за сущность: экран, вьюмодель, роутер, модель состояния экрана и тому подобное.

  • У сущностей есть шаблонные вызовы, например в SomeFeatureScreen вызываются подписки на состояние (viewModel.stateFlow.collectAsStateWithLifecycle()) и side effect-ы (код внутри LaunchedEffect). В классе SomeFeatureViewModel мы видим стандартный подход к хранению сущности состояния (StateFlow<SomeFeatureState>) и его смены (функция reduceState()), а также хранение и отправка sideEffect-ов (one-time-events).

Если бы приложения состояли только из одного экрана и нам нужно было единожды написать шаблонный код, то особых проблем не возникло бы. На деле даже в небольших мобильных приложениях количество фич и экранов может достигать 50—100 штук. Представим, сколько времени суммарно может уйти, чтобы вручную 100 раз создать по семь файлов с шаблонным кодом, не говоря уже о том, что на каждом экране нужно прописать бизнес-логику, верстку, взаимодействие с сетью, локальным хранилищем, навигацию и так далее.

Как уменьшить боль от boilerplate-кода

Вариантов для решения проблемы несколько:

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

Написать annotation- или symbol-processor для генерации файлов или классов на базе аннотаций. Инструмент мощный, но не особо подходит для решения нашей проблемы. Все равно нужно руками создать как минимум один файл, написать какой-то базовый код, расставить аннотации, запустить компиляцию и только после этого пользоваться сгенерированными файлами и кодом в них. 

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

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

Использовать встроенный в Intellij-среду механизм Live Templates — частично решает проблему с генерацией шаблонного кода, но не решает задачу создания и именования семи файлов.

Использовать встроенный в Intellij-среду механизм File Templates — наш выбор, так как умеет создавать файлы по шаблонам, причем сразу несколько. Плюс ко всему есть возможность для написания шаблонного кода по различным условиям.

А если вы знаете другие способы работы с бойлерплейтом — пишите в комментариях 😊

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

Плюсы, возможности и синтаксис File Templates 

Есть несколько весомых причин выбрать File Templates:

  • инструмент решает нашу проблему — генерирует файлы и контент в них по шаблонам;

  • генерирует несколько файлов в одно действие;

  • задает пользовательские переменные, которые можно использовать при генерации шаблонного кода;

  • для написания шаблонов не нужно глубоких и специфических знаний — достаточно изучить несколько базовых конструкций, чтобы решать большинство проблем с созданием шаблонов;

  • инструмент интегрирован в Intellij-подобные среды разработки, поэтому дополнительных действий, связанных с установкой и первоначальной настройкой (как это нужно для плагинов), не требуется.

Примеры покажу в Android Studio, но практически вся информация валидна и для остальных сред разработки на базе Intellij IDEA.

Чтобы добраться до инструмента File Templates, нужно открыть Settings → Editor → File and Code Templates:

Путь до меню File Templates

Путь до меню File Templates

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

Нас интересуют вкладки Files и Includes. На вкладке Files располагаются коллекции с шаблонами, а во вкладке Includes — шаблоны, предназначенные для переиспользования в шаблонах из вкладки Files.

Шаблоны переиспользуем в шаблонах, осталось прикрутить мониторы

Шаблоны переиспользуем в шаблонах, осталось прикрутить мониторы

Весь шаблонный код, который мы будем писать, по сути своей будет миксом из кода языка и из вспомогательного кода, состоящего из специальных переменных для File Templates и конструкций, написанных на Apache Velocity — языке, используемом для шаблонов.

Из стандартных переменных чаще всего приходится использовать ${NAME} и ${PACKAGE_NAME}. Можно создавать свои переменные, для этого их достаточно просто объявить в месте использования в шаблоне и тогда при создании шаблона появится отдельное поле для ввода кастомной переменной.

Посмотрим синтаксис Apache Velocity, так как на нем пишется логика для шаблонов.

Первое, что можно увидеть в стандартных шаблонах, — указание package name для файлов через конструкцию if:

#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")     package ${PACKAGE_NAME}  #end

Есть возможность писать конструкции if-else:

#if (${NAME} != "")     class ${PACKAGE_NAME}  #else      class Unknown  #end

Или if-elseif-else:

#if (${NAME} == "")     class Unknown  #elseif (${NAME} == "a")     class OneLetter  #else      class ${NAME}  #end

Все ключевые слова начинаются со знака #, ссылки на переменные с $. Как указано в документации: ссылки начинаются со знака $ и используются для получения чего-либо. Директивы начинаются со знака # и используются, чтобы сделать что-либо (присвоить значение, обработать что-либо и т. п.).

Присвоение переменной и ее последующее применение выполняется так:

#set( $someVar = "Hello, World!" ) $someVar

Если мы хотим, чтобы переменная была задана путем пользовательского ввода, ее нужно сразу указывать в месте использования без set-конструкции:

$USER_INPUT  // or ${USER_INPUT}

А еще нам доступны циклы:

#set( $customerParts = ["John", "Doe"] ) #set( $customerFull = "" ) #set( $part = "" )   #foreach( $part in $customerParts )     #set( $customerFull = $customerFull + $part ) #end  $customerFull  // prints `JohnDoe`

Ключевые слова #break позволяют прервать цикл #foreach, а #stop прекращает выполнение кода в template-файле.

Для переиспользования скриптов между файлами шаблонов нужно задать шаблон во вкладке Includes, а потом в целевых файлах вызвать с помощью конструкции #parse — простейший пример есть в шаблонах для проставления хедера файла:

#parse("File Header.java")

Аналогично #parse есть еще конструкции #include:

#include("SharedData.java")

Ключевые отличия #parse от #include:

  • #parse допускает, что в передаваемом в параметры файле может содержаться VLT-код, который будет выполнен в месте вызова. #include же просто добавит контент из передаваемого в параметрах файла прямиком как есть в место вызова, без обработки VLT-кода.

  • #parse может принимать в себя только один аргумент, а #include — несколько:

#parse("File1.java", "File2.java") // ERROR, 2 parameters passed. Must be 1 parameter #include("File1.java", "File2.java") // CORRECT, more than 1 parameters available

Еще есть директива #define, которая позволяет задать какой-то шаблон с переменными, которые должны вычислиться позже, к примеру:

#define( $block )     Hello $who #end #set( $who = 'World!' )  $block // "Hello World!" will be print

Есть возможность задать переиспользуемые части с помощью конструкции #macro:

#macro( cell ) <tr><td></td></tr> #end

Заданную в #macro конструкцию дальше в шаблоне можно вызвать так:

#cell // "<tr><td></td></tr>" will be print #cell // "<tr><td></td></tr>" will be print

В VLT-коде нам доступны функции из Java-мира. Особенно полезны функции работы со строками, например:

$NAME.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase() // replaces "SomeString" camel-case string to "some_string" snake-case string

Указанных конструкций в большинстве случаев должно хватить при написании шаблонов. Больше информации можно найти в документации по ссылкам: User Guide, VLT Reference, Glossary.

Итоги первой части

В этой части мы выделили проблему с необходимостью написания шаблонного кода и рассмотрели возможные варианты уменьшения боли. А еще познакомились со встроенным инструментом File Templates и основным синтаксисом.

В следующей статье рассмотрим примеры использования File Templates для решения типовых сценариев, где требуется шаблонный код. Не переключайтесь 🙂


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *