Menu
Coddy logo textTech

Zustandsmuster

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

Das State-Muster ermöglicht es einem Objekt, sein Verhalten zu ändern, wenn sich sein interner Zustand ändert. Jeder Zustand ist ein eigener Typ, der entscheidet, was passiert und welcher Zustand als Nächstes folgt:

protocol LightState {
    var name: String { get }
    func next() -> any LightState
}
struct Red: LightState { var name: String { "red" }; func next() -> any LightState { Green() } }
struct Green: LightState { var name: String { "green" }; func next() -> any LightState { Yellow() } }
struct Yellow: LightState { var name: String { "yellow" }; func next() -> any LightState { Red() } }

var light: any LightState = Red()
for _ in 1...4 {
    print(light.name)
    light = light.next()
}

Ausgabe:

red
green
yellow
red

Das Objekt, dem der Zustand gehört, leitet Anfragen an seinen aktuellen Zustand weiter. Jeder Zustand verarbeitet ein Ereignis auf seine eigene Weise, und unzulässige Aktionen werden von den Zuständen abgelehnt, die sie nicht erlauben:

protocol OrderState {
    var name: String { get }
    func pay() -> (any OrderState)?
    func ship() -> (any OrderState)?
}
struct New: OrderState { var name: String { "new" }; func pay() -> (any OrderState)? { Paid() }; func ship() -> (any OrderState)? { nil } }
struct Paid: OrderState { var name: String { "paid" }; func pay() -> (any OrderState)? { nil }; func ship() -> (any OrderState)? { Shipped() } }
struct Shipped: OrderState { var name: String { "shipped" }; func pay() -> (any OrderState)? { nil }; func ship() -> (any OrderState)? { nil } }

final class Order {
    private(set) var state: any OrderState = New()
    func handle(_ event: String) -> String {
        guard let next = event == "pay" ? state.pay() : state.ship() else { return "cannot \(event) when \(state.name)" }
        state = next
        return "now \(state.name)"
    }
}

let order = Order()
for e in ["ship", "pay", "pay", "ship"] { print(order.handle(e)) }

Ausgabe:

cannot ship when new
now paid
cannot pay when paid
now shipped

Für eine kleine Anzahl von Zuständen ohne eigene Daten drückt ein Enum mit einem switch dieselbe Idee oft kompakter aus, wobei der Compiler überprüft, ob jeder Zustand behandelt wird:

enum Door {
    case open, closed, locked

    func handle(_ action: String) -> Door? {
        switch (self, action) {
        case (.closed, "open"): return .open
        case (.open, "close"): return .closed
        case (.closed, "lock"): return .locked
        case (.locked, "unlock"): return .closed
        default: return nil
        }
    }
}

var door = Door.closed
for a in ["lock", "open", "unlock", "open"] {
    if let next = door.handle(a) { door = next; print("\(a) -> \(door)") } else { print("cannot \(a)") }
}

Ausgabe:

lock -> locked
cannot open
unlock -> closed
open -> open

Zustandstypen machen sich bezahlt, wenn jeder Zustand mehrere Verhaltensweisen oder eigene Daten hat. In jedem Fall befinden sich die Regeln für jeden Zustand an einer Stelle, anstatt dass sich if-Prüfungen über das Objekt verteilen.

// verstreut: jede Methode prüft den Zustand
func ship() { if status == "paid" { ... } else if status == "new" { ... } }
// Zustandsmuster: jeder Zustand antwortet für sich selbst
state = state.ship() ?? state
challenge icon

Aufgabe

Einfach

Modelliere einen Verkaufsautomaten mit einem Enum-Zustand Vending und den Fällen idle, hasCoin(Int) und soldOut. handle(_ event:) gibt den nächsten Zustand oder nil für ein nicht erlaubtes Ereignis zurück: coin <n> in idle führt zu hasCoin(n), und in hasCoin wird der Betrag erhöht; buy in hasCoin führt bei mindestens 150 zu idle (ein erfolgreicher Verkauf); refund in hasCoin führt zu idle; empty führt in jedem Zustand zu soldOut; refill in soldOut führt zu idle.

Der bereitgestellte Code wendet die Ereignisse der Reihe nach an und gibt nach jedem Ereignis den Zustand (idle, coin 100, sold out) oder not allowed: <event> aus.

Dein Code gehört in Vending.swift. main.swift enthält den bereitgestellten Eingabe-/Ausgabecode und darf nicht bearbeitet werden.

Probier es selbst

// Gelieferter Ein-/Ausgabecode: belasse ihn wie er ist
var input: [String] = []
while let line = readLine() { input.append(line) }
var machine = Vending.idle
for event in input {
    if let next = machine.handle(event) {
        machine = next
        print(machine.label)
    } else {
        print("not allowed: \(event)")
    }
}
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: Swift-Playground