Паттерн состояние
Часть раздела Объектно-ориентированное программирование путешествия по Kotlin на Coddy. Урок 49 из 57.
Паттерн состояние задаёт объекту различное поведение для каждого состояния, в котором он находится. Каждое состояние является отдельным типом, а объект передаёт каждое событие своему текущему состоянию, которое возвращает следующее:
interface LampState {
val name: String
fun press(): LampState
}
object Off : LampState {
override val name = "off"
override fun press() = Dim
}
object Dim : LampState {
override val name = "dim"
override fun press() = Bright
}
object Bright : LampState {
override val name = "bright"
override fun press() = Off
}
class Lamp {
var state: LampState = Off
private set
fun press() { state = state.press() }
}Внутри main:
val lamp = Lamp()
repeat(4) {
lamp.press()
println(lamp.state.name)
}Вывод:
dim
bright
off
dimСостояние может содержать данные. В запечатанном интерфейсе data object обозначает состояние без данных, класс данных — состояние с данными, а одна функция возвращает следующее состояние для каждого события:
sealed interface Parking
data object Free : Parking
data class Taken(val plate: String, val since: Int) : Parking
fun Parking.arrive(plate: String, time: Int): Parking = when (this) {
Free -> Taken(plate, time)
is Taken -> this
}
fun Parking.leave(time: Int): Pair<Parking, Int> = when (this) {
Free -> this to 0
is Taken -> Free to (time - since) * 2
}Внутри main:
var spot: Parking = Free
spot = spot.arrive("AB12", 9)
println(spot)
val (next, fee) = spot.leave(12)
println("$next, fee $fee")Вывод:
Taken(plate=AB12, since=9)
Free, fee 6Некоторые события недопустимы в определённых состояниях. Для них переход может вернуть null, после чего объект остаётся на месте и сообщает об отказе:
sealed interface Door
data object Open : Door
data object Closed : Door
data object Locked : Door
fun next(door: Door, event: String): Door? = when (door) {
Open -> if (event == "close") Closed else null
Closed -> when (event) { "open" -> Open; "lock" -> Locked; else -> null }
Locked -> if (event == "unlock") Closed else null
}Внутри main:
var door: Door = Open
for (e in listOf("lock", "close", "lock", "open", "unlock")) {
val n = next(door, e)
if (n == null) println("$e refused while $door") else door = n
}
println("final: $door")Вывод:
lock refused while Open
open refused while Locked
final: ClosedПочему бы не использовать несколько флагов типа Boolean? Такие флаги, как isOpen и isLocked, допускают невозможные комбинации, например одновременное открытое и заблокированное состояние, и каждый метод должен их проверять. Одно свойство состояния содержит ровно одно допустимое состояние, а конструкция when для запечатанного типа позволяет компилятору проверить, обработано ли каждое состояние:
// flags: 4 комбинации, одна из них невозможна
class DoorWithFlags {
var isOpen = false
var isLocked = false // isOpen && isLocked никогда не должно происходить
}
// state: всегда ровно одно из Open, Closed, Locked
class DoorWithState {
var state: Door = Closed
private set
}Задание
ЛегкоТорговый автомат находится в одном из заданных состояний: Idle, HasCoins(cents) или SoldOut. Напишите Vending.label(), которая возвращает idle, coins 100 или sold out, а также класс VendingMachine, состояние state которого изначально равно Idle и может изменяться только внутри класса. handle(event) применяет событие и возвращает true либо возвращает false и ничего не изменяет, если событие недопустимо: coin <n> переводит из Idle в HasCoins(n) и добавляет сумму к количеству в HasCoins; buy в состоянии HasCoins при наличии как минимум 150 центов является продажей (она учитывается в sales) и переводит автомат обратно в Idle; refund в состоянии HasCoins переводит автомат в Idle; empty переводит автомат из любого состояния в SoldOut; refill в состоянии SoldOut переводит автомат в Idle.
Предоставленный код применяет события по порядку и после каждого выводит метку состояния либо not allowed: <event>, а в конце — sales: 1.
Ваш код должен находиться в файлах VendingMachine.kt, Label.kt, Vending.kt, Idle.kt, HasCoins.kt и SoldOut.kt. В Main.kt находится предоставленный код ввода/вывода, и его нельзя изменять.
Попробуйте сами
fun main() {
// Предоставленный код ввода/вывода: оставьте его как есть
val input = generateSequence(::readLine).toList()
val machine = VendingMachine()
for (event in input) {
if (machine.handle(event)) println(machine.state.label()) else println("not allowed: $event")
}
println("sales: ${machine.sales}")
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Классы и объекты
Работа с файламиОпределение классовПервичные конструкторыБлоки init и валидацияВторичные конструкторыПовторение — читательский билет4Наследование
Открытые классы и подклассыПереопределение членовsuper и конструкторыАбстрактные классыПовторение — каталог сотрудников7Специальные классы
Классы данныхКлассы перечисленийЗапечатанные иерархииequals, hashCode, toStringПовторение — трекер заказов10Паттерны проектирования. Часть 2
Делегирование с помощью byПаттерн декораторПаттерн строительПаттерн состояние2Свойства и инкапсуляция
Пользовательские аксессорыМодификаторы видимостиПриватные сеттерыlazy и lateinitПовторение — банковский счёт5Интерфейсы
Объявление интерфейсовМетоды по умолчаниюМножественные интерфейсыИнтерфейсы как типыПовторение — способы оплаты8Обобщения
Обобщённые функцииОбобщённые классыОграничения типовВариативность с in и outПовторение — очередь с приоритетом11Проект: Управление библиотекой
Книги и читателиВыдача книг3Объекты и компаньоны
Объявления объектовОбъекты-компаньоныКонстанты с constОбъектные выраженияПовторение — билетная касса6Полиморфизм
Динамическая диспетчеризацияПроверка типов и умные приведения типовwhen с типамиФункции-расширенияПовторение — смотритель зоопарка9Паттерны проектирования. Часть 1
Паттерн «Одиночка»Фабричный паттернПаттерн «Наблюдатель»Паттерн «Стратегия»Потренируйтесь самостоятельно: Песочница Kotlin