Clean Code with Kotlin

With Kotlin we can write concise, expressive, and safe code. Sounds like clean code, doesn’t it?

In this article we will recap what clean code is, we will highlight the importance of defining meaningful names, and how to write clean functions and classes.

Finally, we will be able to learn more about the advantages of immutability, how to handle the errors in Kotlin, and what are the best practices in terms of writing tests. By the end of this blog post, you will better understand what clean code means and learn a series of tips and tricks ready to be applied in your code.

“Coding is not a sprint, is a marathon” so let’s exercise together our clean code skills.

What is Clean Code?

Uncle Bob in the book “Clean Code” mentioned that “Clean code is readable. It tells a story.”

Venkat Subramaniam in the book “Practices of an Agile Developer: Working in the Real World” emphasis the importance of expressing clear our intentions “Express your intentions clearly to the reader of the code. Unreadable code isn’t clever.”

How to measure if we have Clean Code?

Now that we defined the concept of clean code, how do we measure the quality of the code?

An unofficial measure is wtfs/min. If we have few wtf/min we have a team of happy developers, if we have many wtf/min we have a team with not so happy developers. An important thing to mention here is that wtf means “what a terrible feature”.

🔖Meaningful names

But how to write clean code? There is a set of rules that should be applied. Let’s start by talking about meaningful names.

When we write code we create packages, classes, functions, variables. And all of these elements should have names. By choosing a name we should reveal our intention for that component, even if it’s just a variable or a class. To make sure that our code is readable when we search for a name we should answer at these 3 questions: 

  • Why it exists?
  • What does it do?
  • How it is used?

To provide relevant samples we will have 2 types of code snippets: “unclean” code samples (Evil Jerry), and clean code samples (Angel Jerry).

Sample 1:

data class GetFile(val d: String, val n: String)
val pattern = Regex("(.+)/([^/]*)")
fun files(ph: String): PathParts {
val match = pattern.matchEntire(ph)
?: return PathParts("", ph)
return PathParts(match.groupValues[1],
match.groupValues[2])
}
data class PathParts(val directory: String, val fileName: String)
fun splitPath(path: String) =
PathParts(
path.substringBeforeLast('/', ""),
path.substringAfterLast('/'))

Sample 2:

class Book(val title: String?, val publishYear: Int?)
fun displayBookDetails(book: Book) {
val title = book.title
if (title == null)
throw IllegalArgumentException("Title required")
val publishYear = book.publishYear
if (publishYear == null) return
println("$title: $publishYear")
}
class Book(val title: String?, val publishYear: Int?)
fun displayBookDetails(book: Book) {
val title = book.title ?:
throw IllegalArgumentException("Title required")
val publishYear = book.publishYear ?: return
println("$title: $publishYear")
}

Sample 3:

users.filter{ it.job == Job.Developer }
.map{ it.birthDate.dayOfMonth }
.filter{ it <= 10 }
.min()
users.filter{ user -> user.job == Job.Developer }
.map{ developer -> developer.birthDate.dayOfMonth }
.filter { birthDay -> birthDay <= 10 }
.min()

By using Kotlin we are able to say more often YES to immutability:

  • Prefer to use val over var
  • Prefer to use read-only properties over mutable properties
  • Prefer to use read-only collections over mutable collections
  • Prefer to use data classes that give us copy()

⚙️Functions

What are the rules when it comes to write functions in Kotlin:

  • First rule: The functions should be small
  • Second rule: The functions should be smaller than that

Why is it not ok to have long functions?

Because long functions are:

  • Hard to test
  • Hard to read
  • Hard to reuse
  • Leads to duplication
  • Many reasons to change
  • Hard to debug

Extra-rules to be applied when we write functions in Kotlin:

  • Many arguments => group them in objects or use lists (same type)
  • A function should not have side effects => side effects are lies
  • The indent level of a function => max 2
  • if, else, while statements contain a block with one line of code
  • A long descriptive name is better than a short enigmatic name
  • Change and restructure the code until it easy enough to read it
  • Apply Command Query Separation Principle
