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
dimUn é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 6Certains é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: ClosedPourquoi 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
}Défi
FacileUne 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}")
}
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
1Classes et objets
Travailler avec des fichiersDéfinir des classesConstructeurs primairesBlocs init et validationConstructeurs secondairesRécapitulatif - Carte de bibliothèque4Héritage
Classes ouvertes et sous-classesRedéfinition des membressuper et constructeursClasses abstraitesRécapitulatif – Annuaire du personnel2Propriétés et encapsulation
Accesseurs personnalisésModificateurs de visibilitéSetters privéslazy et lateinitRécapitulatif - Compte bancaire5Interfaces
Déclarer des interfacesMéthodes par défautInterfaces multiplesInterfaces en tant que typesRécapitulatif - Méthodes de paiement8Génériques
Fonctions génériquesClasses génériquesContraintes de typeVariance avec in et outRécapitulatif - File de priorité11Projet : Gestion de bibliothèque
Livres et membresEmprunter des livresEntraînez-vous par vous-même : Playground Kotlin