Послевкусие от Kotlin, часть 2

в 16:43, , рубрики: kotlin

В прошлой части я рассказывал о подводных камнях Kotlin, в этой я расскажу что их перевешивает.

Меня несколько раз спрашивали, а что же такого в Kotlin, что могло бы сподвигнуть на переход с Java, в чем его фишка? Да, Kotlin привнес Null-safety и большое количество синтакcического сахара, закрыл часть слабых мест Java. Но это не становится поводом для перехода. Что может стать? Новые возможности и новая философия.

Послевкусие от Kotlin, часть 2 - 1

Новые возможности

1. Null safety

Об этом говорится в первую очередь. Это действительно делает код более безопасным. Да, есть проблема при вызове Java. Сводится она к двум вариантам:

  1. Вызов сторонних библиотек. Лечится либо явным объявлением типа переменной, к которой присваивается результат, либо написанием extension, что и так часто хочется делать для выпрямления цепочки вызовов (пример будет в конце).
  2. Получение сообщения из внешнего мира (REST, MQ и т.д.). Тут на помощь приходит Spring Validate.

2. Корутины

Сам ещё не использовал, но, судя по всему, это может сильно изменить подход к многопоточному программированию.

3. Компиляция в JavaScript

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

Новая философия

Философия Kotlin — Concise, Safe, Interoperable, Tool-friendly (с оф. сайта и докладов).

1. Interoperable

Совместимость с Java действительно 100%-ая в обе стороны. Чтобы я ни делал, всё работало отлично.

2. Tool-friendly

На Eclipse не пробовал, но в Intellij всё замечательно, и продолжает улучшаться.

3. Safe

На мой прогматичный взгляд — это самое главное. Concise, Interoperable, Tool-friendly — это минимаотные условия выживания языков на JVM, иначе они будут проигрывать Java.

Null-safety + mutability

К примеру, локальная переменная — list из строк. В java компилятор знает только о двух вариантах:

List<String> list1;
final List<String> list2;

Второй вариант встречается в единичных случаях. Обычно, это не 8-ая Java, и этот list нужен в анонимном классе.
А вот Kotlin:

val list1: List<String>?
val list2: List<String?>?
val list3: List<String>
val list4: List<String?>
val list5: MutableList<String>?
val list6: MutableList<String?>?
val list7: MutableList<String>
val list8: MutableList<String?>
var list9: List<String>?
var list10: List<String?>?
var list11: List<String>
var list12: List<String?>
var list13: MutableList<String>?
var list14: MutableList<String?>?
var list15: MutableList<String>
var list16: MutableList<String?>

Что это дает? У каждого типа есть свои гарантии и набор разрешенных над ним операций. Так, += null можно вызывать только на var list12: List<String?>, а add(null) — на val list8: MutableList<String?> и var list16: MutableList<String?>.
При каждом объявлении писать полный тип будет накладно. Поэтому есть вывод типов:

val test = Random().nextBoolean()
val list1 = if (test) null else listOf("")
val list2 = if (test) null else listOf(null, "")
val list3 = listOf("")
val list4 = listOf(null, "")
val list5 = if (test) null else mutableListOf("")
val list6 = if (test) null else mutableListOf(null, "")
val list7 = mutableListOf("")
val list8 = mutableListOf(null, "")

var list9 = list2?.filterNotNull()
var list10 = list2
var list11 = list2?.filterNotNull() ?: emptyList()
var list12 = list2 ?: emptyList()
var list13 = list2?.filterNotNull()?.toMutableList()
var list14 = list2?.toMutableList()
var list15 = list2?.filterNotNull()?.toMutableList() ?: mutableListOf()
var list16 = list2?.toMutableList() ?: mutableListOf()

Когда пишешь код, не хочется лишнее поле объявлять как nullable, чтобы позже не писать? и ?:, а большая часть операций приводит к immutable коллекциям. В итоге, в коде объявляются самые узкие состояния, что дает более строгие контракты и снижает сложность программы.

