Паттерн состояния
Часть раздела Объектно-ориентированное программирование путешествия по Swift на Coddy. Урок 49 из 57.
Паттерн state позволяет объекту изменять своё поведение при изменении его внутреннего состояния. Каждое состояние является самостоятельным типом, который определяет, что произойдёт и какое состояние наступит следующим:
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Задание
ЛегкоСмоделируйте торговый автомат с помощью перечисления состояний Vending со случаями idle, hasCoin(Int) и soldOut. handle(_ event:) возвращает следующее состояние или nil для недопустимого события: coin <n> в состоянии idle переводит автомат в hasCoin(n), а в состоянии hasCoin добавляет указанную сумму; buy в состоянии hasCoin при наличии как минимум 150 переводит автомат в idle (успешная покупка); refund в состоянии hasCoin переводит автомат в idle; empty в любом состоянии переводит автомат в soldOut; refill в состоянии soldOut переводит автомат в 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)")
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Классы и объекты
Работа с файламиОпределение классовМетоды и selfИнициализаторыКлассы — это ссылкиПовторение — читательский билет4Инкапсуляция
Контроль доступаТолько чтение извнеЗащита состоянияИнициализаторы с возможностью сбояПовторение — банковский счёт7Полиморфизм и обобщения
ПолиморфизмПриведение типовОбобщённые функцииОбобщённые типыПовторение — калькулятор фигур10Паттерны проектирования. Часть 2
Паттерн делегированияПаттерн декораторПаттерн строительПаттерн состояния2Структуры и типы-значения
СтруктурыСемантика значенийИзменяющие методыСтруктура или классПовторение — элемент инвентаря5Наследование
ПодклассыПереопределение методовИнициализаторы и superfinal и иерархииПовторение — иерархия сотрудников8Стандартные протоколы
CustomStringConvertibleEquatableComparableHashableПовторение — пользовательская сортировка11Проект: Управление библиотекой
Книги и читателиВыдача книгПотренируйтесь самостоятельно: Песочница Swift