# 狀態模式 state pattern create : 2024/11/29 last update : 2024/12/24 ## 上下文物件一被建立就建立所有state ## 簡介 [状态设计模式](https://refactoringguru.cn/design-patterns/state?_gl=1*1e0pql6*_ga*MTM5NjM0OTAzMi4xNzI5MDQ5NDcy*_ga_SR8Y3GYQYC*MTczMjg0NjM2MS44Ny4xLjE3MzI4NDcwNzkuNjAuMC4w) 看看他的智慧型手機的例子 一個程式當前處於某一個狀態,它某個(或某些)行為(或功能)會根據此狀態的不同而有所不同。 以vscode為例,假設vscode分日間跟夜間模式,它有一個狀態是日夜狀態,當你設定他的狀態為日間模式,他的行為(編輯器顯示的背景顏色就不一樣)。 以函數為例,假設一個函數的行為(內部實做)會根據你輸入的某個參數而改變。通常是在函數內用if-else跟switch-case。那此函數的行為就是依據該函數當前的狀態(那個參數)而改變。 class也是一樣,裡面某幾個method的行為可能會根據他的某個/某些屬性而改變。跟函數一樣通常是在方法內用if-else跟switch-case(狀態也有可能是同時考慮多個屬性,但通常是只考慮一個屬性,也可以額外宣告一個屬性去考慮多個其它屬性) ## class UML  from:大話設計模式(比較好理解) 我認為 1. 上下文對狀態物件是組合關係 2. 狀態物件對上下文是關聯關係 ## 原始方法的缺點 假設一個class裡的五六個method的行為都取決於該class的某一個屬性。當你要新增屬性時,就會改得很痛苦。類似簡單工廠模式,但它新增一個子類時,只改CreateVehicle()這一個method跟enum class。但這個例子要改的method的數量可不止一個。 ## 優缺點 優點: 1. 符合單一職責:將與特定狀態相關的code放到單獨的一個類之中 2. 符合開放封閉原則 3. 增加可讀性 缺點: (或許在大部分的需求中並不會這麼複雜) 當情況只要稍微複雜一點點就會產生跟組合模式一樣的缺點:被迫在interface中宣告一些可能有某些子類不該實做的純虛函數,只能實做成{}。比如當你有三個方法M1~M3,都會因為某一個狀態(資料成員的值)的不同而改變。 假設該狀態有四個值s1~s4: M1會依s1~s4改變行為; M2只會依s1~s3改變行為; M3依s2~s4改變行為。 也就是 state1 應該只實做M1、M2; state2 應該實做M1~M3; state3 應該實做M1~M3; state4 應該只實做M1、M3; 這時為了可以用動態多型 = 為了讓class Context物件在任何狀態(這裡是s1~s4)下都可正確的透過State指標呼叫到正確的method,導致interfae State必須有M1~M3這三個純虛函數。那state1就被迫要實做M3、state4就被迫要實做M2,即使他們不該實做它。 ## 實務上的用法 在播放軟體上,播放器物件一旦變建立,就會建立它所有的狀態物件。因為播放器通常會使用到所有的狀態。在播放器物件被銷毀時,同時銷毀這些狀態物件。應該可以用map去存這些狀態 ## 典型情況(必看) by gpt o1: https://chatgpt.com/share/67565616-c728-800f-97dd-5ddfaea4b3eb 一個上下文物件的狀態不該被外部直接改變,比如在main()裡直接去呼叫context.SetState()。而是在其內部改變(比如某些方法會改變當前物件的狀態),或是該物件接收外部的事件並根據特定邏輯而改變狀態(比如當你手機沒電時,會觸發沒電的事件給手機實體按鍵的物件,該物件(有method去)根據此事件去改變他的狀態,進而改變行為)。 音樂播放器的例子: 1. 這裡它額外多了一個鎖定模式(我猜氏為了防止誤觸之類的) 2. 音樂播放軟體會快速的在play、ready跟lock三種模式切換。因此比較好的設計是在該類給三個該狀態的指標成員。一旦該物件被建立,就建立這三個狀態的物件給他用。就不會有快速新建刪除狀態物件的overhead。 3. 通常音樂播放軟體同一時間只會建立一個播放器(你應無法在spotify同時播放兩首歌)。因此不需考慮多個音樂撥放器共用三個狀態物件的情況。 ## 我的實做 需求: 1. 該物件不會頻繁的切換他的狀態,通常只會用到部份的狀態: 一個指標指向當前狀態。切換狀態:刪除舊的建立新的 2. 該物件會有預設狀態(通常都會有,想想音樂播放器的例子。寫在建構子上) 3. 狀態物件只有Handle2()會改變Context當前的狀態 4. 狀態物件維護的上下文指標可以在狀態物件被建立時,用狀態物件的建構子建立上下文指標,或是在上下文類的SetState()中去呼叫狀態類的SetContext(); 上下文物件被生成時,會生成一個預設狀態物件給它,當上下文物件的狀態改變:清除當前狀態物件並生成一個新的狀態物件;或是上下文物件被清除時,要清除當前狀態物件。上下文物件負責管理狀態物件的生命週期,為了不手動delete,所以會用std::unique_ptr\<State>。 在狀態類裡面宣告上下文物件指標的目的是為了使用上下文類裡面的方法(狀態類裡面的某些成員函數可能會需要上下文物件的資訊 = 根據該資訊改變行為) = 狀態物件是借用上下文物件去使用他的資訊 = 用raw指標或引用 = 狀態物件不負責管理上下文物件的生命週期。 對於只能將state宣告為friend之外好像沒其他更好的方法了。 ## 實做 [状态设计模式](https://refactoringguru.cn/design-patterns/state?_gl=1*udktq9*_ga*MTM5NjM0OTAzMi4xNzI5MDQ5NDcy*_ga_SR8Y3GYQYC*MTczMzEwMjAxOC45MC4wLjE3MzMxMDIwMTguNjAuMC4w) : 看他的class UML圖 下面的設計是認為,class Context跟class State是composition不是aggregation,state物件的生命週期是Context(的SetState())掌控。 教學中的需求是State物件可以同時給不同的class context物件使用,此時就是aggregation。 做法: 1. 建一個interface State,裡面有那幾個原本Context class中會根據某一屬性(狀態)而改變的method,這裡宣告為純虛函數。 2. 假設Context class會有五種狀態(該屬性有五種可能的數值),寫五個interface State的子類實做這些方法。 3. 修改原本Context class的那些根據狀態改變行為的methods主體,變成呼叫state_->method();該class本身要有指向該state的屬性 4. 通常至少會有一個method會改變原本Context class的狀態,因此interface State本身要存一個指向Context class的指標。 5. 一般是Context class會有個預設的state(建立一個state物件給Context class),當Context class的狀態改變,delete預設的state物件,並建立一個新的state物件。 假設原本Context class有5個methods會因為一個state(比如某一個attirbute)而改變行為,那用state pattern 重構後Context class還是會有5個methods,只是method body變成去呼叫State(也有五個對應的methods)裡面對應的method。 ### class State要存指向原Context class的指標 根據需求,Context class的狀態有可能被 1. Context class內的(因state而改變行為的、或一般的)methods改變 2. 被客戶端改變(在class外被改變,比如main()) 因此比較好的設計是在Context class內定義一個SetState()去變更該Context class物件的state = 刪除舊的state物件、建立新的state物件: ```cpp= void SetState() { delete state_; state_ = new State1(); state_->SetContext = this; } ``` 所以class State要存指向原Context class的指標,才可以呼叫到他的SetState()。 ### 重構前後的code對比 1. class Context裡面 1. 多了state_屬性 2. 多了SetState()方法 3. 會因state_而改變的方法的實做給Interface State的子類去做 4. 因為1.所以預設建構子應該對state_給個預設值 5. class State建構子要傳入Context指標。 2. 多了interface State跟實做它的子類。 補充:State做為Interface很適合,因為他的職責就是定義這些(會因狀態而改變)行為。 ## 狀態模式 context物件更新狀態:建新的狀態物件、刪除舊的狀態物件:直接用unique_ptr ## 實做 [C++ 状态模式讲解和代码示例](https://refactoringguru.cn/design-patterns/state/cpp/example) 他的code寫得很好。 class State的屬性Context\* 應該是要在State內建立一個SetState(),並用此方法去設定該State物件的屬性Context\*,而不是在State的建構子裡去傳Context\*。 因為State... 需求: class Context的State\*資料成員可被 1. Context的建構子初始化 : 建立狀態物件 2. 可被外部(比如main())設定 : public: void SetState() 3. 可能被Method()改變 : 刪除當前狀態物件並生成新的狀態物件給上下文物件 因為Method()的實做都放在狀態類裡,又因為Method()有可能改變當前上下文物件的狀態,所以需要呼叫上下文類的SetState(),那就要維護一個指向Context的指標。 因為一個state物件在整個生命週期中只會指向唯一一個上下文物件,並且是被建立就馬上指向該上下文物件,持有它直到該狀態物件的生命週期結束。可以用建構子或是寫SetContext()達到此目的。 void SetState(): 刪除當前狀態物件並生成新的狀態物件給上下文物件 Context跟State的關係: 上下文物件擁有狀態物件並掌管他的生命週期:has-a、aggregation 上下文類的state\*適合使用unique_ptr。這樣外面傳進來後必須轉移所有權,外面的人就不能用他了。增加了上下文類的建構子跟SetState()的安全性。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up