fun parseProduct(response: Response?): Product? {
if (response == null) {
throw ClientException("Response is null")
}
val code: Int = response.code()
if (code == 200 || code == 201) {
return mapToDTO(response.body())
}
if (code >= 400 && code <= 499) {
throw ClientException("Invalid request")
}
if (code >= 500 && code <= 599) {
throw ClientException("Server error")
}
throw ClientException("Error $code")
}
fun parseProduct(response: Response?) = when (response?.code()){
null -> throw ClientException("Response is null")
200, 201 -> mapToDTO(response.body())
in 400..499 -> throw ClientException("Invalid request")
in 500..599 -> throw ClientException("Server error")
else -> throw ClientException("Error ${response.code()}")
}

Sample 2:

for (user in users) {
if(user.subscriptions != null) {
if (user.subscriptions.size > 0) {
var isYoungerThan30 = user.isYoungerThan30()
if (isYoungerThan30) {
countUsers++
}
}
}
}
var countUsersYoungerThan30WithSubscriptions = 0
for (user in users) {
if (user.isYoungerThan30WithSubscriptions) {
countUsersYoungerThan30WithSubscriptions++;
}
}

Command Query Separation Principle

The main idea is that we should divide an object’s methods into two separated categories:

  • Queries: Return a result and do not change the observable state of the system (are free of side effects).
  • Commands: Change the state of a system but do not return a value.

📰Classes

A class should be smaller than small.  It should have only one responsibility and a single reason to change. A class collaborates with a few others to achieve the desired system behaviors.

A class file is like a newspaper article:

  • The name should be simple and self-explanatory
  • The parts from the top contain the high-level concepts and algorithms
  • Details appear as we move downward
  • At the end we find the lowest level functions

Cohesion

  • Cohesion is a measure of the degree to which the elements of the class/ module are functionally related.
  • When classes lose cohesion, split them.

Coupling

  • Coupling is the measure of the degree of interdependence between the modules.
  • This isolation makes it easier to understand each element of the system.

✨✨✨Best case scenario: high cohesion and loosed coupling

Other than having high cohesion and loosed coupling, when we write classes we should follow also principles like:

  • DRY
    • Don’t Repeat Yourself
    • Applicable whenever we copy / paste a piece of code
    • It reduces the cost of development
  • KISS
    • Keep It Simple and Stupid
    • Whenever we want to implement a method to do all things
    • Simple things fail less
    • Simple is different than familiar: a for loop is familiar, but it is not necessary a simple one
  • YAGNI
    • You Ain’t Gonna Need It
    • Don’t write code which is not yet necessary 
    • How much do you know? 
    • Evaluate the cost of implementation: it is expensive to do it now, postpone it if it is possible 
  • SOLID
    • Single Responsibility Principle (SRP) states that a class or module should have one, and only one, reason to change.
    • Open-closed (OCP) – Software entities like classes, modules and functions should be open for extension but closed for modifications.
    • Liskov substitution (LSP) – Objects in a program should be replaceable with instances of their subtypes without changing the behavior of that program.
    • Interface segregation (ISP) – Classes that implement interfaces, should not be forced to implement methods they do not use. 
    • Dependency inversion (DIP) – High level modules should not depend on low level modules rather both should depend on abstraction. Abstraction should not depend on details; rather detail should depend on abstraction

💣Error handling

When we talk about how to handle the unexpected scenarios we should:

  • Prefer standard errors to custom ones.
  • Error handling is important, but if it obscures logic, it’s wrong.
  • In Kotlin we have only unchecked exceptions.
  • Prefer null or Failure result when the lack of result is possible

In Kotlin we could use Nothing whenever the functions don’t return anything.

fun computeSqrt(number: Double): Double {
if(number >= 0) {
return Math.sqrt(number)
} else {
throw RuntimeException("No negative please")
}
}

Based on the official documentation Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.”

