Чтобы полностью раскрыть все преимущества 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
Надеюсь данный перевод вам показался полезным. Буду весьма благодарен всем тем, кто заметил какие-либо неточности или ошибки в переводе и напишет об этом в переписке.
Спасибо за внимание!
Автор: nerumb