Menu
Coddy logo textTech

State Pattern

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 49 of 57.

The state pattern gives an object a different behaviour for each state it is in. Each state is its own type, and the object hands every event to its current state, which returns the next one:

interface LampState {
    val name: String
    fun press(): LampState
}
object Off : LampState {
    override val name = "off"
    override fun press() = Dim
}
object Dim : LampState {
    override val name = "dim"
    override fun press() = Bright
}
object Bright : LampState {
    override val name = "bright"
    override fun press() = Off
}

class Lamp {
    var state: LampState = Off
        private set
    fun press() { state = state.press() }
}

Inside main:

val lamp = Lamp()
repeat(4) {
    lamp.press()
    println(lamp.state.name)
}

Output:

dim
bright
off
dim

A state can carry data. With a sealed interface, a data object stands for a state without data, a data class for a state with data, and one function returns the next state for every event:

sealed interface Parking
data object Free : Parking
data class Taken(val plate: String, val since: Int) : Parking

fun Parking.arrive(plate: String, time: Int): Parking = when (this) {
    Free -> Taken(plate, time)
    is Taken -> this
}

fun Parking.leave(time: Int): Pair<Parking, Int> = when (this) {
    Free -> this to 0
    is Taken -> Free to (time - since) * 2
}

Inside main:

var spot: Parking = Free
spot = spot.arrive("AB12", 9)
println(spot)
val (next, fee) = spot.leave(12)
println("$next, fee $fee")

Output:

Taken(plate=AB12, since=9)
Free, fee 6

Some events are not allowed in some states. The transition can return null for them, and the object then stays where it is and reports the refusal:

sealed interface Door
data object Open : Door
data object Closed : Door
data object Locked : Door

fun next(door: Door, event: String): Door? = when (door) {
    Open -> if (event == "close") Closed else null
    Closed -> when (event) { "open" -> Open; "lock" -> Locked; else -> null }
    Locked -> if (event == "unlock") Closed else null
}

Inside main:

var door: Door = Open
for (e in listOf("lock", "close", "lock", "open", "unlock")) {
    val n = next(door, e)
    if (n == null) println("$e refused while $door") else door = n
}
println("final: $door")

Output:

lock refused while Open
open refused while Locked
final: Closed

Why not a few Boolean flags? Flags like isOpen and isLocked allow impossible combinations, such as open and locked at once, and every method has to check them. One state property holds exactly one valid state, and a when over a sealed type makes the compiler check that every state is handled:

// flags: 4 combinations, one of them impossible
class DoorWithFlags {
    var isOpen = false
    var isLocked = false        // isOpen && isLocked must never happen
}
// state: always exactly one of Open, Closed, Locked
class DoorWithState {
    var state: Door = Closed
        private set
}
challenge icon

Challenge

Easy

A vending machine is in one of the supplied states Idle, HasCoins(cents) or SoldOut. Write Vending.label(), which returns idle, coins 100 or sold out, and the class VendingMachine, whose state starts as Idle and can only be changed inside. handle(event) applies an event and returns true, or returns false and changes nothing when the event is not allowed: coin <n> goes from Idle to HasCoins(n) and adds to the amount in HasCoins; buy in HasCoins with at least 150 cents is a sale (it counts in sales) and goes back to Idle; refund in HasCoins goes to Idle; empty goes to SoldOut from any state; refill in SoldOut goes to Idle.

The supplied code applies the events in order and prints the state's label after each, or not allowed: <event>, and finally sales: 1.

Your code goes in VendingMachine.kt, Label.kt, Vending.kt, Idle.kt, HasCoins.kt and SoldOut.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    val machine = VendingMachine()
    for (event in input) {
        if (machine.handle(event)) println(machine.state.label()) else println("not allowed: $event")
    }
    println("sales: ${machine.sales}")
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground