Kotlin ❤️ Backend - (не)Уникальный опыт

Kotlin ❤️ Backend


Какое-то время назад я проводил сравнение JVM фреймворков для разработки backend-сервисов. Задача оказалась довольно амбициозной и затратной по времени, поэтому я ее так и не доделал до конца. Но часть вещей все же удалось сравнить из описать. Я решил, что не пропадать же тексту зря и публикую здесь кусок который касается набирающего сейчас популярного фреймворка - Ktor.

Довольно молодой игрок в команде старых и замшелых зрелых JVM фреймворков. Начать писать на нем web-приложения очень просто и для быстрого старта даже есть неплохая документация. Я бы сказал, что она покрывает все нужды начинающих и продолжающих, но из-за молодости самого фреймворка порой очень тяжело находить какие-то правильные практики его использования. Даже существующие на github-е примеры весьма неоднородны и используют ряд экспериментальных API. Туториалы найденные в гугле тоже не отличаются единством подходов, из-за чего мне пришлось несколько раз переписывать приложение при открытии новых граней этого фреймворка. Это неплохо в качестве развлечения, но когда я хочу построить большое приложение, то наличие какого-то референсного шаблонного проекта отвечающего на вопрос “сейчас принято делать так” очень бы помогло. В противном случае можно получить набор разных подходов в разных частях приложения, которые открывались разработчикам в ходе эволюции проекта.

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

routing {
    authenticate("myauth1") {
        get("/authenticated/route1") {
            // ...
        }    
        get("/other/route2") {
            // ...
        }    
    }
    get("/") {
        // ...
    }
}

Понравился функционал StatusPages, который позволяет задать стандартные ответы на исключения. Очень удобно потом где-то в коде выкидывать исключение и быть уверенным, что пользователь получить адекватный ответ. Также эта штука может реагировать на HTTP коды.

Ну и самое важное наверное то, что Ktor можно назвать микрофреймворком. Со всеми плюсами и минусами отсюда вытекающими. В нем вы не увидите ORM или DI из коробки. Все это придется откуда-то брать, но при этом не все такие топики покрываются официальной документацией и примерами из официального репозитория. Поэтому в качестве DI было решено использовать Kodein. При чем скорее от безысходности, т.к. примера с Dagger 2, который по моему мнению лучше подходит для больших проектов в официальном репозитории не было, а то что удалось найти на github - выглядело странно и разбираться в этом было откровенно лень.

fun Application.libraryApp() = mainModule(Kodein {
    bind<JwtService>() with singleton { JwtService() }
    bind<PasswordHasher>() with singleton { PasswordHasher() }
})

fun Application.mainModule(kodein: Kodein, testing: Boolean = false) {
    val jwtService by kodein.instance<JwtService>()
    val passwordHasher by kodein.instance<PasswordHasher>()
		...
}

Та же история с ORM. Официальных примеров, да и вообще каких-либо упоминаний в документации я не нашел, поэтому пришлось “выйти с этим вопросом в Интернет”. Там обнаружился полуподпольный ORM Exposed от JetBrains, документация к которому была еще более скудная чем к Ktor. Но с гуглом пополам удалось это втащить и завести. Надо сказать, что до выразительности Django ORM ему очень далеко. Для сравнения приведу пример с созданием связи многие ко многим (все примеры из официальной документации):

Django ORM

class Publication(models.Model):
    title = models.CharField(max_length=30

class Article(models.Model):
    headline = models.CharField(max_length=100)
    publications = models.ManyToManyField(Publication)

Exposed

object Actors: IntIdTable() {
    val firstname = varchar("firstname", 50)
    val lastname = varchar("lastname", 50)
}
class Actor(id: EntityID<Int>): IntEntity(id) {
    companion object : IntEntityClass<Actor>(Actors)
    var firstname by Actors.firstname
    var lastname by Actors.lastname
}

object StarWarsFilmActors : Table() {
    val starWarsFilm = reference("starWarsFilm", StarWarsFilms)
    val actor = reference("actor", Actors)
    override val primaryKey = PrimaryKey(starWarsFilm, actor, name = "PK_StarWarsFilmActors_swf_act") // PK_StarWarsFilmActors_swf_act is optional here
}

class StarWarsFilm(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<StarWarsFilm>(StarWarsFilms)
    ...
    var actors by Actor via StarWarsFilmActors
    ...
}

Ну и там еще есть нюансы с созданием объектов при таких связях. Но даже без этих нюансов видна некоторая церемониальность ORM от JetBrains. При чем кажется, что дело тут далеко не в динамической природе python-а.

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

Завершить свой опыт с Ktor я решил упаковкой приложения в Docker с последующей “эмуляцией продакшена”. Тут претензий нет. Dockerfile из документации простой, понятный и заработал с первого раза. Был нюанс с описанием процесса создания fat-jar - описана очень старая версия Gradle Shadow Plugin, но больших проблем это не вызвало. Описания из репозитория плагина сняло все проблемы.

Итоговая оценка: 5 джанг из 10


Смотрите также