Reader small image

You're reading from  Kotlin Design Patterns and Best Practices - Second Edition

Product typeBook
Published inJan 2022
Reading LevelBeginner
PublisherPackt
ISBN-139781801815727
Edition2nd Edition
Languages
Right arrow
Author (1)
Alexey Soshin
Alexey Soshin
author image
Alexey Soshin

Alexey Soshin is a software architect with 15 years of experience in the industry. He started exploring Kotlin when Kotlin was still in beta, and since then has been a big enthusiast of the language. He's a conference speaker, published writer, and the author of a video course titled Pragmatic System Design.
Read more about Alexey Soshin

Right arrow

Chapter 9: Idioms and Anti-Patterns

In the previous chapters, we discussed the different aspects of the Kotlin programming language, the benefits of functional programming, and concurrent design patterns.

This chapter discusses the best and worst practices in Kotlin. You'll learn what idiomatic Kotlin code should look like and which patterns to avoid. This chapter contains a collection of best practices spanning those different topics.

In this chapter, we will cover the following topics:

  • Using the scope functions
  • Type checks and casts
  • An alternative to the try-with-resources statement
  • Inline functions
  • Implementing algebraic data types
  • Reified generics
  • Using constants efficiently
  • Constructor overload
  • Dealing with nulls
  • Making asynchronicity explicit
  • Validating input
  • Preferring sealed classes over enums

After completing this chapter, you should be able to write more readable and maintainable Kotlin code, as well as...

Technical requirements

In addition to the requirements from the previous chapters, you will also need a Gradle-enabled Kotlin project to be able to add the required dependencies.

You can find the source code for this chapter here: https://github.com/PacktPublishing/Kotlin-Design-Patterns-and-Best-Practices/tree/main/Chapter09.

Using the scope functions

Kotlin has the concept of scoping functions, which are available on any object and can replace the need to write repetitive code. Among other benefits, these scoping functions help us simplify single-expression functions. They are considered higher-order functions since each scoping function receives a lambda expression as an argument. In this section, we'll discuss all the necessary functions and execute their code blocks using objects as their scope. In this section, we'll use the terms scope and context object interchangeably to describe the objects that those functions operate on.

Let function

We can use the let() function to invoke a function on a nullable object, but only if the object is not null.

Let's take, as an example, the following map of quotes (we discussed this in Chapter 1, Getting Started with Kotlin):

