第四部分:行为型模式 - 状态模式 (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. 总结
状态模式是一种强大的行为设计模式,它通过将与特定状态相关的行为局部化,并将这些行为委托给代表当前状态的对象,从而使得对象在内部状态改变时能够改变其行为。这不仅消除了大量的条件判断语句,使得代码更加清晰和易于维护,还使得添加新的状态和转换变得更加容易。当你发现一个对象的行为高度依赖于其内部状态,并且这些状态和转换可以用清晰的界限划分时,状态模式会是一个非常好的选择。