Book Image

Kotlin Design Patterns and Best Practices - Second Edition

By : Alexey Soshin
Book Image

Kotlin Design Patterns and Best Practices - Second Edition

By: Alexey Soshin

Overview of this book

This book shows you how easy it can be to implement traditional design patterns in the modern multi-paradigm Kotlin programming language, and takes you through the new patterns and paradigms that have emerged. This second edition is updated to cover the changes introduced from Kotlin 1.2 up to 1.5 and focuses more on the idiomatic usage of coroutines, which have become a stable language feature. You'll begin by learning about the practical aspects of smarter coding in Kotlin, as well as understanding basic Kotlin syntax and the impact of design patterns on your code. The book also provides an in-depth explanation of the classical design patterns, such as Creational, Structural, and Behavioral families, before moving on to functional programming. You'll go through reactive and concurrent patterns, and finally, get to grips with coroutines and structured concurrency to write performant, extensible, and maintainable code. By the end of this Kotlin book, you'll have explored the latest trends in architecture and design patterns for microservices. You’ll also understand the tradeoffs when choosing between different architectures and make informed decisions.
Table of Contents (17 chapters)
1
Section 1: Classical Patterns
6
Section 2: Reactive and Concurrent Patterns
11
Section 3: Practical Application of Design Patterns

Classes and inheritance

Although Kotlin is a multi-paradigm language, it has a strong affinity to the Java programming language, which is based on classes. Keeping Java and JVM interoperability in mind, it's no wonder that Kotlin also has the notion of classes and classical inheritance.

In this section, we'll cover the syntax for declaring classes, interfaces, abstract classes, and data classes.

Classes

A class is a collection of data, called properties, and methods. To declare a class, we use the class keyword, exactly like Java.

Let's imagine we're building a video game. We can define a class to represent the player as follows:

class Player {
}

The instantiation of a class simply looks like this:

val player = Player()

Note that there's no new keyword in Kotlin. The Kotlin compiler knows that we want to create a new instance of that class by the round brackets after the class name.

If the class has no body, as in this simple example, we can omit the curly braces:

class Player // Totally fine

Classes without any functions or properties aren't particularly useful, but we'll explore in Chapter 4, Getting Familiar with Behavioral Patterns, why this syntax exists and how it is consistent with other language features.

Primary constructor

It would be useful for the player to be able to specify their name during creation. In order to do that, let's add a primary constructor to our class:

class Player(name: String) 

Now, this declaration won't work anymore:

val player = Player()

Also, we'll have to provide a name for every new player we instantiate:

val player = Player("Roland")

We'll return to constructors soon enough. But for now, let's discuss properties.

Properties

In Java, we are used to the concept of getters and setters. If we were to write a class representing a player in a game in Kotlin using Java idioms, it may have looked like this:

class Player(name: String) {
    private var name: String = name
 
    fun getName(): String {
        return name
    }
 
    fun setName(name: String) {
        this.name = name;
    }
}

If we want to get a player's name, we invoke the getName() method. If we want to change a player's name, we invoke the setName() method. That's quite simple to follow but very verbose.

It is the first time we see the this keyword in Kotlin, so let's quickly explain what it means. Similar to many other languages, this holds the reference to the current object of that class. In our case, it points to the instance of a Player class.

Why don't we write our classes like that, though?

class Player {
    var name: String = ""
}

Seems like this approach has lots of benefits. It is much less verbose for sure. Reading a person's name is now much shorter – player.name.

Also, changing the name is much more intuitive – player.name = "Alex";.

But by doing so, we lost a lot of control over our object. We cannot make Player immutable, for example. If we want everybody to be able to read the player's name, they'll also be able to change it at any point in time. This is a significant problem if we want to change that code later. With a setter, we can control that, but not with a public field.

Kotlin properties provide a solution for all those problems. Let's look at the following class definition:

class Player(val name: String)

Note that this is almost the same as the example from the Primary constructor section, but now name has a val modifier.