Поддержка Safe другими возможностями языка

  1. Естественным образом хочется уходить от конструктора без параметров и сеттеров для полей, так как конструктор без параметров будет создавать некомпилируемое состояние объекта — null логин пользователя, к примеру.
  2. Язык поощряет отсутствие локальных переменных — нет промежуточных состояний.
  3. Удешевление DTO — data class. Так мы можем ослабить контроль над состояниями, когда передаем объект в UI, не ослабляя контракты модели.
  4. Удешевление перегрузки методов — параметры по умолчанию — нет соблазна писать кучу setters.
    Пример

    data class Schedule(
        val delay: Int,
        val delayTimeUnit: TimeUnit = TimeUnit.SECONDS,
        val rate: Int? = null,
        val rateTimeUnit: TimeUnit = TimeUnit.SECONDS,
        val run: () -> Unit
    )
        
    fun usage() {
        Schedule(1) {
            println("Delay for second")
        }
        Schedule(100, TimeUnit.MILLISECONDS) {
            println("Delay for 100 milliseconds")
        }
        Schedule(1, rate = 1) {
            println("Delay for second, repeat every second")
        }
    }
    

То что может появиться

  1. Inline classes/Value classes. Позволит делать классы-обертки вокруг примитивов, при этом компилировать без этого класса. Можно будет, к примеру, сделать два типа строк: login и email, которые не будут друг к другу каститься. Слышал об этом на Jpoint.
  2. Truly immutable data. Поддержка на уровне синтаксиса изменяемости объекта. Immutable объект не сможет содержать ссылки на не immutable и как-либо их изменять. Заняло третье место при голосовании за новые возможности языка.

4. Concise (примеры из моего проекта, практически как есть)

Нет ограничения «один файл — один класс»

Работа со spring data у меня выглядит так (всё в одном файле):

@Repository interface PayerRepository : CrudRepository<Payer, Int> {
    fun findByApprenticeId(id: Int): List<Payer>
}

@Repository interface AttendanceRepository : CrudRepository<LessonAttendance, LessonAttendance.ID> {
    fun findByDateBetween(from: Date, to: Date): List<LessonAttendance>
}

fun AttendanceRepository.byMonth(month: Date): List<LessonAttendance> {
    val from = month.truncateToMonth()
    val to = month.addMonths(1).subtractDays(1)
    return findByDateBetween(from, to)
}
//ещё 10 репозиторий

inline fun <reified T, ID: Serializable> CrudRepository<T, ID>.find(id: ID): T {
    return findOne(id) ?: throw ObjectNotFound(id, T::class.qualifiedName)
}

Extensions.

Выпрямляем обращение к DateUtils

Kotlin

fun isJournalBlocked(date: Date, forMonth: Date) = forMonth <= date.subtractMonths(1).subtractDays(10)
//используется ещё в 20 местах
fun Date.subtractMonths(amount: Int): Date = DateUtils.addMonths(this, -amount)
//используется ещё в 8 местах
fun Date.subtractDays(amount: Int): Date = DateUtils.addDays(this, -amount)

Java

public boolean isJournalBlocked(Date date, Date forMonth) {
    return date.compareTo(DateUtils.addDays(DateUtils.addMonths(forMonth, -1), -1)) <= 0;
}

Надо было хранить последовательности изменения некоторых параметров, для этого написал интерфейс History для хранения такого параметра и extension SortedMap<Date, out History> для выравнивания содержимого после изменений:

Реализация

interface History<out T> {
    val begin: Date
    var end: Date?
    fun historyOf(): T

    fun containsMonth(date: Date): Boolean {
        val month = date.truncateToMonth()
        return begin <= month && (end == null || month < end)
    }
}

fun <T> SortedMap<Date, out History<T>>.fix() {
    removeRepeatedNeighbors()

    val navigableMap = TreeMap<Date, History<T>>(this)
    values.forEach { it.end = navigableMap.higherEntry(it.begin)?.value?.begin }
}

private fun <T> SortedMap<Date, out History<T>>.removeRepeatedNeighbors() {
    var previousHistory: T? = null
    for (history in values.toList()) {
        if (history.historyOf() == previousHistory) {
            remove(history.begin)
        } else {
            previousHistory = history.historyOf()
        }
    }
}
//usage:
fun setGroup(from: Date, group: ClassGroup) {
    val history = GroupHistory(
            this, group, from.truncateToMonth(), null
    )
    groupsHistory[history.begin] = history
    groupsHistory.fix()
    this.group = groupsHistory.getValue(groupsHistory.lastKey()).group
}