val clintEastwoodQuotes = mapOf(
    "The Good, The Bad, The Ugly" to "Every...

Type checks and casts

While writing your code, you may often be inclined to check what type your object is using, is, and cast it using as. As an example, let's imagine we're building a system for superheroes. Each superhero has their own set of methods:

interface Superhero 
class Batman : Superhero { 
    fun callRobin() { 
        println("To the Bat-pole, Robin!") 
    } 
} 
 
class Superman : Superhero { 
    fun fly() { 
        println("Up, up and away!") 
    } 
} 

There's also a function where a superhero tries to invoke their superpower:

fun doCoolStuff(s: Superhero) { 
    if (s is Superman) { 
        (s as Superman).fly() 
    } 
    else if (s is Batman) { 
  ...

An alternative to the try-with-resources statement

Java 7 added the notion of AutoCloseable and the try-with-resources statement.

This statement allows us to provide a set of resources that will be automatically closed once the code is done with them. So, there will be no more risk (or at least less risk) of forgetting to close a file.

Before Java 7, this was a total mess, as shown in the following code:

BufferedReader br = null; // Nulls are bad, we know that 
try { 
    br = new BufferedReader(new FileReader
      ("./src/main/kotlin/7_TryWithResource.kt "));
    System.out.println(br.readLine());
} 
finally { 
    if (br != null) { // Explicit check 
        br.close(); // Boilerplate 
    } 
}

After Java 7 was released, the preceding code could be written as follows:

try (BufferedReader br = new BufferedReader...

Inline functions

You can think of inline functions as instructions for the compiler to copy and paste your code. Each time the compiler sees a call to a function marked with the inline keyword, it will replace the call with the concrete function body.

It makes sense to use the inline function if it's a higher-order function that receives a lambda as one of its arguments. This is the most common use case where you would like to use inline.

Let's look at such a higher-order function and see what pseudocode the compiler will output.

First, here is the function definition:

inline fun logBeforeAfter(block: () -> String) { 
    println("Before") 
    println(block()) 
    println("After") 
}

Here, we pass a lambda, or a block, to our function. This block simply returns the word "Inlining" as a String:

logBeforeAfter { 
    "Inlining" 
}
...

Implementing Algebraic Data Types

Algebraic Data Types, or ATDs for short, is a concept from functional programming and is very similar to the Composite design pattern we discussed in Chapter 3, Understanding Structural Patterns.

To understand how ADTs work and what their benefits are, let's discuss how we can implement a simple binary tree in Kotlin.

First, let's declare an interface for our tree. Since a tree data structure can contain any type of data, we can parameterize it with a type (T):

sealed interface Tree<out T>

The type is marked with an out keyword, which means that this type is covariant. If you aren't familiar with this term, we'll cover it later, while implementing the interface.

The opposite of a covariant is a contravariant. Contravariant types should be marked using the in keyword.

We can also mark this interface with a sealed keyword. We saw this keyword applied to regular classes in Chapter 4, Getting Familiar with Behavioral...

Reified generics

Previously in this chapter, we mentioned inline functions. Since inline functions are copied, we can get rid of one of the major JVM limitations: type erasure. After all, inside the function, we know exactly what type we're getting.

Let's look at the following example. We would like to create a generic function that will receive a Number (Number can either be Int or Long), but will only print it if it's of the same type as the function type.

We'll start with a naïve implementation, simply trying the instance check on the type directly:

fun <T> printIfSameType(a: Number) { 
    if (a is T) { // <== Error 
        println(a)    
    } 
}

However, this code won't compile and we'll get the following error:

> Cannot check for instance of erased type: T

What we usually do in Java, in this case, is pass the class...

Using constants efficiently

Since everything in Java is an object (unless it's a primitive type), we're used to putting all the constants inside our objects as static members.

And since Kotlin has companion objects, we usually try putting them there:

class Spock {
    companion object {
        val SENSE_OF_HUMOR = "None"
    }
}

This will work, but you should remember that companion object is an object, after all.

So, this will be translated into the following code, more or less:

public class Spock {
    private static final String SENSE_OF_HUMOR = "None";
 
    public String getSENSE_OF_HUMOR() {
        return Spock.SENSE_OF_HUMOR;
    }
    ...
}

In this example, the Kotlin compiler generates a getter for our constant, which adds another...

Constructor overload

In Java, we're used to having overloaded constructors. For example, let's look at the following Java class, which requires the a parameter and defaults the value of b to 1:

class User { 
    private final String name; 
    private final boolean resetPassword; 
    public User(String name) { 
        this(name, true); 
    } 
 
    public User(String name, boolean resetPassword) { 
        this.name = name; 
        this.resetPassword = resetPassword; 
    } 
}

We can simulate the same behavior in Kotlin by defining multiple constructors using the constructor keyword:

class User(val name: String, val resetPassword: Boolean) {
    constructor(name: String) : this(name, true)
}

The secondary...

Dealing with nulls

Nulls are unavoidable, especially if you work with Java libraries or get data from a database. We've already discussed that there are different ways to check whether a variable contains null in Kotlin; for example:

// Will return "String" half of the time and null the other 
// half 
val stringOrNull: String? = if (Random.nextBoolean()) 
  "String" else null  
 
// Java-way check 
if (stringOrNull != null) { 
    println(stringOrNull.length) 
}

We could rewrite this code using the Elvis operator (?:):

val alwaysLength = stringOrNull?.length ?: 0 

If the length is not null, this operator will return its value. Otherwise, it will return the default value we supplied, which is 0 in this case.

If you have a nested object, you can chain those checks. For example, let's have a Response object that contains a Profile, which, in turn, contains the first name and last name fields, which...

Making asynchronicity explicit

As you saw in the previous chapter, it is very easy to create an asynchronous function in Kotlin. Here is an example:

fun CoroutineScope.getResult() = async { 
   delay(100) 
   "OK" 
}

However, this asynchronicity may be an unexpected behavior for the user of the function, as they may expect a simple value.

What do you think the following code prints?

println("${getResult()}")

For the user, the preceding code somewhat unexpectedly prints the following instead of "OK":

> Name: DeferredCoroutine{Active}@...

Of course, if you have read Chapter 6, Threads and Coroutines, you will know that what's missing here is the await() function:

println("${getResult().await()}")

But it would have been a lot more obvious if we'd named our function accordingly, by adding an async suffix:

fun CoroutineScope.getResultAsync() = async { 
   delay...

Validating input

Input validation is a necessary but very tedious task. How many times did you have to write code like the following?

fun setCapacity(cap: Int) { 
    if (cap < 0) { 
        throw IllegalArgumentException() 
    } 
    ... 
}

Instead, you can check arguments with the require() function:

fun setCapacity(cap: Int) { 
    require(cap > 0) 
}

This makes the code a lot more fluent. You can use require() to check for nulls:

fun printNameLength(p: Profile) { 
    require(p.firstName != null) 
}

But there's also requireNotNull() for that:

fun printNameLength(p: Profile) { 
    requireNotNull(p.firstName) 
}

Use check() to validate the state of your object. This is useful when you are providing an object that the user may not have set up correctly:

class HttpClient { 
  ...

Preferring sealed classes over enums

Coming from Java, you may be tempted to overload your enum with functionality.

For example, let's say you build an application that allows users to order a pizza and track its status. We can use the following code for this:

// Java-like code that uses enum to represent State
enum class PizzaOrderStatus {
    ORDER_RECEIVED, PIZZA_BEING_MADE, OUT_FOR_DELIVERY,      COMPLETED;
 
    fun nextStatus(): PizzaOrderStatus {
        return when (this) {
            ORDER_RECEIVED -> PIZZA_BEING_MADE
            PIZZA_BEING_MADE -> OUT_FOR_DELIVERY
            OUT_FOR_DELIVERY -> COMPLETED
          ...

Summary

In this chapter, we reviewed the best practices in Kotlin, as well as some of the caveats of the language. Now, you should be able to write more idiomatic code that is also performant and maintainable.

You should make use of the scoping functions where necessary, but make sure not to overuse them as they may make the code confusing, especially for those newer to the language.

Be sure to handle nulls and type casts correctly, with let(), the Elvis operator, and the smart casts that the language provides. Finally, generics and sealed classes and interfaces are powerful tools that help describe complex relationships and behaviors between different classes.

In the next chapter, we'll put those skills to use by writing a real-life microservice Reactive design pattern.

Questions

  1. What is the alternative to Java's try-with-resources in Kotlin?
  2. What are the different options for handling nulls in Kotlin?
  3. Which problem can be solved by reified generics?
lock icon
The rest of the chapter is locked
You have been reading a chapter from
Kotlin Design Patterns and Best Practices - Second Edition
Published in: Jan 2022Publisher: PacktISBN-13: 9781801815727
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Alexey Soshin

Alexey Soshin is a software architect with 15 years of experience in the industry. He started exploring Kotlin when Kotlin was still in beta, and since then has been a big enthusiast of the language. He's a conference speaker, published writer, and the author of a video course titled Pragmatic System Design.
Read more about Alexey Soshin