نمط الحالة
جزء من قسم البرمجة كائنية التوجه في رحلة 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 والتحقق من الصحةالمنشئات الثانويةمراجعة - بطاقة المكتبة4الوراثة
الأصناف المفتوحة والأصناف الفرعيةتجاوز الأعضاءsuper والمنشئاتالأصناف المجرّدةمراجعة - دليل الموظفينتدرّب بنفسك: Playground لـ Kotlin