Идиоматичный Kotlin, набор хороших практик

https://blog.philipphauer.de/idiomatic-kotlin-best-practices/
  • Перевод


Чтобы полностью раскрыть все преимущества Kotlin, пересмотрим некоторые подходы, которые мы используем в Java. Многие из них могут быть заменены на лучшие аналоги из Kotlin. Давайте посмотрим на то, как мы можем написать идиоматичный код на Kotlin.

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

Замечание: приведенный ниже список не является исчерпывающим и только выражает мое скромное мнение. Более того, некоторые фичи языка следует использовать с особой осторожностью. При злоупотреблении, они могут сделать код менее читабельным. Например, когда вы пытаетесь сжать все в одно не читаемое выражение.
Philipp Hauer


Сравнение встроенных возможностей Kotlin с общими паттернами и идиомами Java.


В Java приходится писать довольно много шаблонного кода (boilerplate code) для реализации некоторых паттернов и идиом. К счастью, многие паттерны имеют встроенную поддержку прямо в языке Kotlin или в его стандартной библиотеке:
Java идиомы или паттерны Реализация в Kotlin
Optional Nullable-значения
Геттеры, сеттеры, Backing field Свойства (properties)
Статический класс для утилит Функции верхнего уровня, функции-расширения
Неизменяемость (Immutability), Объекты значений (Value Objects) data class с неизменяемыми свойствами, copy()
Fluent Setter (Wither) Именованные аргументы, и аргументы со значением по умолчанию, apply()
Цепочка методов (Method Chaining) Аргументы со значением по умолчанию
Синглтон (Singleton) object
Делетагы (Delegation) Делегирование свойств by
Ленивая инициализация (потоко-безопасная) Делегирование свойств by: lazy()
Наблюдатель (Observer) Делегирование свойств by: Delegates.observable()

Функциональное программирование


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

— менее подверженным ошибкам
— более легким для понимания
— проще тестируемым
— потоко-безопасным

По сравнению с Java 8, у Kotlin лучше поддержка функционального программирования:
— неизменяемость, val для переменных и свойств, неизменяемые data classes, copy()
— все выражения возвращают результат: if, when и try-catch являются выражениями. Можно их комбинировать с другими выражениями и функциями.
— функции как типы первого класса
— краткие лямбда выражения
— Kotlin API коллекций

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

Использование выражений:


// Don't
fun getDefaultLocale(deliveryArea: String): Locale {
    val deliverAreaLower = deliveryArea.toLowerCase()
    if (deliverAreaLower == "germany" || deliverAreaLower == "austria") {
        return Locale.GERMAN
    }
    if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") {
        return Locale.ENGLISH
    }
    if (deliverAreaLower == "france") {
        return Locale.FRENCH
    }
    return Locale.ENGLISH
}

// Do
fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) {
    "germany", "austria" -> Locale.GERMAN
    "usa", "great britain" -> Locale.ENGLISH
    "france" -> Locale.FRENCH
    else -> Locale.ENGLISH
}

Практическое правило: каждый раз когда вы пишете if имейте ввиду, что его можно заменить на более короткую запись с помощью when.

try-catch так же полезное выражение:

val json = """{"message":"HELLO"}"""
val message = try {
    JSONObject(json).getString("message")
} catch (ex: JSONException) {
    json
}

Функции верхнего уровня, функции-расширения


В Java мы часто создаем статичные классы со статичными методами для утилит. Непосредственная реализация этого паттерна в Kotlin будет выглядеть следующим образом:

