Patrón de estado
Parte de la sección Programación orientada a objetos del Journey de Kotlin de Coddy. Lección 49 de 57.
El patrón de estado proporciona a un objeto un comportamiento diferente para cada estado en el que se encuentra. Cada estado es su propio tipo, y el objeto entrega cada evento a su estado actual, que devuelve el siguiente:
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() }
}Dentro de main:
val lamp = Lamp()
repeat(4) {
lamp.press()
println(lamp.state.name)
}Salida:
dim
bright
off
dimUn estado puede contener datos. Con una interfaz sellada, un data object representa un estado sin datos, una clase de datos representa un estado con datos y una función devuelve el siguiente estado para cada evento:
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
}Dentro de main:
var spot: Parking = Free
spot = spot.arrive("AB12", 9)
println(spot)
val (next, fee) = spot.leave(12)
println("$next, fee $fee")Salida:
Taken(plate=AB12, since=9)
Free, fee 6Algunos eventos no están permitidos en ciertos estados. La transición puede devolver null para ellos, y el objeto permanece donde está e informa del rechazo:
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
}Dentro de 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")Salida:
lock refused while Open
open refused while Locked
final: Closed¿Por qué no usar unos cuantos indicadores Boolean? Los indicadores como isOpen y isLocked permiten combinaciones imposibles, como estar abierta y bloqueada al mismo tiempo, y cada método tiene que comprobarlos. Una propiedad de estado contiene exactamente un estado válido, y un when sobre un tipo sellado hace que el compilador compruebe que se gestionan todos los estados:
// flags: 4 combinaciones, una de ellas imposible
class DoorWithFlags {
var isOpen = false
var isLocked = false // isOpen && isLocked nunca debe ocurrir
}
// state: siempre exactamente uno de Open, Closed, Locked
class DoorWithState {
var state: Door = Closed
private set
}Desafío
FácilUna máquina expendedora se encuentra en uno de los estados proporcionados Idle, HasCoins(cents) o SoldOut. Escribe Vending.label(), que devuelve idle, coins 100 o sold out, y la clase VendingMachine, cuyo state comienza como Idle y solo puede cambiarse desde dentro. handle(event) aplica un evento y devuelve true, o devuelve false y no cambia nada cuando el evento no está permitido: coin <n> pasa de Idle a HasCoins(n) y suma al importe en HasCoins; buy en HasCoins con al menos 150 centavos es una venta (se cuenta en sales) y vuelve a Idle; refund en HasCoins pasa a Idle; empty pasa a SoldOut desde cualquier estado; refill en SoldOut pasa a Idle.
El código proporcionado aplica los eventos en orden e imprime la etiqueta del estado después de cada uno, o not allowed: <event>, y finalmente sales: 1.
Tu código debe estar en VendingMachine.kt, Label.kt, Vending.kt, Idle.kt, HasCoins.kt y SoldOut.kt. Main.kt contiene el código de entrada/salida proporcionado y no se puede editar.
Pruébalo tú mismo
fun main() {
// Código de entrada/salida suministrado: déjalo tal como está
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}")
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación orientada a objetos
1Clases y objetos
Trabajar con archivosDefinir clasesConstructores primariosBloques init y validaciónConstructores secundariosRepaso - Tarjeta de biblioteca4Herencia
Clases abiertas y subclasesSobrescritura de miembrossuper y constructoresClases abstractasRepaso: directorio del personal2Propiedades y encapsulación
Accesores personalizadosModificadores de visibilidadSetters privadoslazy y lateinitRepaso: cuenta bancaria5Interfaces
Declaración de interfacesMétodos predeterminadosInterfaces múltiplesInterfaces como tiposRepaso: métodos de pago8Genéricos
Funciones genéricasClases genéricasRestricciones de tiposVariancia con in y outRepaso - Cola de prioridad11Proyecto: Gestión de biblioteca
Libros y miembrosPréstamo de librosPractica por tu cuenta: Playground de Kotlin