Menu
Coddy logo textTech

Patron État

Fait partie de la section Programmation orientée objet du Journey Kotlin de Coddy. Leçon 49 sur 57.

Le modèle state donne à un objet un comportement différent pour chaque état dans lequel il se trouve. Chaque état est son propre type, et l’objet transmet chaque événement à son état actuel, qui renvoie ensuite l’état suivant :

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() }
}

À l’intérieur de main :

val lamp = Lamp()
repeat(4) {
    lamp.press()
    println(lamp.state.name)
}

Sortie :

dim
bright
off
dim

Un état peut contenir des données. Avec une interface scellée, un data object représente un état sans données, une classe de données représente un état avec des données, et une fonction renvoie l’état suivant pour chaque événement :

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
}

Dans main :

var spot: Parking = Free
spot = spot.arrive("AB12", 9)
println(spot)
val (next, fee) = spot.leave(12)
println("$next, fee $fee")

Sortie :

Taken(plate=AB12, since=9)
Free, fee 6

Certains événements ne sont pas autorisés dans certains états. La transition peut renvoyer null pour ces événements ; l’objet reste alors là où il se trouve et signale le refus :

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
}

Dans 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")

Sortie :

lock refused while Open
open refused while Locked
final: Closed

Pourquoi pas quelques indicateurs Boolean ? Des indicateurs comme isOpen et isLocked permettent des combinaisons impossibles, comme être ouvert et verrouillé à la fois, et chaque méthode doit les vérifier. Une propriété d’état contient exactement un état valide, et un when portant sur un type scellé permet au compilateur de vérifier que chaque état est géré :

// flags : 4 combinaisons, l'une d'elles impossible
class DoorWithFlags {
    var isOpen = false
    var isLocked = false        // isOpen && isLocked ne doit jamais se produire
}
// state : toujours exactement un parmi Open, Closed, Locked
class DoorWithState {
    var state: Door = Closed
        private set
}
challenge icon

Défi

Facile

Une machine distributrice se trouve dans l’un des états fournis Idle, HasCoins(cents) ou SoldOut. Écrivez Vending.label(), qui renvoie idle, coins 100 ou sold out, ainsi que la classe VendingMachine, dont l’état initial est Idle et qui ne peut être modifié qu’à l’intérieur de la classe. handle(event) applique un événement et renvoie true, ou renvoie false sans rien modifier lorsque l’événement n’est pas autorisé : coin <n> fait passer de Idle à HasCoins(n) et ajoute cette valeur au montant dans HasCoins ; buy dans HasCoins avec au moins 150 cents constitue une vente (comptabilisée dans sales) et ramène à Idle ; refund dans HasCoins ramène à Idle ; empty mène à SoldOut depuis n’importe quel état ; refill dans SoldOut mène à Idle.

Le code fourni applique les événements dans l’ordre et affiche l’étiquette de l’état après chacun d’eux, ou not allowed: <event>, puis finalement sales: 1.

Votre code doit se trouver dans VendingMachine.kt, Label.kt, Vending.kt, Idle.kt, HasCoins.kt et SoldOut.kt. Main.kt contient le code d’entrée/sortie fourni et ne peut pas être modifié.

Essayez vous-même

fun main() {
    // Code d'entrée/sortie fourni : le garder tel quel
    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 iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation orientée objet

Entraînez-vous par vous-même : Playground Kotlin