Kotlin
February 27, 2023

Самодостаточный системный подход с применением мультиплатформенного Kotlin

Готов ли мультиплатформенный Kotlin для создания полностековых (веб-)сервисов? Как такая разработка воспринимается с точки зрения тех, кто уже имеет опыт работы с Kotlin? Поделюсь моим опытом по созданию веб-UI для JVM-микросервиса при помощи Kotlin Multiplatform.

Введение

Не буду здесь вдаваться в детали о том, с какой целью применяется микросервисный подход, а также не стану углубляться в теорию микросервисов. Начнём этот пост с допущения, что вы хотите улучшить микросервисный ландшафт, имеющийся у вас в настоящий момент, либо собираетесь мигрировать на микросервисную систему, чтобы улучшить удобство использования и/или администрирования – предоставив для этого веб-UI. Идеально, если при этом вы уже знакомы с Kotlin.

История

Наша команда занималась разработкой гигантских приложений Java EE для бэкенда – это был один из недавних проектов, растянувшийся на несколько лет. За годы работы мы концептуально разработали, как дробить эти приложения на несколько микросервисов, а также как переносить бизнес целиком в облако. В частности, мы занимались: технологическими и организационными преобразованиями, изучением и практикой новых технологий и технологических подходов, параллельной обработкой в старых и новых условиях, притом, что сам бизнес постоянно рос и менялся.

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

Прежде всего, он шёл на пользу не только разработчику, который мог «в лёгкую» обращаться с действующими приложениями, но и пользователю, которому было не менее удобно. К актуальному состоянию базы данных можно было обращаться, ничего не зная о базе данных. В том числе, можно было не допускать к управлению доступом к базе данных никого кроме разработчиков. События или синхронизационные действия можно было выводить на экран или инициировать без какой-либо технической подготовки. Поэтому наша цель была такой: в закрепляющемся микросервисном ландшафте эти возможности, предоставляемые через веб-UI, должны каким-то образом сохраниться.

Прежде, чем продолжить, разберём некоторые концепции.

Концепции

Микросервис без UI

Прежде всего, вопрос: нужен ли вообще UI нашему (микро)сервису? Как всегда, смотря по ситуации. Например, если вашему сервису не требуется что-либо «показывать», например, при управлении персистентностью или внутренним состоянием, либо если не окупятся усилия и время, потраченные на разработку UI (достаточно будет простой конечной точки API), то, вероятно, стоит обойтись без UI. Другой момент: есть ли у вас нужные ресурсы и знания для разработки UI?

Полный клиент

Рассмотрим пример: обращённый к пользователю монолитный веб-UI (Angular, React, т.п.), подкреплённый несколькими конечными точками на бекенде. Такие клиенты называются «полнофункциональными» или «толстыми».

Подход с плагинами

Можно вместо одного толстого UI, можно сделать несколько самостоятельных компонентов UI, сопряжённых вместе. Организовать и оркестровать разработку этих компонентов можно по-разному. Например, сделать одиночный репозиторий или несколько независимых, разрабатывать всё одной командой или несколькими. В данном случае принято говорить о «микрофронтендах».

Самодостаточная система (встраиваемый UI)

При подходе с созданием самодостаточной системы мы изолируем сам микросервис от возможностей его UI. В таком случае и UI, и сам сервис обычно выполняются в одном и том же процессе и развёртываются единым блоком. Именно этого подхода мы будем придерживаться в рамках данной статьи, так как он идеально сочетается с Kotlin Multiplatform.

Kotlin Multiplatform

Можно создать микросервис и UI к нему, воспользовавшись классическим подходом: держать наш код для JVM в одном пакете, а весь материал для UI – в другом, изолировав их друг от друга. Разделяться между этими пакетами ничего не будет, но сетевая конфигурация у них будет одинаковая. Такой подход хорош, например, в случае с сервисом на Spring-Kotlin, предоставляющим UI на Vue.js. Вывод клиентской части вставляется в classpath серверной части и предоставляется при помощи REST-контроллера. Работает хорошо, но как улучшить такую структуру?

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

Kotlin Multiplatform позволяет извлекать общий код – разделяемый между клиентской и серверной частью – и служит мостом между двумя мирами. Вы определяете общие внутренние модели, конечные точки, константы, функции, тесты, т.д. в центральном пакете или модуле, а ваши сервер и клиент всегда остаются синхронизированы, совместно работая с общей базой. Код становится разделяем между платформами. Ещё одно достоинство: работая с Kotlin Multiplatform, можно не отказываться от Kotlin!

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

Kotlin отлично подходит для разработки JVM-приложений, и уже очень хорошо известен в этом сообществе. Он применяется как для разработки сервисов с нуля, так и для переписывания унаследованного кода Java, интеграции нового кода на Kotlin в существующую базу кода на Java. Притом, что Kotlin очень хорош для разработки серверной части, концепция использовать Kotlin для разработки на клиенте сравнительно нова. Компания JetBrains поддерживает несколько Kotlin-обёрток, в частности, для React, Mocha или компонентов, работающих со стилями. В сообществе известны и иные решения. В Gradle предлагается несколько способов управления и объединения такого кода в пакеты при помощи webpack и для интеграции с npm при помощи yarn.

