תבנית State
חלק מהיחידה תכנות מונחה עצמים במסלול ה-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 על טיפוס אטום מאפשר למהדר לבדוק שכל המצבים מטופלים:
// דגלים: 4 צירופים, אחד מהם בלתי אפשרי
class DoorWithFlags {
var isOpen = false
var isLocked = false // אסור ש־isOpen && isLocked יתקיים אי פעם
}
// מצב: תמיד בדיוק אחד מבין 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 ואימות נתוניםבנאים משנייםסיכום – כרטיס ספרייהתרגלו בעצמכם: Kotlin אונליין