第四部分:行為型模式 - 狀態模式 (State Pattern)
我們繼續學習行為型模式,接下來是狀態模式。這個模式允許一個對象在其內部狀態改變時改變它的行為,對象看起來就像是改變了它的類。
- 核心思想:允許一個對象在其內部狀態改變時改變它的行為。對象看起來似乎修改了它的類。
狀態模式 (State Pattern)
“允許一個對象在其內部狀態改變時改變它的行為。對象看起來似乎修改了它的類。” (Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.)
想象一下一個自動售貨機:
- 自動售貨機 (Context):這是我們的主要對象。
- 狀態 (State):售貨機有多種狀態,比如:
- 無幣狀態 (NoCoinState):沒有投幣。此時你按購買按鈕是無效的。
- 有幣狀態 (HasCoinState):已經投幣。此時你可以選擇商品或退幣。
- 售出商品狀態 (SoldState):正在出貨。此時你不能再投幣或選擇商品。
- 商品售罄狀態 (SoldOutState):所有商品都賣完了。此時投幣會立即退回,選擇商品無效。
當用戶進行操作(如投幣、按按鈕)時,售貨機的行為會根據其當前狀態而有所不同。例如,在“無幣狀態”下投幣,售貨機會轉換到“有幣狀態”。在“有幣狀態”下按購買按鈕,如果商品充足,售貨機會轉換到“售出商品狀態”,然后(如果還有貨)可能回到“無幣狀態”。
狀態模式將每種狀態的行為封裝在不同的狀態對象中,Context 對象(售貨機)會將行為委托給當前的狀態對象。當 Context 的狀態改變時,它會切換到另一個狀態對象,從而改變其行為。
1. 目的 (Intent)
狀態模式的主要目的:
- 封裝與狀態相關的行為:將不同狀態下的行為邏輯分離到各自的狀態類中。
- 使狀態轉換明確:狀態轉換的邏輯可以分布在狀態類中,或者由 Context 統一管理。
- 避免大量的條件語句:如果不使用狀態模式,Context 類中可能會充斥著大量的
if/else
或switch
語句來根據當前狀態執行不同的行為。狀態模式通過多態性消除了這些條件分支。 - 使對象看起來像改變了類:當對象的狀態改變時,它的行為也隨之改變,給外部調用者的感覺就像是對象的類發生了變化。
2. 生活中的例子 (Real-world Analogy)
-
電燈開關:
- 狀態:開 (OnState),關 (OffState)。
- 行為:按下開關。在“關”狀態下按,燈會亮,狀態變為“開”。在“開”狀態下按,燈會滅,狀態變為“關”。
-
TCP連接狀態:
- 狀態:已建立 (Established),監聽 (Listen),關閉 (Closed),正在關閉 (Closing) 等。
- 行為:發送數據、接收數據、關閉連接等操作在不同狀態下有不同的表現或限制。
-
播放器狀態:
- 狀態:播放中 (PlayingState),暫停 (PausedState),停止 (StoppedState)。
- 行為:點擊播放/暫停按鈕,點擊停止按鈕。在不同狀態下,這些按鈕的行為不同。
-
游戲角色的狀態:
- 狀態:站立、行走、跑步、跳躍、攻擊、防御、中毒、冰凍等。
- 行為:玩家輸入指令(如移動、攻擊)時,角色的響應會根據其當前狀態而變化。
3. 結構 (Structure)
狀態模式通常包含以下角色:
-
Context (上下文):
- 定義客戶端感興趣的接口。
- 維護一個 ConcreteState 子類的實例,這個實例定義當前狀態。
- 可以將行為委托給當前的狀態對象。
- 負責狀態的轉換,可以由 Context 自身或 State 對象來管理轉換邏輯。
-
State (狀態接口或抽象類):
- 定義一個接口以封裝與 Context 的一個特定狀態相關的行為。
- 通常包含處理各種請求的方法,這些方法的實現在具體狀態類中。
-
ConcreteState (具體狀態):
- 實現 State 接口。
- 每一個子類實現一個與 Context 的一種狀態相關的行為。
- 可以負責狀態的轉換,即在執行完某個行為后,改變 Context 的當前狀態到下一個狀態。
狀態轉換的職責:
- 由 Context 決定:Context 接收到請求后,根據當前狀態和請求類型,決定轉換到哪個新狀態。
- 由 State 子類決定:每個 ConcreteState 在處理完請求后,自行決定下一個狀態是什么,并通知 Context 改變狀態。這種方式更符合“狀態對象知道下一個狀態”的邏輯,但可能導致狀態類之間產生依賴。
4. 適用場景 (When to Use)
- 一個對象的行為取決于它的狀態,并且它必須在運行時刻根據狀態改變它的行為。
- 代碼中包含大量與對象狀態有關的條件語句(
if/else
或switch
)。狀態模式可以將這些分支邏輯分散到不同的狀態類中。 - 當操作中含有龐大的多分支的條件語句,且這些分支依賴于該對象的狀態時。狀態模式將每一個分支封裝到一個獨立的類中。
- 當你希望代碼更清晰地表達狀態和狀態轉換時。
5. 優缺點 (Pros and Cons)
優點:
- 封裝了與狀態相關的行為:將所有與特定狀態相關的行為都放入一個對象中,使得代碼更加集中和易于維護。
- 使得狀態轉換明確:將狀態轉換邏輯封裝在狀態類或 Context 中,使得狀態轉換的規則更加清晰。
- 消除了大量的條件分支:通過多態性替代了冗長的
if/else
或switch
語句,使代碼更簡潔,更易于理解和擴展。 - 易于增加新的狀態:增加新的狀態只需要添加一個新的 State 子類,并修改相關的轉換邏輯,符合開閉原則。
缺點:
- 類數量增多:狀態模式會導致系統中類的數量增加,每個狀態都需要一個對應的類。
- 結構可能變得復雜:如果狀態過多或者狀態轉換邏輯非常復雜,整個系統的結構可能會變得難以理解。
- Context 與 State 的耦合:State 對象通常需要訪問 Context 對象來改變其狀態或獲取 Context 的數據,這可能導致一定的耦合。如果 State 對象也負責狀態轉換,那么 State 對象之間也可能產生耦合。
6. 實現方式 (Implementations)
讓我們以一個簡單的文檔編輯器為例,它有草稿 (Draft)、審核中 (Moderation) 和已發布 (Published) 三種狀態。
狀態接口 (State)
// document_state.go (State interface and concrete states)
package state// Forward declaration for Document context
type Documenter interface {SetState(state Statelike)// Potentially other methods the state might need from the documentGetContent() stringSetContent(content string)
}// Statelike 狀態接口
type Statelike interface {Render(doc Documenter)Publish(doc Documenter)Review(doc Documenter) // New method for review processGetName() string
}
// State.java (State interface)
package com.example.state;public interface State {void render(Document document);void publish(Document document);void review(Document document); // New method for review processString getName();
}
具體狀態 (DraftState, ModerationState, PublishedState)
// document_state.go (continued)
package stateimport "fmt"// --- DraftState --- (草稿狀態)
type DraftState struct{}func NewDraftState() *DraftState { return &DraftState{} }func (s *DraftState) Render(doc Documenter) {fmt.Printf("Draft: Rendering content - '%s' (can be edited)\n", doc.GetContent())
}func (s *DraftState) Publish(doc Documenter) {fmt.Println("Draft: Content submitted for review.")doc.SetState(NewModerationState()) // Transition to Moderation
}func (s *DraftState) Review(doc Documenter) {fmt.Println("Draft: Cannot review a draft directly. Submit for review first.")
}func (s *DraftState) GetName() string { return "Draft" }// --- ModerationState --- (審核中狀態)
type ModerationState struct{}func NewModerationState() *ModerationState { return &ModerationState{} }func (s *ModerationState) Render(doc Documenter) {fmt.Printf("Moderation: Rendering content - '%s' (awaiting review, read-only)\n", doc.GetContent())
}func (s *ModerationState) Publish(doc Documenter) {fmt.Println("Moderation: Content approved and published.")doc.SetState(NewPublishedState()) // Transition to Published
}func (s *ModerationState) Review(doc Documenter) {// Simulate review logic, e.g., admin approves or rejects// For simplicity, let's assume it's always approved here by calling Publishfmt.Println("Moderation: Reviewing content...")s.Publish(doc) // If approved, it publishes// If rejected, it might go back to DraftState:// fmt.Println("Moderation: Content rejected, returning to draft.")// doc.SetState(NewDraftState())
}func (s *ModerationState) GetName() string { return "Moderation" }// --- PublishedState --- (已發布狀態)
type PublishedState struct{}func NewPublishedState() *PublishedState { return &PublishedState{} }func (s *PublishedState) Render(doc Documenter) {fmt.Printf("Published: Displaying content - '%s' (live, read-only)\n", doc.GetContent())
}func (s *PublishedState) Publish(doc Documenter) {fmt.Println("Published: Content is already published.")
}func (s *PublishedState) Review(doc Documenter) {fmt.Println("Published: Content is already published, no further review needed.")
}func (s *PublishedState) GetName() string { return "Published" }
// DraftState.java
package com.example.state;public class DraftState implements State {@Overridepublic void render(Document document) {System.out.println("Draft: Rendering content - '" + document.getContent() + "' (can be edited)");}@Overridepublic void publish(Document document) {System.out.println("Draft: Content submitted for review.");document.setState(new ModerationState()); // Transition to Moderation}@Overridepublic void review(Document document) {System.out.println("Draft: Cannot review a draft directly. Submit for review first.");}@Overridepublic String getName() {return "Draft";}
}// ModerationState.java
package com.example.state;public class ModerationState implements State {@Overridepublic void render(Document document) {System.out.println("Moderation: Rendering content - '" + document.getContent() + "' (awaiting review, read-only)");}@Overridepublic void publish(Document document) { // This is effectively 'approve and publish'System.out.println("Moderation: Content approved and published.");document.setState(new PublishedState()); // Transition to Published}@Overridepublic void review(Document document) {// In a real scenario, this might involve more complex logic or user roles.// For simplicity, let's say reviewing it means it's ready for publishing.System.out.println("Moderation: Reviewing content... Content looks good!");// If approved, it transitions to Published. This could be done here or by calling publish().// Let's assume the 'publish' action from Moderation state means 'approve and publish'.// If rejected, it might go back to DraftState:// System.out.println("Moderation: Content rejected, returning to draft.");// document.setState(new DraftState());// For this example, let's assume review leads to publish if called.this.publish(document); // Or a more specific 'approve' method that then calls publish.}@Overridepublic String getName() {return "Moderation";}
}// PublishedState.java
package com.example.state;public class PublishedState implements State {@Overridepublic void render(Document document) {System.out.println("Published: Displaying content - '" + document.getContent() + "' (live, read-only)");}@Overridepublic void publish(Document document) {System.out.println("Published: Content is already published.");}@Overridepublic void review(Document document) {System.out.println("Published: Content is already published, no further review needed.");}@Overridepublic String getName() {return "Published";}
}
上下文 (Document - Context)
// document.go (Context)
package context // Renamed package to avoid conflict with built-in contextimport ("../state""fmt"
)// Document 上下文
type Document struct {currentState state.Statelikecontent string
}func NewDocument() *Document {doc := &Document{}doc.currentState = state.NewDraftState() // Initial statedoc.content = "Initial draft content."fmt.Printf("Document created. Initial state: %s\n", doc.currentState.GetName())return doc
}func (d *Document) SetState(s state.Statelike) {fmt.Printf("Document: Changing state from %s to %s\n", d.currentState.GetName(), s.GetName())d.currentState = s
}func (d *Document) GetContent() string {return d.content
}func (d *Document) SetContent(content string) {if d.currentState.GetName() == "Draft" { // Only allow content change in Draft stated.content = contentfmt.Printf("Document: Content updated to '%s'\n", content)} else {fmt.Printf("Document: Cannot set content in %s state.\n", d.currentState.GetName())}
}// Delegate actions to the current state
func (d *Document) Render() {d.currentState.Render(d)
}func (d *Document) Publish() {d.currentState.Publish(d)
}func (d *Document) Review() {d.currentState.Review(d)
}func (d *Document) GetCurrentStateName() string {return d.currentState.GetName()
}
// Document.java (Context)
package com.example.state;public class Document {private State currentState;private String content;public Document() {this.currentState = new DraftState(); // Initial statethis.content = "Initial draft content.";System.out.println("Document created. Initial state: " + currentState.getName());}public void setState(State state) {System.out.println("Document: Changing state from " + this.currentState.getName() + " to " + state.getName());this.currentState = state;}public String getContent() {return content;}public void setContent(String content) {// Example: Only allow content change in Draft stateif (this.currentState instanceof DraftState) {this.content = content;System.out.println("Document: Content updated to '" + content + "'");} else {System.out.println("Document: Cannot set content in " + this.currentState.getName() + " state.");}}// Delegate actions to the current statepublic void render() {this.currentState.render(this);}public void publish() {this.currentState.publish(this);}public void review() {this.currentState.review(this);}public String getCurrentStateName() {return this.currentState.getName();}
}
客戶端使用
// main.go (示例用法)
/*
package mainimport ("./context""fmt"
)func main() {doc := context.NewDocument()fmt.Println("\n--- Current State:" , doc.GetCurrentStateName(), "---")doc.Render() // Draft: Rendering content...doc.SetContent("My awesome first draft!")doc.Render()fmt.Println("\n--- Attempting to publish (from Draft) ---")doc.Publish() // Draft: Content submitted for review. (Transitions to Moderation)fmt.Println("\n--- Current State:" , doc.GetCurrentStateName(), "---")doc.Render() // Moderation: Rendering content...doc.SetContent("Trying to edit in moderation") // Cannot set contentdoc.Publish() // Moderation: Content approved and published. (Transitions to Published if publish means approve)// If publish from Moderation means 'request publish again', then it might stay or error.// In our example, ModerationState.publish() transitions to PublishedState.fmt.Println("\n--- Current State:" , doc.GetCurrentStateName(), "---")doc.Render() // Published: Displaying content...doc.Publish() // Published: Content is already published.// Let's try the review processfmt.Println("\n--- Resetting to a new document for review flow ---")doc2 := context.NewDocument()doc2.SetContent("Content for review process")doc2.Render()fmt.Println("\n--- Submitting for review (Publish from Draft) ---")doc2.Publish() // Transitions to Moderationfmt.Println("\n--- Current State:" , doc2.GetCurrentStateName(), "---")doc2.Render()fmt.Println("\n--- Reviewing the content (from Moderation) ---")doc2.Review() // Moderation: Reviewing content... Content approved and published. (Transitions to Published)fmt.Println("\n--- Current State:" , doc2.GetCurrentStateName(), "---")doc2.Render()
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.state.Document;public class Main {public static void main(String[] args) {Document doc = new Document();System.out.println("\n--- Current State: " + doc.getCurrentStateName() + " ---");doc.render(); // Draft: Rendering content...doc.setContent("My awesome first draft!");doc.render();System.out.println("\n--- Attempting to publish (from Draft) ---");doc.publish(); // Draft: Content submitted for review. (Transitions to Moderation)System.out.println("\n--- Current State: " + doc.getCurrentStateName() + " ---");doc.render(); // Moderation: Rendering content...doc.setContent("Trying to edit in moderation"); // Cannot set content// In our ModerationState, publish() means 'approve and publish'.// If we want a separate 'approve' action, we'd add an 'approve()' method to State and ConcreteStates.System.out.println("\n--- Attempting to publish/approve (from Moderation) ---");doc.publish(); // Moderation: Content approved and published. (Transitions to Published)System.out.println("\n--- Current State: " + doc.getCurrentStateName() + " ---");doc.render(); // Published: Displaying content...doc.publish(); // Published: Content is already published.// Let's try the review process more explicitlySystem.out.println("\n--- Resetting to a new document for review flow ---");Document doc2 = new Document();doc2.setContent("Content for review process");doc2.render();System.out.println("\n--- Submitting for review (Publish from Draft) ---");doc2.publish(); // Transitions to ModerationSystem.out.println("\n--- Current State: " + doc2.getCurrentStateName() + " ---");doc2.render();System.out.println("\n--- Reviewing the content (from Moderation) ---");doc2.review(); // Moderation: Reviewing content... Content approved and published. (Transitions to Published)System.out.println("\n--- Current State: " + doc2.getCurrentStateName() + " ---");doc2.render();}
}
*/
7. 狀態模式 vs. 策略模式 (State vs. Strategy)
狀態模式和策略模式在結構上非常相似(都依賴于組合和委托,將行為封裝在獨立的對象中),但它們的意圖不同:
-
狀態模式 (State Pattern):
- 意圖:允許一個對象在其內部狀態改變時改變它的行為。關注點是對象在不同狀態下的行為變化。
- 如何改變行為:Context 或 State 對象自身在運行時改變 Context 持有的 State 對象,從而改變行為。
- 客戶端:客戶端通常不直接選擇狀態。狀態的改變是內部驅動的(基于操作的結果)或由 Context 自動管理的。
- 生命周期:State 對象通常代表對象生命周期中的不同階段或條件。
-
策略模式 (Strategy Pattern):
- 意圖:定義一系列算法,將它們封裝起來,并使它們可以互相替換。關注點是提供多種算法選擇,并使它們可互換。
- 如何改變行為:客戶端在運行時選擇并向 Context 傳遞一個具體的 Strategy 對象。
- 客戶端:客戶端通常知道有多種策略,并主動選擇一個策略來配置 Context。
- 生命周期:Strategy 對象通常代表解決某個問題的不同方法或算法,與對象的內部狀態不一定直接關聯。
簡單來說:
- 用狀態模式來表示“我是誰”(我的當前狀態決定了我的行為)。狀態轉換通常是預定義的,并且可能由對象內部事件觸發。
- 用策略模式來表示“我如何做”(我選擇哪種算法來完成任務)。策略通常由客戶端在外部設置。
在某些情況下,狀態對象本身也可以使用策略模式來處理其內部的某些行為變化,但這已經是模式的組合應用了。
8. 總結
狀態模式是一種強大的行為設計模式,它通過將與特定狀態相關的行為局部化,并將這些行為委托給代表當前狀態的對象,從而使得對象在內部狀態改變時能夠改變其行為。這不僅消除了大量的條件判斷語句,使得代碼更加清晰和易于維護,還使得添加新的狀態和轉換變得更加容易。當你發現一個對象的行為高度依賴于其內部狀態,并且這些狀態和轉換可以用清晰的界限劃分時,狀態模式會是一個非常好的選擇。