Kotlin/JS позволяет транспилировать код Kotlin, стандартную библиотеку Kotlin и любые совместимые зависимости в JavaScript.

К делу

Сервис, написанный для этого поста, я выложил на Github: https://github.com/jbilandzija/kotlin-multiplatform-sample

Здесь я использую очень простой бэкенд, основанный на Ktor, а также очень простой фронтенд, где применяется KotlinJS и React. Если вы ещё не слышали о Ktor, расскажу: это отличный инструмент из числа микрофреймворков для JVM, позволяющий создавать легковесные сервисы. Ktor отлично интегрируется с Kotlin и корутинами! Если вы предпочитаете работать с Spring или другим фрейморком, который вам нравится – да, можно и так.

Сервис состоит из трёх наборов исходников (не считая тестов): jvmMain, jsMain и commonMain. Эти наборы исходников (модули) определяются в главном файле Gradle. Существуют и иные варианты конфигурации. Мы подразделим код на разные модули и скомпилируем их, соответственно, под JVM и JS. У нас будут выделенные зависимости Ktor/Server и выделенные зависимости KotlinJS/Client.

Клиентская часть

«Традиционно» клиентская часть реализутся на том или ином JS-фреймворке, далее компилируется и складывается в общую папку со сборками Gradle, а далее каким-либо образом предоставляется для использования в серверной части. В качестве альтернативы можно было бы использовать какой-нибудь движок-шаблонизатор. В данном примере код получается сравнительно переплетённым, то есть, и клиент, и сервер могут совместно использовать код, лежащий в одной и той же папке. Это достигается при помощи обёрток Kotlin – в принципе, это библиотеки предметно-ориентированного языка на основе Kotlin. Мы пишем HTML, CSS и JavaScript на типобезопасном Kotlin.

После компиляции получаем два артефакта. Чтобы скомбинировать их оба в единый толстый JAR-архив, воспользуюсь плагином Shadow. С этим одиночным JAR можно обращаться так же, как и с любым другим (микро)сервисным JAR, который мог бы быть у нас и без встроенного UI. Сделаем из него контейнерный образ и загрузим на нашу любимую Platform-as-a-Service или подобный носитель.

Строим UI

В данном примере я пользуюсь React, в том числе, функциями React по управлению состоянием и жизненным циклом. Для этого используются компоненты функций, обёрнутые в DSL для Kotlin. Если эта тема для вас нова, можете познакомиться с ней здесь.

Мой пример структурирован в виде двух функциональных компонентов, InputComponent и OutputComponent. В структуре компонента InputComponent определяются внешний вид и поведение раздела Input. В компоненте OutputComponent отображается серверный вывод.

Именно от вас и вашей команды зависит, как структурировать компоненты. Я придерживался такого подхода: сверху определяем переменные и обработчики, далее идёт структура и вложенные настройки CSS. См. пример.

Тестирование

Тестирование Kotlin Multiplatform – тема, заслуживающая отдельного поста, она выходит за рамки проводимого здесь прототипирования. Для тестирования можно воспользоваться kotlin-test-js или kotest.io.

Модуль kotlin-test-js – это реализация обычных тестовых утверждений и аннотаций, прямо из коробки обеспечивающая поддержку тестировочных фреймворков Jasmine, Mocha и Jest. В качестве эксперимента она позволяет подключить их к вашему собственному фреймворку для модульного тестирования.

Серверная часть

У меня в примере в серверной части расположено две конечные точки для веб-UI, сконфигурированные в плагине маршрутизации. Корневая конечная точка предоставляет index.html и статические ресурсы, в том числе, главный JS-файл (kt-multiplatform-sample.js). Вторая конечная точка предназначена для работы с вызовами к нашему веб-UI. Путь к конечной точке конфигурируется централизованно. Сервер и клиент используют одну и ту же константу. В данном случае хорошо и то, что не требуется беспокоиться о переименовании путей API или версионировании API.

Заключение

Пока сложно принять окончательное решение о том, использовать ли такую незрелую технологию. Но готов ли Kotlin Multiplatform для построения полностековых (веб-)сервисов? Я считаю, что пока нет. Объясню, почему.

Я уже некоторое время экспериментирую как с вышеприведённым практическим примером, так и с очень похожим проектом. Природа молодых технологий такова, что после релиза новых зависимостей в них многое меняется; в особенности это касается технологий на стадии альфа. Мне пришлось немало поработать, пока у меня получился сервис на Kotlin Multiplatform, выглядевший и работавший именно так, как мне хотелось. После выпуска новых зависимостей мне приходилось многое перерабатывать, прежде чем мой сервис возвращался к приемлемому виду.

Каково мне заниматься такой разработкой, если моя специализация – бэкенд на Kotlin? Обращаться с этими новыми DSL для Kotlin было нормально. Писать на HTML и CSS на Kotlin немного странно, но мне нравилось заменять JavaScript на Kotlin. В обёртках, написанных на kotlin, много всякой магии, и мне приходилось повозиться с библиотеками, так как документация по ним – большая редкость.

Если у вас есть микросервисы на Kotlin, и вы планируете надстроить над ними пользовательский интерфейс – определённо имейте в виду Kotlin Multiplatform. Но я рекомендую немного подождать, пока эта технология дозреет.

Источник