Reader small image

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

Product typeBook
Published inApr 2024
PublisherPackt
ISBN-139781805127765
Edition3rd Edition
Right arrow
Author (1)
Alexey Soshin
Alexey Soshin
author image
Alexey Soshin

Alexey Soshin is a software architect with 18 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

Idioms and Anti-Patterns

In the preceding chapters, we explored various aspects of the Kotlin language, delving into the advantages of functional programming and examining concurrent design patterns.

This chapter focuses on the dos and don’ts in Kotlin programming. It aims to help you recognize idiomatic Kotlin code and understand patterns that are best avoided and serves as a compilation of best practices, covering a range of topics discussed earlier.

You might consider some of the content in this chapter simpler compared to the discussions on concurrent data structures and design patterns from the previous two chapters. However, I believe it’s beneficial to address idioms as a unified topic, and to do so, we needed to complete the entire discussion on coroutines first.

The topics covered in this chapter include:

  • Scope functions (and how to utilize them effectively)
  • Type checks and casts
  • An alternative to the try-with-resources statement...

Technical requirements

Scope functions

Kotlin’s scoping functions, which are accessible on any object, offer a powerful tool to reduce repetitive code. These functions, functioning as higher-order functions, accept lambda expressions as arguments. In this section, we’ll explore essential scoping functions and demonstrate their application by executing code blocks within the context of specified objects. We’ll use the terms “scope” and “context object” interchangeably to refer to the objects these functions act upon.

let function

The let() function is useful for operating on nullable objects, executing code only if the object is non-null. Consider the map of quotes introduced in Chapter 1:

val clintEastwoodQuotes = mapOf(
    "The Good, The Bad, The Ugly" to "Every gun makes its own tune.",
    "A Fistful Of Dollars" to "My mistake: four coffins."
)

To safely fetch and print a quote that might not exist...

Type checks and casts

In Kotlin, it’s common to check an object’s type using is and cast it using as. However, Kotlin’s smart casts simplify this process. Let’s consider an example with a superhero system:

interface Superhero
class Batman : Superhero {
    fun callRobin() {
        println("To the Bat-pole, Robin!")
    }
}
 
class Superman : Superhero {
    fun fly() {
        println("Up, up and away!")
    }
}
 
// Function invoking a superhero's power
fun doCoolStuff(s: Superhero) {
    if (s is Superman) {
        s.fly()
    } else if (s is Batman) {
        s.callRobin()
    }
}

Kotlin’s smart casting eliminates the need for explicit casting. The above function can be further optimized using when, producing cleaner code. However, it makes it necessary to handle the else case as long as it is not a sealed interface:

fun doCoolStuff(s: Superhero) {
    when(s) {
        is Superman -> s.fly()
       ...

An alternative to the try-with-resources statement

Java 7 introduced AutoCloseable and the try-with-resources statement for automatic resource management. Pre-Java 7 code required manual resource management, often leading to verbosity and potential errors. Here’s a pre-Java 7 example:

BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("./path/to/file"));
    System.out.println(br.readLine());
} finally {
    if (br != null) {
        br.close();
    }
}

Post-Java 7, this code can be simplified using try-with-resource syntax:

try (BufferedReader br = new BufferedReader(new FileReader("/some/path"))) {
    System.out.println(br.readLine());
}

Kotlin replaces try-with-resources with the use() function:

val br = BufferedReader(FileReader("./path/to/file"))
br.use {
    println(it.readLines())
}

In this example, the BufferedReader is automatically closed at the end of the use{} block, similar to Java...

Inline functions

It’s considered best practice to divide your code into small functions, and rightfully so. However, every function call introduces a level of indirection and slight performance overhead. While insignificant individually, this overhead may stack up if the function is invoked millions of times. In some cases, such as within the Spring Framework, functions may be hundreds of lines long due to performance requirements, leading to a departure from the single responsibility principle.

Inline functions can enhance performance by reducing the overhead of function calls. When you call a regular function, the Kotlin compiler generates a function invocation, which entails pushing arguments onto the stack, jumping to the function code, executing it, and then returning. Inline functions replace the function call with the actual function body at compile time, eliminating this overhead.

