📜 ⬆️ ⬇️

Idiom Kotlin, a set of good practices



To fully unleash the full benefits of Kotlin , we’ll review some of the approaches we use in Java . Many of them can be replaced with the best counterparts from Kotlin . Let's take a look at how we can write idiomatic code in Kotlin .

I present to your attention the translation of the article about the idiomatic Kotlin . In many ways, the author has shown quite well how to write on Kotlin and use the built-in language features for writing simple and expressive code.

Note: the list below is not exhaustive and only expresses my humble opinion. Moreover, some language features should be used with extreme caution. When abused, they can make the code less readable. For example, when you are trying to compress everything into one unreadable expression.
Philipp Hauer

')

Comparison of Kotlin’s built-in capabilities with common patterns and Java idioms.


In Java, you have to write quite a lot of boilerplate code to implement some patterns and idioms. Fortunately, many patterns have built-in support right in the Kotlin language or in its standard library:
Java idioms or patternsImplementation in Kotlin
OptionalNullable values
Getters, setters, Backing fieldProperties (properties)
Static class for utilitiesTop-level functions, extension functions
Immutability, Value Objectsdata class with immutable properties, copy()
Fluent Setter (Wither)Named arguments, and arguments with a default value, apply()
Method ChainingArguments with default value
Singletonobject
DelegationDelegating properties by
Lazy initialization (thread safe)Delegating properties by: lazy()
ObserverDelegating properties by: Delegates.observable()

Functional programming


Among other advantages, functional programming allows to reduce side effects, which in turn makes the code:

- less error prone
- easier to understand
- easier to test
- flow safe

Compared to Java 8 , Kotlin has better functional programming support:
- immutability, val for variables and properties, immutable data classes , copy()
- all expressions return a result: if , when and try-catch are expressions. You can combine them with other expressions and functions.
- functions as first class types
- brief lambda expressions
- Kotlin API collections

All this allows us to write a functional code in a safe, concise and expressive way. And as a result, it is possible to write pure functions (without side effects) much easier.

Using expressions:


 // 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 } 

The rule of thumb is: every time you write if keep in mind that it can be replaced by a shorter entry with when .

try-catch is also a useful expression:

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

Top-level functions, extension functions


In Java, we often create static classes with static methods for utilities. The direct implementation of this pattern in Kotlin will look like this:

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

Kotlin allows you to remove unnecessary wraps in a class using top-level functions. Often, we can also add some extension functions to improve readability. So, our code becomes more like a “story telling”.

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

Named arguments instead of Fluent Setter .


Returning to Java , fluent setters (also called "Wither") are used to emulate named arguments and arguments with a default value. This allows you to make the list of parameters more readable and less prone to errors:

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

In Kotlin, named and default arguments are used for the same purpose, but are also built into the language at the same time:

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

apply() to combine object initialization calls


 //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 

The apply() extension function helps to merge object initialization code. In addition, we do not need to repeat the name of the variable over and over again.

 //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() is also very useful when interacting with Java libraries from Kotlin .

No overloading of methods to simulate arguments with a default value.


No need to overload methods and constructors to implement arguments with a default value (also called a method chain of "method chaining" or a chain of constructors of "constructor chaining")

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

All this is a crutch. For this purpose, Kotlin has arguments with a default value:

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

In fact, the default arguments remove almost all cases of method and constructor overloads, because the overload is mostly used to create arguments with a default value.

Brevity and conciseness with Nullability


Avoid if-null checks.


The Java method of checking for null cumbersome and makes it easy to miss the error.

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

Every time you write a null check, stop. Kotlin provides an easier way to handle these situations. Most often, can you use a secure call ?. or just an operator "elvis" ?:

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

Avoid type checking


All of the above is also true for type checks:

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

Using as? and ?: you can check the type, automatically convert it to a smart cast, or throw an exception if the type is not the one we expect. All in one expression!

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

Avoid calling without checks with !!


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

Surely you noticed that !! look pretty rough. It's almost like you shout at the compiler. So it does not look random. The Kotlin language developers are trying to slightly push you to find the best solution so as not to use an expression that cannot be verified by the compiler.
Kotlin in Action, Dmitry Zhemerov and Svetlana Isakova.

Use let()


In some situations, let() allows you to replace if . But you need to use it with care so that the code remains readable. However, I really want you to think about using let() .

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

With let() no additional variable is needed. So further we deal with one expression:

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

Using Value Objects


It is very easy to write immutable value objects with data classes . Even if they contain only one property. There is no longer any reason not to use them.

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

 //Do fun send(target: EmailAddress){} // expressive, readable, type-safe data class EmailAddress(val value: String) 

Functions consisting of a single expression


 // 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 } 

With functions consisting of a single expression, and named arguments, we can simply, briefly and expressively describe the relationship between objects:

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

If you prefer the extension functions, you can, using them, make the announcement and use at the same time more concise and expressive. At the same time, we do not pollute our value objects with additional logic.

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

Prefer using constructor parameters in property initialization.


Think twice before you use the initialization block (init block) in the body of the constructor just to initialize the properties.

 // 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 } } 

It should be noted that in the initialization of properties you can refer to the parameters of the main constructor (and not only in the init block). apply() can also help group initialization code and get by with one expression.

 // 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 for stateless implementations


object from Kotlin is useful when you need to implement a framework interface that does not store state. For example, the interface Converter from 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) } 

To see more detailed information on Kotlin, Spring Boot and Vaadin interactions , see this post.

Destructing


On the one hand, destructuring is useful when it is necessary to return several values ​​from a function. We can use either our own data class (which is preferable), or use Pair (which is less expressive, due to the fact that the pair does not preserve the semantics)

 //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() 

On the other hand, destructuring can also be convenient for multiple iteration of map elements:

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

Special structures to create structures


listOf , mapOf , and infix function to can be used to quickly create structures (such as JSON ). Of course, this is still not as compact as in Python and JavaScript , but better than in Java.

Note: Andrey Breslav recently at Jpoint 2017 said that they are thinking about how to improve this, so we can hope for some improvements in the foreseeable future.

 //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" ) ) 

True, you usually have to use the data class or object mapping to create JSON. But sometimes (including in tests), such a record is very useful.

Sources


You can find the source code on my GitHub project idiomatic kotlin .

I hope this translation seemed useful to you. I would be very grateful to all those who noticed any inaccuracies or errors in the translation and write about it in correspondence.
Thanks for attention!

Source: https://habr.com/ru/post/328218/


All Articles