Menu
Coddy logo textTech

תבנית 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
}
challenge icon

אתגר

קל

מכונת מכירה נמצאת באחד מהמצבים שסופקו: 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}")
}
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: Kotlin אונליין