//Don't
object StringUtil {
    fun countAmountOfX(string: String): Int{
        return string.length - string.replace("x", "").length
    }
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")

Kotlin позволяет убрать ненужные оборачивания в класс при помощи функций верхнего уровня. Часто, мы так же можем добавить некоторые функции расширения, для повышения читабельности. Так, наш код становится больше похожим на «рассказ истории».

//Do
fun String.countAmountOfX(): Int {
    return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX() 

Именованные аргументы вместо Fluent Setter.


Возвращаясь в Java, fluent setters (так же называемые «Wither») используются для эмуляции именованных аргументов и аргументов со значением по умолчанию. Это позволяет сделать список параметров более читабельным и менее подверженным ошибкам:

//Don't
val config = SearchConfig()
       .setRoot("~/folder")
       .setTerm("kotlin")
       .setRecursive(true)
       .setFollowSymlinks(true)

В Kotlin именованные аргументы и аргументы со значением по умолчанию служат для той же цели, но в это же время являются встроенными в сам язык:

//Do
val config2 = SearchConfig2(
       root = "~/folder",
       term = "kotlin",
       recursive = true,
       followSymlinks = true
)

apply() для объединения вызовов инициализации объекта


//Don't
val dataSource = BasicDataSource()
dataSource.driverClassName = "com.mysql.jdbc.Driver"
dataSource.url = "jdbc:mysql://domain:3309/db"
dataSource.username = "username"
dataSource.password = "password"
dataSource.maxTotal = 40
dataSource.maxIdle = 40
dataSource.minIdle = 4

Функция расширения apply() помогает объединить код инициализации объекта. К тому же, нам не нужно повторять название переменной снова и снова.

//Do
val dataSource = BasicDataSource().apply {
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://domain:3309/db"
    username = "username"
    password = "password"
    maxTotal = 40
    maxIdle = 40
    minIdle = 4
}

apply() так же весьма полезен, когда взаимодействуешь с Java библиотеками из Kotlin.

Не нужна перегрузка методов для имитации аргументов со значением по умолчанию


Не нужно перегружать методы и конструкторы для реализации аргументов со значением по умолчанию (так же называемые цепочкой методов «method chaining» или цепочкой конструкторов «constructor chaining»)

//Don't
fun find(name: String){
    find(name, true)
}
fun find(name: String, recursive: Boolean){
}

Все это костыль. Для этой цели в Kotlin есть аргументы со значением по умолчанию:

//Do
fun (name: String, recursive: Boolean = true){
}

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

Краткость и лаконичность с Nullability


Избегайте if-null проверок.


Java способ проверки на null громоздкий и позволяет легко пропустить ошибку.

//Don't
if (order == null || order.customer == null || order.customer.address == null){
    throw IllegalArgumentException("Invalid Order")
}
val city = order.customer.address.city

Каждый раз, как вы пишите проверку на null, остановитесь. Kotlin предоставляет более простой способ для обработки таких ситуаций. Чаще всего, вы можете использовать безопасный вызов ?. или просто оператор «элвис» ?:

//Do
val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order")

Избегайте проверок типов


Все вышесказанное так же справедливо и для проверок типов:

//Don't
if (service !is CustomerService) {
    throw IllegalArgumentException("No CustomerService")
}
service.getCustomer()

С помощью as? и ?: можно проверить тип, автоматически преобразовать его к нужному (smart cast) или бросить исключение, если тип не тот который мы ожидаем. Все в одно выражение!

//Do
service as? CustomerService ?: throw IllegalArgumentException("No CustomerService")
service.getCustomer()

Избегайте вызовов без проверок с помощью !!


//Don't
order!!.customer!!.address!!.city

Наверняка вы обратили внимание что !! смотрятся достаточно грубо. Это практически как вы кричите на компилятор. Так это выглядит не случайно. Разработчики языка Kotlin пытаются вас слегка подтолкнуть для поиска лучшего решения, чтобы не использовать выражение, которое не может быть проверено компилятором.
«Kotlin in Action», Дмитрий Жемеров и Светлана Исакова.

Используйте let()


В некоторых ситуациях let() позволяет заменить if. Но нужно его использовать с осторожностью, чтобы код оставался читабельным. Тем не менее, я действительно хочу чтобы вы подумали об использовании let().

val order: Order? = findOrder()
if (order != null){
    dun(order.customer)
}

С let() не нужна никакая дополнительная переменная. Так что дальше мы имеем дело с одним выражением:

findOrder()?.let { dun(it.customer) }
//or
findOrder()?.customer?.let(::dun)

Использование объектов-значений


С data classes очень легко писать неизменяемые объекты-значения. Даже если они содержат лишь одно свойство. Больше нет никаких причин не использовать их.

//Don't
fun send(target: String){}

//Do
fun send(target: EmailAddress){}
// expressive, readable, type-safe

data class EmailAddress(val value: String)

Функции, состоящие из одного выражения


// Don't
fun mapToDTO(entity: SnippetEntity): SnippetDTO {
    val dto = SnippetDTO(
            code = entity.code,
            date = entity.date,
            author = "${entity.author.firstName} ${entity.author.lastName}"
    )
    return dto
}

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

// Do
fun mapToDTO(entity: SnippetEntity) = SnippetDTO(
        code = entity.code,
        date = entity.date,
        author = "${entity.author.firstName} ${entity.author.lastName}"
)
val dto = mapToDTO(entity)

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

// Do
fun SnippetEntity.toDTO() = SnippetDTO(
        code = code,
        date = date,
        author = "${author.firstName} ${author.lastName}"
)
val dto = entity.toDTO()

Предпочитайте использование параметров конструктора в инициализации свойств.


Дважды подумайте прежде чем будете использовать блок инициализации (init блок) в теле конструктора только лишь для того, чтобы инициализировать свойства.

// Don't
class UsersClient(baseUrl: String, appName: String) {
    private val usersUrl: String
    private val httpClient: HttpClient
    init {
        usersUrl = "$baseUrl/users"
        val builder = HttpClientBuilder.create()
        builder.setUserAgent(appName)
        builder.setConnectionTimeToLive(10, TimeUnit.SECONDS)
        httpClient = builder.build()
    }
    fun getUsers(){
        //call service using httpClient and usersUrl
    }
}

Следует отметить, что в инициализации свойств можно ссылаться на параметры основного конструктора (и не только в init блоке). apply() может так же помочь сгруппировать код инициализации и обойтись одним выражением.

// Do
class UsersClient(baseUrl: String, appName: String) {
    private val usersUrl = "$baseUrl/users"
    private val httpClient = HttpClientBuilder.create().apply {
        setUserAgent(appName)
        setConnectionTimeToLive(10, TimeUnit.SECONDS)
    }.build()
    fun getUsers(){
        //call service using httpClient and usersUrl
    }
} 

object для реализаций интерфейса без состояния


object из Kotlin пригодится когда нужно реализовать интерфейс фреймворка, который не хранит состояние. Для примера, интерфейс Converter из Vaadin 8.

//Do
object StringToInstantConverter : Converter<String, Instant> {
    private val DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss Z")
            .withLocale(Locale.UK)
            .withZone(ZoneOffset.UTC)

    override fun convertToModel(value: String?, context: ValueContext?) = try {
        Result.ok(Instant.from(DATE_FORMATTER.parse(value)))
    } catch (ex: DateTimeParseException) {
        Result.error<Instant>(ex.message)
    }

    override fun convertToPresentation(value: Instant?, context: ValueContext?) =
            DATE_FORMATTER.format(value)
}

Чтобы посмотреть более подробную информацию о взаимодействии Kotlin, Spring Boot и Vaadin посмотрите этот пост

Destructing


С одной стороны destructuring полезен когда необходимо вернуть несколько значений из функции. Мы можем использовать либо собственный data class (что предпочтительней), либо использовать Pair (что менее выразительно, из-за того что пара не сохраняет семантику)

//Do
data class ServiceConfig(val host: String, val port: Int)
fun createServiceConfig(): ServiceConfig {
    return ServiceConfig("api.domain.io", 9389)
}
//destructuring in action:
val (host, port) = createServiceConfig()

С другой стороны, destructuring может быть удобен и для кратного перебора элементов из map:

//Do
val map = mapOf("api.domain.io" to 9389, "localhost" to 8080)
for ((host, port) in map){
    //...
}

Специальные конструкции для создания структур


listOf, mapOf, и инфиксная функция to могут быть использованы для быстрого способа создания структур (как например JSON). Конечно, это все еще не так компактно как в Python и JavaScript, но лучше чем в Java.

Примечание: Андрей Бреслав недавно на Jpoint 2017 сказал, что они думают над тем как это улучшить, поэтому можно надеяться на некоторое улучшения в обозримом будущем

//Do
val customer = mapOf(
        "name" to "Clair Grube",
        "age" to 30,
        "languages" to listOf("german", "english"),
        "address" to mapOf(
                "city" to "Leipzig",
                "street" to "Karl-Liebknecht-Straße 1",
                "zipCode" to "04107"
        )
)

Правда, обычно приходится использовать data class или сопоставление объектов для создания JSON. Но иногда (в том числе и в тестах), такая запись весьма полезна.

Исходники


Вы можете найти исходный код на моем GitHub проекте idiomatic kotlin.

Надеюсь данный перевод вам показался полезным. Буду весьма благодарен всем тем, кто заметил какие-либо неточности или ошибки в переводе и напишет об этом в переписке.
Спасибо за внимание!
ИНФОРИОН 19,41
Компания
Поделиться публикацией
Комментарии 11
  • 0
    Функции, состоящие из одного выражения

    Ну и зачем в таком случае именованные аргументы? Разве что для author.


    fun SnippetEntity.toDTO() = SnippetDTO(code, date, author = "${author.firstName} ${author.lastName}")
    • 0
      В данном случае как раз для author. Но вообще сами по себе именованные аргументы весьма полезны. Так, например, если есть набор параметров, и у всех есть значения по умолчанию, то можно указывать значения только для нужных. Пример:

      fun someAction(repeat: Int = 1, timeout: Long = 1000, log: Boolean = false, action: () -> Unit) {
         // -----
      }
      
      //use 1
      someAction(repeat = 3) {
         println("Hi!")
      }
      
      //use 2
      someAction(timeout  = 5000, log = true) {
         println("Message, after timeout")
      }
      
      • 0

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

    • 0

      Все хорошо, но именно так в java тоже уже не пишут. Есть ломбок, есть другие способы.


      Кроме того, fluent API (в виде builder скажем) далеко не всегда такой простой и очевидный, как у вас в примере. И не заменяется просто именованными параметрами.

      • 0
        Kotlin позволяет убрать ненужные оборачивания в класс при помощи функций верхнего уровня.


        А такие функции можно переиспользовать
        • 0
          А такие функции можно переиспользовать

          Если под переиспользовать подразумевается переопределить, то ответ — нет.

          Функция верхнего уровня в Kotlin:
          fun test(): Unit {}   // in file Test.kt
          

          В Java будет выглядеть в виде следующего:
          public final class TestKt {
             public static final void test() {
             }
          }
          

          А в Java статичные функции переопределять нельзя, как и в Kotlin.
          • 0
            извиняюсь, отправил раньше чем дописал, а отредактировать не могу, имел ввиду переиспользовать в других классах, где они явно не объявлены, например в классе A определили
            fun String.countAmountOfX(): Int {
                return length - replace("x", "").length
            }
            


            могу ли я в классе B вызвать ее у какой либо строки?
            • 0
              Если такая функция будет в классе A то это уже обычная функция класса (точнее в том виде как у вас это еще и функция расширение). В таком виде ее нельзя будет напрямую использовать в классе B.

              Но если вы такую функцию объявите не в классе (или скажем в объекте или объекте компаньоне) то такую функцию можно будет вызвать:
              Пример 1 с функцией верхнего уровня:
              файл test/A.kt:
              package test
              
              fun String.countAmountOfX(): Int {
                  return length - replace("x", "").length
              }
              

              файл test/B.kt:
              package test
              
              class B {
                  fun testCall(): Unit {
                      "asdf".countAmountOfX()
                  }
              }
              

              Пример 2 с объектом компаньоном:
              файл test/A.kt:
              package test
              
              class A {
                  companion object {
                      fun String.countAmountOfX(): Int {
                          return length - replace("x", "").length
                      }
                  }
              }
              

              файл test/B.kt:
              package test
              
              import test.A.Companion.countAmountOfX
              
              class B {
                  fun testCall(): Unit {
                      "asdf".countAmountOfX()
                  }
              }
              

              Пример 3 с функцией в объекте:
              файл test/A.kt:
              package test
              
              object A {
                  fun String.countAmountOfX(): Int {
                      return length - replace("x", "").length
                  }
              }
              

              файл test/B.kt:
              package test
              
              import test.A.countAmountOfX
              
              class B {
                  fun testCall(): Unit {
                      "asdf".countAmountOfX()
                  }
              }
              

              Надеюсь что ответил на ваш вопрос.
              • 0
                спасибо большое, мог бы лайкнуть сделал бы это!
        • 0
          deleted
          • 0

            Категорически поддерживаю:


            Избегайте проверок типов

            Вот только почему после этого идёт as?, ведь это и есть проверка типа.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое