📜 ⬆️ ⬇️

Kotlin: Without Best Practices, life is not the same. Part 1

Hi, Habr! This article is about the most pressing problems when programming in Kotlin. In particular, I will touch on several topics that cause the most ambiguity - the use of it in lambda expressions, the abuse of functions from the Standard.kt file, and the brevity of writing vs. code readability.

Prehistory


I started looking at Kotlin about a year ago (starting at Milestone 12) and actively used it to write my Android applications. After two years of writing Android applications in the Java language, writing on Kotlin was a breath of fresh air - the code was much more compact (no anonymous classes to you, functional features appeared), and the language itself is much more expressive (extension functions, lambda functions) and safer ( null safety).

When the language came out, I began to write my new project at work without a doubt, praising it at the same time to my colleagues (in my small company I am the only Android developer, the rest are developing Java client-server applications in Java). I understood that after me the new team member would have to learn this language, which in my opinion was not a problem in this case - this language is very similar to Java and after 3-5 days after reading the official documentation, you can begin to write confidently on it.

After some time, I began to notice that in some cases you need to beat your hands and write a longer but understandable code than a short and less clear one. Example:
')
//   ,         Safe-call ("?."),        val user = response?.user ?: return val name = user.name.toLowerCase() //  ,          val name = response?.user?.name?.toLowerCase() ?: return 

Since I was the only programmer, I quickly understood this pattern and implicitly developed a rule for myself to prefer the readability of the code of its brevity. Everything would be nothing until we took an internship Android programmer for an internship. As I expected, after reading the official documentation on the language, he quickly mastered Kotlin, having Java programming experience, but then strange things started to happen: each code review caused half-hour (and sometimes hour) discussions between us on what language constructions are best used in certain situations. In other words, we began to develop a programming style for Kotlin in our company. I believe that these discussions arose for the reason that the documentation, which is the entry point to the world of Kotlin, does not provide the very Best Practices, namely when it is better NOT to use these features and what is better to use instead. That is why I decided to write this article.

At once I want to stipulate that I am not trying to prove the truth of my statements, but I am trying to discuss how to properly write all such things on Kotlin.

Language problems


"It" callback hell


This problem lies in the fact that in Kotlin it is allowed not to name the only parameter of the callback function. It will have the name "it" by default. Example:

  /**    callback,         */ fun execute(callback: (Any?) -> Unit) { ... callback(parameter) ... } /**  . Kotlin    execute { ... },   execute({ ... }),     */ execute { if (it is String) { //   parameter   it,      String .... } .... } 

However, when we have several nested functions, confusion can arise:

 execute { execute { execute { if (it is String) { // it       execute .... } .... } } } execute { execute { execute { parameter -> if (it is String) { //  it        execute,        .... } .... } } } 

On small fragments when this may not seem like such a problem, however, if several people work on the code and such a function with a nested call has 10-15 lines, then it is easy to lose who actually owns it at this nesting level. The situation worsens if the name it is used for some operation in each level of nesting. In this case, the understanding of such a code greatly deteriorates.

 executeRequest { //  it -    Response if (it.body() == null) return executeDB { //  it -    DatabaseHelper it.update(user) executeInBackgroud { //  it -    Thread if (it.wait()) ... .... } } } 

Here is a discussion on the readability of code using it. My opinion is that it helps a lot to shorten the code and makes it clearer for simple functions, but once we deal with a nested callback function, it’s better to name the parameters of both functions:

  //   executeInBackgroud { if (it.wait()) ... .... } //   executeRequest { response -> if (response.body() == null) return executeDB { dbHelper -> dbHelper.update(user) ... } } 

Abuse of functions from the Standard.kt file


For those who do not know, the Standard.kt file contains many useful functions. Here is a detailed description of what each of them is for.

Problems with these functions begin when the programmer starts using them too often.

The first example is the let function, which essentially performs 2 tasks: allows you to call code if a value is not null and shift this value to the variable it:

 response?.user?.let { val name = it.name //  it    user } 

The first drawback of this function overlaps with the topic of the previous section - the variable it appears, which adds possible errors. The second drawback is that using this function, the code is not read as English text. Much better to write as follows:

 val user = response?.user ?: return val name = user.name 

Third, let adds an extra level of indentation, which degrades the readability of the code. You can read about this feature here , here and here . My opinion is that this function is not needed at all in the language, the only advantage of it is help with null safety. However, even this plus can be solved in other more elegant and understandable ways (preliminary check for null with?: Or just if).

As for the other functions, they should be applied extremely rarely and carefully. Take, for example, with. It allows you not to specify each time the object on which you want to call the function:

 with(dbHelper) { update(user) delete(comment) } //    : dbHelper.update(user) dbHelper.delete(comment) 

The problem begins where these calls are mixed with other code that is not related to the dbHelper object:

 with(dbHelper) { val user = query(user.id) user.name = name user.address = getAddress() // getAddress()     dbHelper .... update(user) val comment = getLatestComment() // getLatestComment()      dbHelper .... delete(comment) } 

In this case, it is necessary to constantly monitor who actually owns one or another function, which significantly reduces readability. I will not give an example with nested use with, and so it is clear what kind of spaghetti code will end up.

I will write about other painful things in the next article, because it has already managed to grow.

Update:

It so happened that while I was preparing the material for the second part of the article, a video of the presentation of Anton Keks was published, which not only fully covered all the points of my second article, but also contained some additional important points. But the most important thing is that this video also has developer comments. I decided that the second article would not be in the format that the first one (and will say that there is such a problem, the video says this at such and such a minute), so for now I will not write the second part, but least until I discover new problems in the language. Anyone who was waiting for the continuation, I advise you to watch the video presentation.

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


All Articles