Menu
Coddy logo textTech

Padrão de Estado

Parte da seção Programação Orientada a Objetos do Journey de Swift da Coddy. Lição 49 de 57.

O padrão state permite que um objeto mude seu comportamento quando seu estado interno muda. Cada estado é seu próprio tipo, que decide o que acontece e qual estado vem a seguir:

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

Saída:

red
green
yellow
red

O objeto que possui o estado encaminha as solicitações para seu estado atual. Cada estado trata um evento à sua própria maneira, e ações ilegais são recusadas pelos estados que não as permitem:

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

Saída:

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

Para um pequeno número de estados sem dados próprios, um enum com um switch geralmente expressa a mesma ideia de forma mais compacta, com o compilador verificando se todos os estados foram tratados:

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

Saída:

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

Os tipos de estado compensam quando cada estado tem vários comportamentos ou seus próprios dados. De qualquer forma, as regras de cada estado ficam em um único lugar, em vez de verificações com if espalhadas pelo objeto.

// espalhado: cada método verifica o estado
func ship() { if status == "paid" { ... } else if status == "new" { ... } }
// padrão state: cada estado responde por si mesmo
state = state.ship() ?? state
challenge icon

Desafio

Fácil

Modele uma máquina de venda automática com um estado enum Vending com os casos idle, hasCoin(Int) e soldOut. handle(_ event:) retorna o próximo estado ou nil para um evento que não é permitido: coin <n> em idle passa para hasCoin(n), e em hasCoin adiciona ao valor; buy em hasCoin com pelo menos 150 passa para idle (uma venda bem-sucedida); refund em hasCoin passa para idle; empty em qualquer estado passa para soldOut; refill em soldOut passa para idle.

O código fornecido aplica os eventos na ordem e imprime o estado após cada um (idle, coin 100, sold out) ou not allowed: <event>.

Seu código deve ficar em Vending.swift. main.swift contém o código de entrada/saída fornecido e não pode ser editado.

Experimente você mesmo

// Código de entrada/saída fornecido: mantenha-o 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 iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Programação Orientada a Objetos

Pratique por conta própria: Playground de Swift