This may look the same as the PublicPerson Java example, with all its problems. But actually, this implementation is similar to ImmutablePerson, with all its benefits.

How is that possible? Behind the scenes, Kotlin will generate a member and a getter with the same name for our convenience. We can set the property value in the constructor and then access it using its name:

val player = Player("Alex")
println(player.name)

Trying to change the name of our Player will result in an error, though:

player.name = "Alexey" // value cannot be reassigned

Since we defined this property as a value, it is read-only. To be able to change a property, we need to define it as mutable. Prefixing a constructor parameter with var will automatically generate both a getter and a setter:

class Player(val name: String, var score: Int)

If we don't want the ability to provide the value at construction time, we can move the property inside the class body:

class Player(val name: String) { 
    var score: Int = 0
}

Note that now we must also provide a default value for that property, since it cannot be simply null.

Custom setters and getters

Although we can set a score now easily, its value may be invalid. Take the following example:

player.score = -10

If we want to have a mutable property with some validations, we need to define an explicit setter for it, using set syntax:

class Player(val name: String) { 
    var score: Int = 0
       set(value) {
             field = if (value >= 0) {
                 value
             } else {
                 0
             }
        }
}

Here, value is the new value of the property and field is its current value. If our new value is negative, we decide to use a default value.

Coming from Java, you may be tempted to write the following code in your setter instead:

set(value) {
    this.score = if (value >= 0) value else 0
}

But, in Kotlin, this will create an infinite recursion. You must remember that Kotlin generates a setter for mutable properties. So, the previous code will be translated to something like this:

// This is a pseudocode, not real Kotlin code!
...
fun setValue(value: Int) {
    setValue(value) // Infinite recursion!
}
...

For that reason, we use the field identifier, which is provided automatically.

In a similar manner, we can declare a custom getter:

 class Player(name: String) {
    val name = name
        get() = field.toUpperCase()
}

First, we save a value received as a constructor argument into a field with the same name. Then, we define a custom getter that will convert all characters in this property to uppercase:

println(player.name)

We'll get this as our output:

> ALEX

Interfaces

You are probably already familiar with the concept of interfaces from other languages. But let's quickly recap.

In typed languages, interfaces provide a way to define behavior that some class will have to implement. The keyword to define an interface is simply interface.

Let's now define an interface for rolling a die:

interface DiceRoller {
    fun rollDice(): Int
}

To implement the interface, a class specifies its name after a colon. There's no implement keyword in Kotlin.

import kotlin.random.*
class Player(...) : DiceRoller 
{
    ...
    fun rollDice() = Random.nextInt(0, 6)
}

This is also the first time we see the import keyword. As the name implies, it allows us to import another package, such as kotlin.random, from the Kotlin standard library.

Interfaces in Kotlin also support default functions. If a function doesn't rely on any state, such as this function that simply rolls a random number between 0 and 5, we can move it into the interface:

interface DiceRoller {
    fun rollDice() = Random.nextInt(0, 6)
}

Abstract classes

Abstract classes, another concept familiar to many, are similar to interfaces in that they cannot be instantiated directly. Another class must extend them first. The difference is that unlike interface, an abstract class can contain state.

Let's create an abstract class that is able to move our player on the board or, for the sake of simplicity, just store the new coordinates:

abstract class Moveable() {
    private var x: Int = 0
    private var y: Int = 0
    fun move(x: Int, y: Int) {
        this.x = x
        this.y = y
    } 
}

Any class that implements Moveable will inherit a move() function as well.

Now, let's discuss in some more detail the private keyword you see here for the first time.

Visibility modifiers

We mentioned the private keyword earlier in this chapter but didn't have a chance to explain it. The private properties or functions are only accessible to the class that declared them – Moveable, in this case.

The default visibility of classes and properties is public, so there is no need to use the public keyword all the time.

In order to extend an abstract class, we simply put its name after a colon. There's also no extends keyword in Kotlin.