When passing lambda expressions as arguments to higher-order functions in Kotlin...

Algebraic data types

Algebraic Data Types (ADTs), a concept from functional programming, share similarities with the Composite design pattern, as we explored in Chapter 3, Understanding Structural Patterns.

ADTs offer a solution for modeling intricate data structures and behaviors in a succinct and expressive manner. They establish a structured and type-safe representation of data, enhancing the comprehension of programs and ensuring correctness. By amalgamating simple data types and operations, ADTs empower the creation of elaborate data structures that faithfully portray the modeled domain. They contribute to type safety by enabling developers to precisely define the nature of data each type represents, thereby reducing the likelihood of runtime errors stemming from type mismatches and providing compile-time assurances regarding the correctness of data operations. ADTs also streamline pattern matching, a potent mechanism for dissecting and manipulating data based on its inherent...

Recursive functions

In Chapter 5, Introducing Functional Programming, we explored the concept of recursive functions within the context of functional programming. A recursive function is one that calls itself, either directly or indirectly, to solve a problem by breaking it down into smaller instances of the same problem.

Certain types of problems lend themselves to elegant solutions using recursive functions. However, it’s crucial to note that every function call consumes stack space, and if the function is deeply nested, it may lead to a StackOverflowError. To address this concern, Kotlin offers an optimization called tailrec, which instructs the compiler to use constant stack space instead of growing the stack with each recursive call.

It’s important to acknowledge the limitation of tailrec: for a function to benefit from tail recursion optimization, it must call itself as the last operation in its body. This ensures that the result of the recursive call is...

Reified generics

Reified generics in Kotlin address a limitation of the JVM called type erasure. Type erasure means that generic type information is lost at runtime. This restriction prevents you from performing certain operations, like type checking, with generic types.

For instance, the following code will not compile:

fun <T> printIfSameType(a: Number) {
    if (a is T) { // Error: Cannot check for instance of erased type: T
        println(a)
    }
}

A common workaround is to pass the class as an argument:

fun <T : Number> printIfSameType(clazz: KClass<T>, a: Number) {
    if (clazz.isInstance(a)) {
        println("Yes")
    } else {
        println("No")
    }
}

You can see this approach in the Intent class of the Android codebase, for example.

This approach, requiring the explicit passing of the class type and the use of isInstance() instead of the is operator, can be streamlined using reified generics:

inline...

Using constants efficiently

In Java, constants are typically static members of a class. Kotlin introduces companion objects for a similar purpose, but there are more efficient ways of handling constants in Kotlin.

Initially, one might define constants in Kotlin using a companion object:

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

However, this approach has some inefficiencies. The Kotlin compiler generates a getter for the constant, introducing an additional layer of indirection. The code using the constant effectively calls a getter method, which is less efficient.

Using JVM pseudocode, the results may look as follows:

String var0 = Spock.Companion.getSENSE_OF_HUMOR();
System.out.println(var0);

To optimize, you can use the const keyword:

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

This modification leads to more efficient bytecode:

public class...

Constructor overload

Coming from another language, you may be tempted to declare multiple constructors for your classes. For instance, let’s look at a definition of a User class in Java:

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

Here, the User class has two constructors, one of which sets a default value for resetPassword.

Kotlin simplifies this pattern significantly using the default and secondary constructors or leveraging default parameter values:

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

The secondary constructor here calls the primary constructor, setting a default value for resetPassword.

However, it’s usually better to have default...

Dealing with nulls

Handling nulls is a critical aspect of Kotlin programming, especially when interfacing with Java libraries or databases. Kotlin provides several tools to deal with nullability in a safe and expressive way.

Kotlin supports traditional null checks similar to Java:

val stringOrNull: String? = if (Random.nextBoolean()) "String" else null
 
if (stringOrNull != null) {
    println(stringOrNull.length)
}

This approach is straightforward but can lead to verbose code, especially with nested null checks.

The Elvis operator offers a concise way to handle null values by providing a default value:

val alwaysLength = stringOrNull?.length ?: 0

If stringOrNull is not null, alwaysLength will be its length; otherwise, it defaults to 0.

