Mẫu State
Thuộc phần Lập trình hướng đối tượng trong hành trình Kotlin của Coddy. Bài học 49 trên 57.
Mẫu state giúp một đối tượng có hành vi khác nhau tùy theo trạng thái của nó. Mỗi trạng thái là một kiểu riêng, và đối tượng chuyển mọi sự kiện đến trạng thái hiện tại. Trạng thái đó trả về trạng thái tiếp theo:
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() }
}Bên trong main:
val lamp = Lamp()
repeat(4) {
lamp.press()
println(lamp.state.name)
}Đầu ra:
dim
bright
off
dimMột trạng thái có thể chứa dữ liệu. Với một giao diện sealed, data object đại diện cho trạng thái không có dữ liệu, một lớp dữ liệu đại diện cho trạng thái có dữ liệu, và một hàm trả về trạng thái tiếp theo cho mỗi sự kiện:
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
}Bên trong main:
var spot: Parking = Free
spot = spot.arrive("AB12", 9)
println(spot)
val (next, fee) = spot.leave(12)
println("$next, fee $fee")Đầu ra:
Taken(plate=AB12, since=9)
Free, fee 6Một số sự kiện không được phép ở một số trạng thái. Khi gặp những sự kiện đó, quá trình chuyển trạng thái có thể trả về null, đối tượng sẽ giữ nguyên trạng thái và báo rằng nó từ chối sự kiện:
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
}Bên trong 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")Đầu ra:
lock refused while Open
open refused while Locked
final: ClosedTại sao không dùng một vài cờ Boolean? Các cờ như isOpen và isLocked cho phép những tổ hợp không thể xảy ra, chẳng hạn như cửa vừa mở vừa khóa, và mọi phương thức đều phải kiểm tra chúng. Một thuộc tính trạng thái chỉ chứa đúng một trạng thái hợp lệ, còn câu lệnh when trên một kiểu đóng giúp trình biên dịch kiểm tra xem mọi trạng thái đã được xử lý chưa:
// Các cờ: 4 tổ hợp, trong đó có một tổ hợp không thể xảy ra
class DoorWithFlags {
var isOpen = false
var isLocked = false // isOpen && isLocked không bao giờ được cùng đúng
}
// Trạng thái: luôn chỉ là một trong Open, Closed, Locked
class DoorWithState {
var state: Door = Closed
private set
}Thử thách
DễMáy bán hàng tự động ở một trong các trạng thái được cung cấp: Idle, HasCoins(cents) hoặc SoldOut. Hãy viết Vending.label(), hàm này trả về idle, coins 100 hoặc sold out, và lớp VendingMachine, trong đó state bắt đầu là Idle và chỉ có thể thay đổi bên trong lớp. handle(event) áp dụng một sự kiện và trả về true, hoặc trả về false và không thay đổi gì khi sự kiện không được phép: coin <n> chuyển từ Idle sang HasCoins(n) và cộng thêm vào số tiền trong HasCoins; buy trong HasCoins khi có ít nhất 150 xu là một giao dịch bán hàng (được tính vào sales) và đưa máy về Idle; refund trong HasCoins đưa máy về Idle; empty chuyển máy sang SoldOut từ bất kỳ trạng thái nào; refill trong SoldOut đưa máy về Idle.
Code được cung cấp áp dụng các sự kiện theo thứ tự và in nhãn trạng thái sau mỗi sự kiện, hoặc not allowed: <event>, rồi cuối cùng in sales: 1.
Đặt code của bạn vào VendingMachine.kt, Label.kt, Vending.kt, Idle.kt, HasCoins.kt và SoldOut.kt. Main.kt chứa code đầu vào/đầu ra được cung cấp và bạn không thể chỉnh sửa tệp này.
Tự làm thử
fun main() {
// Đoạn code đầu vào/đầu ra đã được cung cấp: giữ nguyên
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}")
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Lớp và đối tượng
Làm việc với tệpĐịnh nghĩa lớpHàm khởi tạo chínhKhối init và kiểm tra tính hợp lệHàm khởi tạo phụÔn tập: thẻ thư viện4Kế thừa
Lớp mở và lớp conGhi đè thành viênsuper và hàm khởi tạoLớp trừu tượngÔn tập: Danh bạ nhân viên2Thuộc tính và tính đóng gói
Trình truy cập tùy chỉnhBộ sửa đổi phạm vi truy cậpSetter riêng tưlazy và lateinitÔn tập: tài khoản ngân hàng5Giao diện
Khai báo giao diệnPhương thức mặc địnhNhiều giao diệnGiao diện như kiểu dữ liệuÔn tập: Phương thức thanh toánTự luyện tập: Playground Kotlin