Wzorzec stanu
Część sekcji Programowanie obiektowe ścieżki Kotlin w Coddy. Lekcja 49 z 57.
Wzorzec stanu nadaje obiektowi inne zachowanie dla każdego stanu, w którym się znajduje. Każdy stan jest osobnym typem, a obiekt przekazuje każde zdarzenie swojemu bieżącemu stanowi, który zwraca następny:
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() }
}Wewnątrz main:
val lamp = Lamp()
repeat(4) {
lamp.press()
println(lamp.state.name)
}Wynik:
dim
bright
off
dimStan może przechowywać dane. W przypadku zapieczętowanego interfejsu obiekt data object reprezentuje stan bez danych, klasa danych — stan z danymi, a jedna funkcja zwraca następny stan dla każdego zdarzenia:
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
}Wewnątrz main:
var spot: Parking = Free
spot = spot.arrive("AB12", 9)
println(spot)
val (next, fee) = spot.leave(12)
println("$next, fee $fee")Wynik:
Taken(plate=AB12, since=9)
Free, fee 6Niektóre zdarzenia są niedozwolone w niektórych stanach. Przejście może dla nich zwrócić null, a wtedy obiekt pozostaje w miejscu i zgłasza odmowę:
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
}Wewnątrz 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")Wynik:
lock refused while Open
open refused while Locked
final: ClosedDlaczego nie kilka flag typu Boolean? Flagi takie jak isOpen i isLocked dopuszczają niemożliwe kombinacje, na przykład jednoczesne otwarcie i zablokowanie, a każda metoda musi je sprawdzać. Jedna właściwość stanu przechowuje dokładnie jeden prawidłowy stan, a instrukcja when dotycząca typu zapieczętowanego pozwala kompilatorowi sprawdzić, czy obsłużono każdy stan:
// flagi: 4 kombinacje, jedna z nich jest niemożliwa
class DoorWithFlags {
var isOpen = false
var isLocked = false // isOpen && isLocked nigdy nie może wystąpić
}
// stan: zawsze dokładnie jeden z Open, Closed, Locked
class DoorWithState {
var state: Door = Closed
private set
}Wyzwanie
ŁatwyAutomat sprzedający znajduje się w jednym z podanych stanów: Idle, HasCoins(cents) lub SoldOut. Napisz Vending.label(), która zwraca idle, coins 100 lub sold out, oraz klasę VendingMachine, której state początkowo ma wartość Idle i może być zmieniany tylko wewnątrz klasy. handle(event) obsługuje zdarzenie i zwraca true albo zwraca false i niczego nie zmienia, gdy zdarzenie jest niedozwolone: coin <n> powoduje przejście ze stanu Idle do HasCoins(n) i dodaje kwotę do wartości w HasCoins; buy w stanie HasCoins przy co najmniej 150 centach oznacza sprzedaż (jest ona zliczana w sales) i powoduje powrót do Idle; refund w stanie HasCoins powoduje przejście do Idle; empty powoduje przejście do SoldOut z dowolnego stanu; refill w stanie SoldOut powoduje przejście do Idle.
Podany kod obsługuje zdarzenia po kolei i po każdym wypisuje etykietę stanu albo not allowed: <event>, a na końcu sales: 1.
Twój kod umieść w plikach VendingMachine.kt, Label.kt, Vending.kt, Idle.kt, HasCoins.kt i SoldOut.kt. Main.kt zawiera podany kod wejścia/wyjścia i nie można go edytować.
Spróbuj swoich sił
fun main() {
// Dostarczony kod wejścia/wyjścia: pozostaw go bez zmian
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}")
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Klasy i obiekty
Praca z plikamiDefiniowanie klasKonstruktory główneBloki init i walidacjaKonstruktory dodatkowePodsumowanie — karta biblioteczna4Dziedziczenie
Klasy otwarte i podklasyNadpisywanie składowychsuper i konstruktoryKlasy abstrakcyjnePodsumowanie – katalog pracowników7Klasy specjalne
Klasy danychKlasy wyliczenioweHierarchie zapieczętowaneequals, hashCode, toStringPodsumowanie — śledzenie zamówień10Wzorce projektowe, część 2
Delegowanie za pomocą byWzorzec dekoratoraWzorzec budowniczegoWzorzec stanu2Właściwości i hermetyzacja
Własne akcesoryModyfikatory widocznościPrywatne setterylazy i lateinitPowtórka – konto bankowe5Interfejsy
Deklarowanie interfejsówMetody domyślneWiele interfejsówInterfejsy jako typyPowtórzenie – metody płatności8Typy generyczne
Funkcje generyczneKlasy generyczneOgraniczenia typówKowariancja i kontrawariancja: in i outPodsumowanie — kolejka priorytetowa11Projekt: Zarządzanie biblioteką
Książki i czytelnicyWypożyczanie książekPoćwicz samodzielnie: Edytor online Kotlin