For nested objects with nullable properties, chaining null-safe calls prevents receiving a NullPointerException:

data class Response(val profile: UserProfile?)
data class UserProfile(val firstName: String...

Making asynchronicity explicit

In Kotlin, managing asynchronous operations is streamlined with coroutines. However, the asynchronous nature of a function is not always apparent from its name or signature, which can lead to misunderstandings about its behavior.

Consider a simple asynchronous function defined within a CoroutineScope:

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

Since this function implicitly returns a Deferred object, not the direct result, the following code might not behave as expected:

println("Result: ${getResult()}")

This prints a reference to the Deferred object instead of OK:

> Result: DeferredCoroutine{Active}@...

If you’ve been following along with the book, you will already know that to obtain the actual result, the await() function must be used:

println("Result: ${getResultAsync().await()}")

This code correctly waits for Deferred to complete and then prints...

Validating input

Input validation is a crucial aspect of programming, ensuring that functions receive appropriate and expected inputs. Kotlin offers several built-in functions to streamline this process, making the code more concise and expressive.

The require() function is used for argument validation. It throws an IllegalArgumentException if its condition is not met. This simplifies the traditional way of argument checking:

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

This is more fluent compared to manually throwing an exception:

fun setCapacity(cap: Int) {
    if (cap < 0) {
        throw IllegalArgumentException("Capacity must be positive")
    }
    ...
}

The requireNotNull() function is specifically designed for null checks:

fun printNameLength(p: UserProfile) {
    requireNotNull(p.firstName) { "First name must not be null " }
    requireNotNull(p.lastName)
    println(p.firstName.length + 1 + p.lastName.length)
}
...

Sealed hierarchies versus enums

In Kotlin, while enums are suitable for a fixed set of constants, sealed interfaces and classes provide a more flexible and powerful alternative, especially when dealing with a state machine-like scenario or when additional data needs to be associated with each state.

In Java, enums are often used to represent a fixed set of states and can be overloaded with functionality. Consider a pizza order tracking system:

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

This approach is straightforward but limited in terms of scalability and flexibility.

Sealed interfaces in Kotlin allow for a more expressive representation of...

Context receivers

At the beginning of this book, we explored one of Kotlin’s most characteristic features: extension functions. These functions allow us to add methods to classes without modifying their original definitions, thanks to a clever compiler technique.

Despite their remarkable capabilities, extension functions have limitations, particularly noticeable to library authors. In this section, we’ll illustrate these limitations using an imaginary database framework as an example.

In Chapter 11, Concurrent Microservices with Ktor, we will examine the Exposed framework, which provides a Kotlin-native approach for interacting with relational databases. This framework serves as a real-world inspiration for our exploration.

For now, let’s consider two interfaces essential for our discussion.

First, the Transaction interface, facilitating database transactions:

interface Transaction {
    fun commit()
    fun rollback()
}

Next, an interface...

Summary

In this chapter, we discussed the intricacies and best practices of Kotlin programming, equipping you with the knowledge to write idiomatic, efficient, and maintainable code.

Key among these practices is the effective use of scoping functions. While they are powerful for reducing boilerplate and improving readability, it’s important to use them judiciously to avoid confusion, especially for those less familiar with Kotlin.

We also explored handling nulls safely using Kotlin’s robust null-safety features, such as the Elvis operator and smart casts, ensuring your code is less prone to null-related errors.

In the realm of type checks and casts, Kotlin’s smart casts significantly simplify the code, reducing the need for explicit and often cumbersome type casting.

Inline functions and reified generics were highlighted as solutions to overcome JVM’s type erasure, demonstrating how Kotlin enhances functionality that’s limited in...

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?

Learn more on Discord

Join our community’s Discord space for discussions with the author and other readers:

https://discord.com/invite/xQ7vVN4XSc

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Kotlin Design Patterns and Best Practices - Third Edition
Published in: Apr 2024Publisher: PacktISBN-13: 9781805127765
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 $15.99/month. Cancel anytime

Author (1)

author image
Alexey Soshin

Alexey Soshin is a software architect with 18 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