Операции с коллекциями

Пример 1.

Kotlin

val apprentices: List<ApprenticeDTO> = apprenticeRepository.findAll()
           .map(::ApprenticeDTO)
           .sortedWith(compareBy({ it.lastName }, { it.firstName }))

Java

List<ApprenticeDTO> apprentices = StreamSupport.stream(
                   apprenticeRepository.findAll().spliterator(),
                   false
           ).map(ApprenticeDTO::new)
           .sorted(Comparator.comparing(ApprenticeDTO::getLastName)
                   .thenComparing(Comparator.comparing(ApprenticeDTO::getFirstName)))
           .collect(Collectors.toList());

Пример 2.

Kotlin

val attendances: Map<Pair<Date, Int>, Int> attendances = attendanceRepository
          .byMonth(month)
          .groupBy { it.date to it.group.id }
          .mapValues { it.value.count() }
          .toMap()

Java

Map<Pair<Date, Integer>, Integer> attendances = attendanceRepository
           .byMonth(month)
           .stream()
           .collect(Collectors.groupingBy((it) -> new Pair<>(it.getDate(), it.getGroup().getId())))
           .entrySet()
           .stream()
           .map(entry -> new Pair<>(entry.getKey(), entry.getValue().size()))
           .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));

Есть filterNot (чаще можно использовать method reference), отдельные first и firstOrNull и т.д… А если чего-то не хватает — дописываешь свой extension (я, к примеру, дописывал sum для листа BigDecimal).

Lazy

При работе с jsf это просто спасение. Jsf часто дергает одно и то же поле (а я за ним в базу хожу), а в случае таблицы с сортировкой, ожидает, что вернется ровно тот же объект, что и в прошлый раз. А главное, lazy очень легко убрать/вставить.

Smart cast + sealed class

Пример

Kotlin

fun rentForGroup(month: Date, group: ClassGroup): Int {
    val hall = group.hall
    val hallRent = hall.rent(month)
    return when (hallRent) {
        is Monthly -> hallRent.priceForMonth() / hall.groups(month).size
        is PercentOfRevenue -> hallRent.priceForMonth(creditForGroup(month, group))
        is Hourly -> hallRent.priceForLessons(group.monthLessons(month))
    }
}

Java

public int rentForGroup(Date month, ClassGroup group) {
    Hall hall = group.getHall();
    Rent hallRent = hall.rent(month);
    if (hallRent instanceof Monthly) {
        return ((Monthly) hallRent).priceForMonth() / hall.groups(month).size();
    } else if (hallRent instanceof PercentOfRevenue) {
        return ((PercentOfRevenue) hallRent).priceForMonth(creditForGroup(month, group));
    } else if (hallRent instanceof Hourly) {
        return ((Hourly) hallRent).priceForLessons(group.monthLessons(month));
    } else {
        throw new UnsupportedOperationException();
    }
}

Inline functions

На Java это просто невозможно сделать (если не добавлять явный парметр с классом).

inline fun <reified E : Throwable> assertFail(expression: () -> Unit) {
    try {
        expression()
        Assert.fail("expression must fail with ${E::class.qualifiedName}")
    } catch (e: Throwable) {
        if (e !is E) {
            throw e
        }
    }
}
@Test fun greenTest() {
    assertFail<ArrayIndexOutOfBoundsException> {
        arrayOf(1, 2)[3]
    }
}

String literals.

Разница небольшая, но шансов ошибиться намного меньше, а ещё можно из инета копипастить без головной боли.

regexp

Kotlin

val email = """^([_A-Za-z0-9-+]+(.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(.[A-Za-z0-9]+)*(.[A-Za-z]{2,}))?$"""

Java

String email = "^([_A-Za-z0-9-+]+(\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,}))?$"

Кроме того, очень приятные шаблоны.

Послевкусие

У меня только один проект на Kotlin, не считая мелких поделок. Так что могу с уверенностью сказать одно: Kotlin, Spring и элементы DDD отлично друг друга поддерживают. Если писать на Kotlin как на Java, почувствуется только синтаксический сахар (что уже приятно), но если отказаться от классических бинов, в которые кто угодно может что угодно вставить (а значит, практически нет ограничений на состояния), то Kotlin расцветет.

Автор: Нефедьев Георгий

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js