Menu
Coddy logo textTech

Patron d’état

Fait partie de la section Programmation orientée objet du Journey Swift de Coddy. Leçon 49 sur 57.

Le modèle state permet à un objet de changer de comportement lorsque son état interne change. Chaque état est son propre type, qui décide de ce qui se passe et de l’état suivant :

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

Sortie :

red
green
yellow
red

L’objet qui possède l’état transmet les requêtes à son état actuel. Chaque état gère un événement à sa manière, et les actions illégales sont refusées par les états qui ne les autorisent pas :

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

Sortie :

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

Pour un petit nombre d’états sans données propres, un enum avec un switch exprime souvent la même idée de manière plus concise, le compilateur vérifiant que chaque état est traité :

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

Résultat :

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

Les types d’état sont utiles lorsque chaque état possède plusieurs comportements ou ses propres données. Dans tous les cas, les règles de chaque état sont regroupées au même endroit au lieu d’avoir des vérifications if dispersées dans l’objet.

// dispersé : chaque méthode vérifie l'état
func ship() { if status == "paid" { ... } else if status == "new" { ... } }
// pattern State : chaque état répond pour lui-même
state = state.ship() ?? state
challenge icon

Défi

Facile

Modélisez un distributeur automatique avec une énumération d’état Vending comportant les cas idle, hasCoin(Int) et soldOut. handle(_ event:) renvoie l’état suivant ou nil pour un événement qui n’est pas autorisé : coin <n> dans idle passe à hasCoin(n), et dans hasCoin, ajoute au montant ; buy dans hasCoin avec au moins 150 passe à idle (vente réussie) ; refund dans hasCoin passe à idle ; empty dans n’importe quel état passe à soldOut ; refill dans soldOut passe à idle.

Le code fourni applique les événements dans l’ordre et affiche l’état après chacun (idle, coin 100, sold out) ou not allowed: <event>.

Votre code doit être placé dans Vending.swift. main.swift contient le code d’entrée/sortie fourni et ne peut pas être modifié.

Essayez vous-même

// Code d'entrée/sortie fourni : le laisser tel quel
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 iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation orientée objet

Entraînez-vous par vous-même : Playground Swift