sealed class MovieSearchResult
data class MovieFound(val movie: Movie) : MovieSearchResult()
object MovieNotFound : MovieSearchResult()
object DatabaseOffline : MovieSearchResult()
fun displayMovieResult(movieResult: MovieSearchResult) {
when(movieResult) {
is MovieFound -> println("yey, we found the movie")
is MovieNotFound -> TODO()
}
}
val <T> T.exhaustive: T
get() = this
fun displayMovieResult(movieResult: MovieSearchResult) {
when(movieResult) {
is MovieFound -> println("yey, we found the movie")
is MovieNotFound -> TODO()
}.exhaustive
}

Testing

Keep your tests clean:

  • Use same quality standards for tests as for production code and readability is again important
  • Tests should preserve and enhance:
    • Flexibility
    • Maintainability
    • Reusability
    • Readability
  • Even for tests we should use the same domain language that also helps readability.
  • Refactor the tests, just like we do with the production code.

Include only one asset per test:

  • Every test function in a JUnit test should have one and only one assert statement.
  • Given, When, Then naming (BDD – Behavior Driven Development)
  • The best thing to do is to minimize the number of asserts.
  • Test just one concept per test function.

A test should have only one reason to fail.

Respect the FIRST principle when you write tests:

  1. Fast – tests should be fast / run quickly
  2. Independent – tests should NOT depend on each other – tests should be able to be run in any order they like
  3. Repeatable – they should be repeatable in ANY environment without need for any specific infrastructure
  4. Self-validating – they should have a boolean output – pass or fail, nothing else
  5. Timely – they need to be written in a timely fashion – just before the production code is written – ensures they are easy to code against

💬Comments

The rule: Don’t add comments on the bad code, just rewrite it until it’s self-explanatory.

When we write code is almost impossible to get it right the first time, so software is never written, it is always rewritten. Don’t attach too much to your code, refactor it until is clear enough. And remember…

👩🏽‍💻Code Review Best Practices

Code Review advantages:

  1. Help to improve the quality and consistency of the code
  2. Exchange of knowledge and best practices
  3. Learn the code base
  4. New perspective(s) on your code
  5. Learn new tips and tricks about writing code 

When we talk about code review, 3 entities are involved: the author of the code, the reviewer of the code, and the code itself.

If you are the author of the code:

  • Writing the code
    • Make sure you understand your task and start a new branch for it
    • Write the code and check if your code is easy to understand or if it can be improved (refactored)
    • Respect the boy scout rule
    • Write tests and make sure your code follows the team conventions and practices
    • Format your code before commit it
    • Use tools like Ktlint or SonarKotlin to check your code before it is sent for review
  • Before the Pull Request is sent
    • Add relevant commit comments for the reviewer(s)
    • Send pull requests often and specify if the task is fully done or not
    • Have minimum 2 reviewers, one of them should be a senior
  • After your code is reviewed
    • Be humble, your code is reviewed, not you as a person
    • You are part of the team, so you are on the same side with your reviewer(s)
    • Development is continuous so also feedback is continuous
    • Know when to unlearn the old habits
    • Opportunity to learn new best practices, tips and tricks

If you are the reviewer of the code:

  • Use I… comments
    • I think…
    • I would…
    • I believe…
    • I suggest…
  • Ask questions to help the author of the code to think at different perspectives
    • Have you consider using…
    • What do you think about…
    • Have you tried to…
  • Remember that it’s about the code, not about the coder
    • This code…
    • This function…
    • This line of code…
  • Use the feedback equation to make your comments relevant and based on measurable observations

📌My summary

  1. Define with your team a set of conventions and make sure they are aware about them
  2. Justify technology use, do not use a framework or library, just because is trendy or cool
  3. Enforce good practices (XP): pair programming, coding standards, coding katas
  4. Question until you understand: make sure you know and understand what problem do you want to solve
  5. Criticize ideas, not people: remember to give honest and relevant feedback based on measurable observations
  6. Testing, testing, testing: test your code, learn about testing pyramid
  7. Integrate early, integrate often: will speed up the releases
  8. Emphasize collective ownership of code: you are part of the team, and all the team is responsible for the delivered code
  9. Prioritize and actively evaluate trade-offs: asses the pros and cons of implementing features, make decisions based on data
  10. Listen to users: we are delivering software for people, so keep your focus on your customers’ needs and feedback.

📚Learn more…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s