Menu
CoddyTech

Mẫu trạng thái

Thuộc phần Lập trình hướng đối tượng trong hành trình Swift của Coddy. Bài học 49 trên 57.

Mẫu state cho phép một đối tượng thay đổi hành vi khi trạng thái bên trong thay đổi. Mỗi trạng thái là một kiểu riêng, quyết định điều gì xảy ra và trạng thái nào sẽ đến tiếp theo:

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

Đầu ra:

red
green
yellow
red

Đối tượng sở hữu trạng thái chuyển các yêu cầu đến trạng thái hiện tại. Mỗi trạng thái xử lý một sự kiện theo cách riêng, còn những trạng thái không cho phép các hành động trái phép sẽ từ chối chúng:

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

Đầu ra:

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

Với một số lượng nhỏ trạng thái không có dữ liệu riêng, enum cùng với switch thường diễn đạt cùng một ý tưởng gọn hơn, đồng thời trình biên dịch kiểm tra để bảo đảm mọi trạng thái đều được xử lý:

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

Đầu ra:

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

Kiểu trạng thái phát huy tác dụng khi mỗi trạng thái có nhiều hành vi hoặc dữ liệu riêng. Dù thế nào, các quy tắc cho từng trạng thái đều nằm ở một chỗ thay vì rải rác trong các câu lệnh kiểm tra if khắp đối tượng.

// Phân tán: mỗi phương thức (method) đều kiểm tra trạng thái
func ship() { if status == "paid" { ... } else if status == "new" { ... } }
// Mẫu trạng thái: mỗi trạng thái tự xử lý
state = state.ship() ?? state
challenge icon

Thử thách

Dễ

Mô hình hóa máy bán hàng tự động bằng trạng thái enum Vending với các trường hợp idle, hasCoin(Int) và soldOut. handle(_ event:) trả về trạng thái tiếp theo hoặc nil nếu sự kiện không được phép: coin <n> trong idle chuyển sang hasCoin(n), còn trong hasCoin thì cộng thêm vào số tiền; buy trong hasCoin với số tiền ít nhất là 150 chuyển sang idle (giao dịch thành công); refund trong hasCoin chuyển sang idle; empty ở bất kỳ trạng thái nào cũng chuyển sang soldOut; refill trong soldOut chuyển sang idle.

Code được cung cấp áp dụng các sự kiện theo thứ tự và in ra trạng thái sau mỗi sự kiện (idle, coin 100, sold out) hoặc not allowed: <event>.

Viết code trong Vending.swift. main.swift chứa code đầu vào/đầu ra được cung cấp và bạn không thể chỉnh sửa tệp này.

Tự làm thử

// Đoạn code đầu vào/đầu ra đã được cung cấp: giữ nguyên
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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Playground Swift