class ActivePlayer(name: String) : Moveable(), DiceRoller {
...
}

How would you be able to differentiate between an abstract class and an interface, then?

An abstract class has round brackets after its name to indicate that it has a constructor. In the upcoming chapters, we'll see some uses of that syntax.

Inheritance

Apart from extending abstract classes, we can also extend regular classes as well.

Let's try to extend our Player class using the same syntax we used for an abstract class. We will attempt to create a ConfusedPlayer class, that is, a player that when given (x and y) moves to (y and x) instead.

First, let's just create a class that inherits from Player:

class ConfusedPlayer(name: String ): ActivePlayer(name)

Here, you can see the reason for round brackets even in abstract classes. This allows passing arguments to the parent class constructor. This is similar to using the super keyword in Java.

Surprisingly, this doesn't compile. The reason for this is that all classes in Kotlin are final by default and cannot be inherited from.

To allow other classes to inherit from them, we need to declare them open:

open class ActivePlayer (...) : Moveable(), DiceRoller {
...
}

Let's now try and override the move method now:

class ConfusedPlayer(name : String): Player(name) {
    // move() must be declared open
    override fun move(x: Int, y: Int) {
        this.x = y // must be declared protected
        this.y = x // must be declared protected
    }
}

Overriding allows us to redefine the behavior of a function from a parent class. Whereas in Java, @Override is an optional annotation, in Kotlin override is a mandatory keyword. You cannot hide supertype methods, and code that doesn't use override explicitly won't compile.

There are two other problems that we introduced in that piece of code. First, we cannot override a method that is not declared open as well. Second, we cannot modify the coordinates of our player from a child class since both coordinates are private.

Let's use the protected visibility modifier the makes the properties accessible to child classes and mark the function as open to be able to override it:

abstract class Moveable() {
    protected var x: Int = 0
    protected var y: Int = 0
    open fun move(x: Int, y: Int) {
        this.x = x
        this.y = y
    } 
}

Now, both of the problems are fixed. You also see the protected keyword here for the first time. Similar to Java, this visibility modifier makes a property or a method visible only to the class itself and to its subclasses.

Data classes

Remember that Kotlin is all about productiveness. One of the most common tasks for Java developers is to create yet another Plain Old Java Object (POJO). If you're not familiar with POJO, it is basically an object that only has getters, setters, and implementation of equals or hashCode methods. This task is so common that Kotlin has it built into the language. It's called a data class.

Let's take a look at the following example:

data class User(val username: String, private val 
  password: String)

This will generate us a class with two getters and no setters (note the val part), which will also implement equals, hashCode, and clone functions in the correct way.

The introduction of data classes is one of the most significant improvements in reducing the amount of boilerplate in the Kotlin language. Just like the regular classes, data classes can have their own functions:

data class User(val username: String, private val 
  password: String) {
    fun hidePassword() = "*".repeat(password.length)
}
val user = User("Alexey", "abcd1234")
println(user.hidePassword()) // ********

Compared to regular classes, the main limitation of data classes is that they are always final, meaning that no other class can inherit from them. But it's a small price to pay to have equals and hashCode functions generate automatically.

Kotlin data classes versus Java records

Learning from Kotlin, Java 15 introduced the notion of records. Here is how we can represent the same data as a Java record:

public record User(String username, String password) {}

Both syntaxes are pretty concise. Are there any differences, though?

  • Kotlin data classes a have copy() function that records lack. We'll cover it in Chapter 2, Working with Creational Patterns, while discussing the prototype design pattern.
  • In a record, all properties must be final, or, in Kotlin terms, records support only values and not variables.
  • The data classes can inherit from other classes, while records don't allow that.

To summarize, data classes are superior to records in many ways. But both are great features of the respective languages. And since Kotlin is built with interoperability in mind, you can also easily mark a data class as a record to be accessible from Java:

@JvmRecord
data class User(val username: String, val password: String)