狀態模式是透過將行為封裝在狀態之中,Context 的行為會委派給一個物件,而狀態改變,Context 的行為也會改變,但是使用Context 的客戶並不會知道狀態變化,也不知道行為有變
舉個例子吧,美式軟餅乾!!! 下面是真實食譜XD
typealias pcs = Int
abstract class SoftCookieRecipe {
private val sodaPowder = 3
private val salt = 5
protected val flour = 280 + sodaPowder + salt
protected val butter = 250
protected val sugar = 140
protected val brownSugar = 140
protected val vanillaExtra = 10
protected val chocolateBean = 225
protected val egg: pcs = 2
abstract fun makeCookie()
fun mix(vararg t: Int): Int {
return t.sum()
}
fun bake(chocolateDough:Int){
// temperature 170
}
}
class StandardRecipe : SoftCookieRecipe() {
override fun makeCookie() {
val solidButterWithSugar = mix(butter, sugar)
val eggCream = mix(egg,solidButterWithSugar, vanillaExtra)
val dough = mix(eggCream,flour)
val chocolateDough = mix(dough,chocolateBean)
}
}
class CustomRecipe : SoftCookieRecipe() {
private val iceCube = butter * 13 / 100
override fun makeCookie() {
val brownButter = brownTheButter(butter)
val eggCream = whippedEgg(sugar)
val brownButterWithSugar = mix(brownButter, brownSugar)
val batter = mix(brownButterWithSugar, eggCream, flour)
val chocolateBatter = mix(batter, chocolateBean)
val chocolateDough = chocolateBatter.moveToRefrigerator()
}
private fun whippedEgg(item: Int): Int {
return item
}
private fun brownTheButter(item: Int): Int {
//start remove 13% water
//add equal weight to 13% water ice cube to cool down
return item + iceCube
}
}
private fun Int.moveToRefrigerator():Int {
Thread.sleep(180000)
return this
}
好的,上面有兩種食譜,一種是傳統食譜,另一種是特製軟餅乾食譜,但是兩者之間很明顯製作方式不同,特製軟食譜醒的時間長,麵粉部分會從複雜澱粉變成單醣類,而且糖分別加在蛋液和奶油液體裡面,而麵團再加熱後第一時間會是往外攤,液體糖漿相比固體糖粒的面積大,也就讓特製軟餅乾會有更多焦化外殼,以及更鬆軟的內餡
那呼叫方呢?烘焙師可以會有製作餅乾的方法,一天做經典口味,一天做客制口味,每天晚上要備隔天的料
class WorkContext(var state: State) {
fun switch() {
state.jobLog(this)
}
}
abstract class State {
abstract fun jobLog(ctx: WorkContext)
}
class ClassicWorkLog : State() {
override fun jobLog(ctx: WorkContext) {
StandardRecipe().makeCookie()
ctx.state = CusotmWorkLog()//每天的最後更改狀態
}
}
class CusotmWorkLog:State() {
override fun jobLog(ctx: WorkContext) {
CustomRecipe().makeCookie()
ctx.state = ClassicWorkLog()//每天的最後更改狀態
}
}
之後烘焙師就殷勤的每天上工
fun main(){
val workStateContext = WorkContext(ClassicWorkLog())
while(true) {
workStateContext.jobLog()//一天工作結束後就會切換狀態拉
}
}
狀態模式優點在於將狀態的變化封裝起來,利用不同狀態分割行為,使得新增新的行為變得容易,而外部調用者甚至不需關注狀態如何變化,也極大程度的簡化了 if/ else 或是 switch 判斷
狀態模式和策略模式其實很像,他的差異在狀態模式封裝了狀態的變化,而策略模式則是由外部調用指定,那將上面的程式碼轉換成策略模式就會變成
fun main(){
var isClassicDay = true
while(true){
if(isClassicDay) StandardRecipe().makeCookie()
else CustomRecipe().makeCookie()
isClassicDay = !isClassicDay
}
}
狀態機是一種狀態模式的進階實現,可能會隨著狀態的增加而有不同的實現,現實中最簡單的有限狀態機就是門
門會有開著跟關著兩種狀態,你沒辦法關一個已經關上的門
通常在實現狀態機時,也會又個狀態變化圖供工程師參考,再舉個交友軟體的範例
用戶在左滑右滑會分為 like, dislike,而付費用戶可以反悔他的 dislike ,把拒絕過的候選人拉回候選池,而如果雙方都 like 對方,就可以配對成功
fun main() {
val match = MatchEntity()
//tryFail(match)
tryMatch(match)
}
fun tryFail(match:MatchEntity){
match.like() //pending
match.unMatch() //unmatch
}
fun tryMatch(match:MatchEntity){
match.dislike() //failed
match.rewindToLiked() //pending
match.match() //matched
}
enum class MatchState {
Created,
Pending,
Failed,
Matched,
}
enum class Command {
Create,
Like,
Dislike,
RewindToCreated,
RewindToLiked,
RewindToDisliked,
Match,
UnMatch,
}
data class StateTransition(
val currentState: MatchState,
val command: Command
)
class MatchEntity:MatchCommand{
private var currentState = MatchState.Created
private val transitionChecker = mapOf(
StateTransition(MatchState.Created, Command.Like) to MatchState.Pending,
StateTransition(MatchState.Created, Command.Dislike) to MatchState.Failed,
StateTransition(MatchState.Pending, Command.Match) to MatchState.Matched,
StateTransition(MatchState.Pending, Command.UnMatch) to MatchState.Failed,
StateTransition(MatchState.Failed, Command.RewindToCreated) to MatchState.Created,
StateTransition(MatchState.Failed, Command.RewindToLiked) to MatchState.Pending,
StateTransition(MatchState.Failed, Command.RewindToDisliked) to MatchState.Failed,
)
private fun changeState(command: Command){
val newTransition = StateTransition(currentState, command)
transitionChecker[newTransition]?.let {
currentState = it
} ?: throw Exception("not support exception")
}
override fun like() {
changeState(Command.Like)
println(currentState.name)
}
override fun dislike() {
changeState(Command.Dislike)
println(currentState.name)
}
override fun match() {
changeState(Command.Match)
println(currentState.name)
}
override fun unMatch() {
changeState(Command.UnMatch)
println(currentState.name)
}
override fun rewindToCreated() {
changeState(Command.RewindToCreated)
println(currentState.name)
}
override fun rewindToLiked() {
changeState(Command.RewindToLiked)
println(currentState.name)
}
override fun rewindToDisliked() {
changeState(Command.RewindToDisliked)
println(currentState.name)
}
}
interface MatchCommand{
fun like()
fun dislike()
fun match()
fun unMatch()
fun rewindToCreated()
fun rewindToLiked()
fun rewindToDisliked()
}
我們透過預先定義所有確定存在狀態路徑,以呼叫指令的方式,由狀態機執行實際邏輯,在調用方只需送出指令即可切換狀態,而透過上面的範例我們可以歸納出一些特徵,狀態機會包含
既然對狀態機已經有初步了解,我們就可以看看 redux 狀態機模型
在 react/ redux 的狀態機模型中,就是以 ui 為狀態顯示和事件觸發的, state 渲染畫面,用戶互動產生 event 丟到 eventHandler 產出對應的 action 交由 reducer 執行狀態變化,並產生新的狀態去繪製 ui
誒,那 react 關 Kotlin 什麼事?
像我之前說過的,不要被架構綁架, Android 不是只有 MVVM 可以實現,架構本身可以獨立討論,也可以套進其他專案,但我在這邊提起的原因,不僅是因為講到狀態機,同時也因為 jetpack compose 便是使用狀態去做響應式畫面渲染,這個架構可以使用 MVVM, MVI, Redux,我們可以看看在開源專案中的案例
在 KMM 之中,ios and android,會有各自的 ui 實現,而他們會共用資料和商業邏輯,當然最重要的事,架構並不會限於某個語言或是框架,而要要選擇適合的去使用
而 coroutine 可以說是把狀態機玩得出神入化,在 coroutine 裡面,有一個關鍵字叫做 suspend ,中文叫掛起,那他的功能就是在不同執行序之間切換任務執行,其背後的原理就是狀態機,詳細的細節可以看Day 9 Kotlin coroutine 黑魔法 suspend
這邊也簡單提一下,掛起函示在編譯後,會變成
when(continuation.label) {
0 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Next time this continuation is called, it should go to state 1
continuation.label = 1
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.user = continuation.result as User
// Next time this continuation is called, it should go to state 2
continuation.label = 2
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userLocalDataSource.logUserIn(continuation.user, continuation)
}
2 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.userDb = continuation.result as UserDb
// Resumes the execution of the function that called this one
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
他會利用 continuation.label
去紀錄執行到哪邊
而最後執行的效果就會是
state pattern change behavior by encapsulate behavior within different state, the behavior of Context will interest to another object, when the state changed, the behavior of context also change, but the client of Context wouldn't know about it
Let's check out a real sample
typealias pcs = Int
abstract class SoftCookieRecipe {
private val sodaPowder = 3
private val salt = 5
protected val flour = 280 + sodaPowder + salt
protected val butter = 250
protected val sugar = 140
protected val brownSugar = 140
protected val vanillaExtra = 10
protected val chocolateBean = 225
protected val egg: pcs = 2
abstract fun makeCookie()
fun mix(vararg t: Int): Int {
return t.sum()
}
fun bake(chocolateDough:Int){
// temperature 170
}
}
class StandardRecipe : SoftCookieRecipe() {
override fun makeCookie() {
val solidButterWithSugar = mix(butter, sugar)
val eggCream = mix(egg,solidButterWithSugar, vanillaExtra)
val dough = mix(eggCream,flour)
val chocolateDough = mix(dough,chocolateBean)
}
}
class CustomRecipe : SoftCookieRecipe() {
private val iceCube = butter * 13 / 100
override fun makeCookie() {
val brownButter = brownTheButter(butter)
val eggCream = whippedEgg(sugar)
val brownButterWithSugar = mix(brownButter, brownSugar)
val batter = mix(brownButterWithSugar, eggCream, flour)
val chocolateBatter = mix(batter, chocolateBean)
val chocolateDough = chocolateBatter.moveToRefrigerator()
}
private fun whippedEgg(item: Int): Int {
return item
}
private fun brownTheButter(item: Int): Int {
//start remove 13% water
//add equal weight to 13% water ice cube to cool down
return item + iceCube
}
}
private fun Int.moveToRefrigerator():Int {
Thread.sleep(180000)
return this
}
Alright, so we have two different kind of recipe, first one is tradition cookie, the other one is special soft cookie, although the ingredient is same, the process is different, the cook hour of special one is longer, allow the strach transfer into Monosaccharides(simple sugar), and the sugar is separate to mix with egg and liquid butter, when the dough met heat, it will become fluid for a while, the liquid syrup dough comes with bigger area of coking Shell and softer stuffing
What about the client?
bakery have the method the make cookie, one day tradition, one day special, every night he need to prepare for tomorrow
class WorkContext(var state: State) {
fun switch() {
state.jobLog(this)
}
}
abstract class State {
abstract fun jobLog(ctx: WorkContext)
}
class ClassicWorkLog : State() {
override fun jobLog(ctx: WorkContext) {
StandardRecipe().makeCookie()
ctx.state = CusotmWorkLog()//change state in the end of day
}
}
class CusotmWorkLog:State() {
override fun jobLog(ctx: WorkContext) {
CustomRecipe().makeCookie()
ctx.state = ClassicWorkLog()//change state in the end of day
}
}
Then the baker works hard everyday
fun main(){
val workStateContext = WorkContext(ClassicWorkLog())
while(true) {
workStateContext.jobLog()//change state
}
}
The advantage of state pattern is it encapsulate the state change, using different state separate behavior, more flexible to add new behavior, and outer client don't need to care about how does the state change, simplemize the if, else, switch
state pattern and strategy is similar, the different is state pattern encapsulate state change, but strategy pattern indicate by client, so the code above transfer into strategy pattern is
fun main(){
var isClassicDay = true
while(true){
if(isClassicDay) StandardRecipe().makeCookie()
else CustomRecipe().makeCookie()
isClassicDay = !isClassicDay
}
}
State machine is a advance implement of state pattern, could come out with different implement with new state added, in the real life, the simple limited state machine is door
there are two state of door, closed and open
Check a sample with dating app
All user slip to right means like, left means dislike, and paid user can regret their dislike, add rejected candidate back to pool, if two person sent a like to each other, then they are matched
fun main() {
val match = MatchEntity()
//tryFail(match)
tryMatch(match)
}
fun tryFail(match:MatchEntity){
match.like() //pending
match.unMatch() //unmatch
}
fun tryMatch(match:MatchEntity){
match.dislike() //failed
match.rewindToLiked() //pending
match.match() //matched
}
enum class MatchState {
Created,
Pending,
Failed,
Matched,
}
enum class Command {
Create,
Like,
Dislike,
RewindToCreated,
RewindToLiked,
RewindToDisliked,
Match,
UnMatch,
}
data class StateTransition(
val currentState: MatchState,
val command: Command
)
class MatchEntity:MatchCommand{
private var currentState = MatchState.Created
private val transitionChecker = mapOf(
StateTransition(MatchState.Created, Command.Like) to MatchState.Pending,
StateTransition(MatchState.Created, Command.Dislike) to MatchState.Failed,
StateTransition(MatchState.Pending, Command.Match) to MatchState.Matched,
StateTransition(MatchState.Pending, Command.UnMatch) to MatchState.Failed,
StateTransition(MatchState.Failed, Command.RewindToCreated) to MatchState.Created,
StateTransition(MatchState.Failed, Command.RewindToLiked) to MatchState.Pending,
StateTransition(MatchState.Failed, Command.RewindToDisliked) to MatchState.Failed,
)
private fun changeState(command: Command){
val newTransition = StateTransition(currentState, command)
transitionChecker[newTransition]?.let {
currentState = it
} ?: throw Exception("not support exception")
}
override fun like() {
changeState(Command.Like)
println(currentState.name)
}
override fun dislike() {
changeState(Command.Dislike)
println(currentState.name)
}
override fun match() {
changeState(Command.Match)
println(currentState.name)
}
override fun unMatch() {
changeState(Command.UnMatch)
println(currentState.name)
}
override fun rewindToCreated() {
changeState(Command.RewindToCreated)
println(currentState.name)
}
override fun rewindToLiked() {
changeState(Command.RewindToLiked)
println(currentState.name)
}
override fun rewindToDisliked() {
changeState(Command.RewindToDisliked)
println(currentState.name)
}
}
interface MatchCommand{
fun like()
fun dislike()
fun match()
fun unMatch()
fun rewindToCreated()
fun rewindToLiked()
fun rewindToDisliked()
}
We define the state could happen, by calling command, use state machine operator logic, in the caller side, we only need to send command to change state, with this sample, we have following conclusion
Now we have basic understand of state machine, we can check out redux state model
in react/ redux state machine, ui is used for display state and trigger event, render ui with state, and throw new event to eventHandler by user interaction, generate action and hand over reducer to execute state changed, using new state to render UI
So, what is the point with Kotlin?
As i said before, do blind your eyes by architecture, software doesn't only runs on one kind of architecture, the architecture could discuss alone, the reason I mentioned here, nit just because of state machine, but also the jetpack compose is render based on state, we can choose MVVM, MVI, Redux, check out an open source sample
In KMM, ios and Android has their own ui Implement, and they shared data and business logic, more important is choose the proper architecture for your use case
Inside Kotlin Coroutine
they have great usage of state machine, there is a keyword call suspend
, its function is switch task between different thread, under the hood it use state machine, the detail implement can check out hereDay 9 Kotlin coroutine 黑魔法 suspend
Quickly spoiler, suspend function will compiled into
when(continuation.label) {
0 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Next time this continuation is called, it should go to state 1
continuation.label = 1
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userRemoteDataSource.logUserIn(userId!!, password!!, continuation)
}
1 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.user = continuation.result as User
// Next time this continuation is called, it should go to state 2
continuation.label = 2
// The continuation object is passed to logUserIn to resume
// this state machine's execution when it finishes
userLocalDataSource.logUserIn(continuation.user, continuation)
}
2 -> {
// Checks for failures
throwOnFailure(continuation.result)
// Gets the result of the previous state
continuation.userDb = continuation.result as UserDb
// Resumes the execution of the function that called this one
continuation.cont.resume(continuation.userDb)
}
else -> throw IllegalStateException(...)
By using continuation.label
to record its state, how it behavior in code will look like
image source
如何有邏輯的釐清事物的狀態
sample source
States and State Machines