상태 패턴
Coddy Kotlin 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 57개 중 49번째.
상태 패턴은 객체가 현재 속한 각 상태에 따라 서로 다른 동작을 하도록 합니다. 각 상태는 고유한 타입이며, 객체는 모든 이벤트를 현재 상태에 전달하고 현재 상태는 다음 상태를 반환합니다:
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상태는 데이터를 포함할 수 있습니다. sealed interface를 사용하면 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와 같은 플래그는 문이 열려 있으면서 동시에 잠겨 있는 것처럼 불가능한 조합을 허용하고, 모든 메서드가 이를 확인해야 합니다. 하나의 상태 속성은 정확히 하나의 유효한 상태를 저장하며, sealed 타입에 대한 when을 사용하면 컴파일러가 모든 상태가 처리되는지 확인합니다:
// flags: 4가지 조합, 그중 하나는 불가능
class DoorWithFlags {
var isOpen = false
var isLocked = false // isOpen && isLocked는 절대 발생해서는 안 됨
}
// state: 항상 Open, Closed, Locked 중 정확히 하나
class DoorWithState {
var state: Door = Closed
private set
}챌린지
쉬움자판기는 주어진 상태 Idle, HasCoins(cents) 또는 SoldOut 중 하나입니다. idle, coins 100 또는 sold out을 반환하는 Vending.label()과, state가 Idle로 시작하며 내부에서만 변경할 수 있는 VendingMachine 클래스를 작성하세요. handle(event)는 이벤트를 적용하고 true를 반환하며, 이벤트가 허용되지 않을 때는 아무것도 변경하지 않고 false를 반환합니다. coin <n>은 Idle에서 HasCoins(n)으로 전환하고 HasCoins의 금액에 더합니다. HasCoins에서 150센트 이상일 때의 buy는 판매로 처리되어(sales에 집계됨) Idle로 돌아갑니다. HasCoins에서 refund는 Idle로 전환합니다. empty는 어떤 상태에서든 SoldOut으로 전환합니다. SoldOut에서 refill은 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}")
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
직접 연습해 보세요: Kotlin 플레이그라운드