Menu
Coddy logo textTech

Patrón de estado

Parte de la sección Programación orientada a objetos del Journey de Swift de Coddy. Lección 49 de 57.

El patrón de estado permite que un objeto cambie su comportamiento cuando cambia su estado interno. Cada estado es su propio tipo, que decide qué sucede y cuál es el siguiente estado:

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

Salida:

red
green
yellow
red

El objeto que posee el estado reenvía las solicitudes a su estado actual. Cada estado gestiona un evento a su manera, y los estados que no permiten determinadas acciones rechazan las acciones ilegales:

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

Salida:

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

Para un número reducido de estados sin datos propios, un enum con un switch suele expresar la misma idea de forma más compacta, y el compilador comprueba que se gestione cada estado:

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

Salida:

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

Los tipos de estado resultan útiles cuando cada estado tiene varios comportamientos o sus propios datos. De cualquier manera, las reglas de cada estado permanecen en un solo lugar en vez de estar en comprobaciones if repartidas por todo el objeto.

// disperso: cada método comprueba el estado
func ship() { if status == "paid" { ... } else if status == "new" { ... } }
// patrón de estado: cada estado responde por sí mismo
state = state.ship() ?? state
challenge icon

Desafío

Fácil

Modela una máquina expendedora con un estado enum Vending con los casos idle, hasCoin(Int) y soldOut. handle(_ event:) devuelve el siguiente estado o nil para un evento no permitido: coin <n> en idle pasa a hasCoin(n), y en hasCoin suma al importe; buy en hasCoin con al menos 150 pasa a idle (una venta correcta); refund en hasCoin pasa a idle; empty en cualquier estado pasa a soldOut; refill en soldOut pasa a idle.

El código proporcionado aplica los eventos en orden e imprime el estado después de cada uno (idle, coin 100, sold out) o not allowed: <event>.

Tu código va en Vending.swift. main.swift contiene el código de entrada/salida proporcionado y no se puede editar.

Pruébalo tú mismo

// Código de entrada/salida suministrado: déjalo tal como está
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 iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación orientada a objetos

Practica por tu cuenta: Playground de Swift