Menu
Coddy logo textTech

상태 패턴

Coddy Swift 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 57개 중 49번째.

상태 패턴을 사용하면 객체의 내부 상태가 변경될 때 객체의 동작을 변경할 수 있습니다. 각 상태는 다음에 어떤 일이 발생하고 어떤 상태가 이어질지 결정하는 고유한 타입입니다:

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

출력:

red
green
yellow
red

상태를 소유한 객체는 요청을 현재 상태로 전달합니다. 각 상태는 이벤트를 고유한 방식으로 처리하며, 허용하지 않는 상태에서는 잘못된 동작을 거부합니다:

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

출력:

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

자체 데이터가 없는 소수의 상태에는 switch가 포함된 열거형이 종종 동일한 개념을 더 간결하게 표현하며, 컴파일러가 모든 상태가 처리되는지 확인합니다:

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

출력:

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

각 상태에 여러 동작이나 고유한 데이터가 있을 때 상태 타입이 유용합니다. 어느 쪽이든 각 상태의 규칙이 객체 전체에 흩어진 if 검사 대신 한곳에 모입니다.

// 분산: 모든 메서드가 상태를 확인한다
func ship() { if status == "paid" { ... } else if status == "new" { ... } }
// 상태 패턴: 각 상태가 스스로 응답한다
state = state.ship() ?? state
challenge icon

챌린지

쉬움

idle, hasCoin(Int), soldOut 케이스를 갖는 열거형 상태 Vending으로 자판기를 모델링하세요. handle(_ event:)는 다음 상태를 반환하거나, 허용되지 않는 이벤트에 대해 nil을 반환합니다. idle에서 coin <n>은 hasCoin(n)으로 전환되고, hasCoin에서는 금액에 더해집니다. hasCoin에서 buy를 입력하고 금액이 150 이상이면 idle로 전환됩니다(판매 성공). hasCoin에서 refund는 idle로 전환됩니다. 어떤 상태에서든 empty는 soldOut으로 전환됩니다. soldOut에서 refill은 idle로 전환됩니다.

제공된 코드는 이벤트를 순서대로 적용하고 각 이벤트 후 상태(idle, coin 100, sold out) 또는 not allowed: <event>를 출력합니다.

코드는 Vending.swift에 작성합니다. main.swift에는 제공된 입력/출력 코드가 들어 있으며 편집할 수 없습니다.

직접 해보기

// 제공된 입력/출력 코드: 그대로 유지하세요
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 icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨

직접 연습해 보세요: Swift 플레이그라운드