Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط الحالة

جزء من قسم البرمجة كائنية التوجه في رحلة 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اختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس البرمجة كائنية التوجه

تدرّب بنفسك: Playground لـ Kotlin