Menu
Coddy logo textTech

Zustandsmuster

Teil des Abschnitts Objektorientierte Programmierung der Kotlin-Journey von Coddy. Lektion 49 von 57.

Das State-Muster gibt einem Objekt für jeden Zustand, in dem es sich befindet, ein anderes Verhalten. Jeder Zustand ist ein eigener Typ, und das Objekt übergibt jedes Ereignis an seinen aktuellen Zustand, der den nächsten zurückgibt:

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() }
}

Innerhalb von main:

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

Ausgabe:

dim
bright
off
dim

Ein Zustand kann Daten enthalten. Mit einer versiegelten Schnittstelle steht ein data object für einen Zustand ohne Daten, eine Datenklasse für einen Zustand mit Daten, und eine Funktion gibt für jedes Ereignis den nächsten Zustand zurück:

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
}

Innerhalb von main:

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

Ausgabe:

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

Manche Ereignisse sind in bestimmten Zuständen nicht erlaubt. Die Transition kann für diese Ereignisse null zurückgeben; das Objekt bleibt dann, wo es ist, und meldet die Ablehnung:

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
}

Innerhalb von 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")

Ausgabe:

lock refused while Open
open refused while Locked
final: Closed

Warum nicht ein paar Boolean-Flags? Flags wie isOpen und isLocked erlauben unmögliche Kombinationen, etwa gleichzeitig offen und verriegelt, und jede Methode muss sie überprüfen. Eine Zustandseigenschaft enthält genau einen gültigen Zustand, und ein when über einen versiegelten Typ sorgt dafür, dass der Compiler überprüft, ob jeder Zustand behandelt wird:

// flags: 4 Kombinationen, eine davon unmöglich
class DoorWithFlags {
    var isOpen = false
    var isLocked = false        // isOpen && isLocked darf nie vorkommen
}
// state: immer genau einer von Open, Closed, Locked
class DoorWithState {
    var state: Door = Closed
        private set
}
challenge icon

Aufgabe

Einfach

Ein Verkaufsautomat befindet sich in einem der vorgegebenen Zustände Idle, HasCoins(cents) oder SoldOut. Schreibe Vending.label(), das idle, coins 100 oder sold out zurückgibt, sowie die Klasse VendingMachine, deren state mit Idle beginnt und nur innerhalb der Klasse geändert werden kann. handle(event) wendet ein Ereignis an und gibt true zurück oder gibt false zurück und ändert nichts, wenn das Ereignis nicht zulässig ist: coin <n> wechselt von Idle zu HasCoins(n) und addiert den Betrag zu dem Betrag in HasCoins; buy in HasCoins mit mindestens 150 Cent ist ein Verkauf (er wird in sales gezählt) und wechselt zurück zu Idle; refund in HasCoins wechselt zu Idle; empty wechselt aus jedem Zustand zu SoldOut; refill in SoldOut wechselt zu Idle.

Der vorgegebene Code wendet die Ereignisse der Reihe nach an und gibt nach jedem Ereignis die Zustandsbezeichnung aus oder not allowed: <event> und schließlich sales: 1.

Dein Code kommt in VendingMachine.kt, Label.kt, Vending.kt, Idle.kt, HasCoins.kt und SoldOut.kt. Main.kt enthält den vorgegebenen Eingabe-/Ausgabecode und kann nicht bearbeitet werden.

Probier es selbst

fun main() {
    // Gelieferter Ein-/Ausgabe-Code: belasse ihn wie er ist
    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 iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Kotlin-Playground