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
dimA 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 6Some 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: ClosedWhy 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
EasyA 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}")
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Classes and Objects
Working With FilesDefining ClassesPrimary Constructorsinit Blocks and ValidationSecondary ConstructorsRecap - Library Card4Inheritance
Open Classes and SubclassesOverriding Memberssuper and ConstructorsAbstract ClassesRecap - Staff Directory2Properties and Encapsulation
Custom AccessorsVisibility ModifiersPrivate Setterslazy and lateinitRecap - Bank Account5Interfaces
Declaring InterfacesDefault MethodsMultiple InterfacesInterfaces as TypesRecap - Payment Methods8Generics
Generic FunctionsGeneric ClassesType ConstraintsVariance With in and outRecap - Priority Queue11Project: Library Management
Books and MembersBorrowing BooksPractice on your own: Kotlin playground