零基础设计模式——行为型模式 - 状态模式

第四部分:行为型模式 - 状态模式 (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)

状态模式的主要目的:

  1. 封装与状态相关的行为:将不同状态下的行为逻辑分离到各自的状态类中。
  2. 使状态转换明确:状态转换的逻辑可以分布在状态类中,或者由 Context 统一管理。
  3. 避免大量的条件语句 :如果不使用状态模式,Context 类中可能会充斥着大量的 if/elseswitch 语句来根据当前状态执行不同的行为。状态模式通过多态性消除了这些条件分支。
  4. 使对象看起来像改变了类:当对象的状态改变时,它的行为也随之改变,给外部调用者的感觉就像是对象的类发生了变化。

2. 生活中的例子 (Real-world Analogy)

  • 电灯开关

    • 状态:开 (OnState),关 (OffState)。
    • 行为:按下开关。在"关"状态下按,灯会亮,状态变为"开"。在"开"状态下按,灯会灭,状态变为"关"。
  • TCP连接状态

    • 状态:已建立 (Established),监听 (Listen),关闭 (Closed),正在关闭 (Closing) 等。
    • 行为:发送数据、接收数据、关闭连接等操作在不同状态下有不同的表现或限制。
  • 播放器状态

    • 状态:播放中 (PlayingState),暂停 (PausedState),停止 (StoppedState)。
    • 行为:点击播放/暂停按钮,点击停止按钮。在不同状态下,这些按钮的行为不同。
  • 游戏角色的状态

    • 状态:站立、行走、跑步、跳跃、攻击、防御、中毒、冰冻等。
    • 行为:玩家输入指令(如移动、攻击)时,角色的响应会根据其当前状态而变化。

3. 结构 (Structure)

状态模式通常包含以下角色:

  1. Context (上下文)

    • 定义客户端感兴趣的接口。
    • 维护一个 ConcreteState 子类的实例,这个实例定义当前状态。
    • 可以将行为委托给当前的状态对象。
    • 负责状态的转换,可以由 Context 自身或 State 对象来管理转换逻辑。
  2. State (状态接口或抽象类)

    • 定义一个接口以封装与 Context 的一个特定状态相关的行为。
    • 通常包含处理各种请求的方法,这些方法的实现在具体状态类中。
  3. ConcreteState (具体状态)

    • 实现 State 接口。
    • 每一个子类实现一个与 Context 的一种状态相关的行为。
    • 可以负责状态的转换,即在执行完某个行为后,改变 Context 的当前状态到下一个状态。

状态转换的职责

  • 由 Context 决定:Context 接收到请求后,根据当前状态和请求类型,决定转换到哪个新状态。
  • 由 State 子类决定:每个 ConcreteState 在处理完请求后,自行决定下一个状态是什么,并通知 Context 改变状态。这种方式更符合"状态对象知道下一个状态"的逻辑,但可能导致状态类之间产生依赖。

4. 适用场景 (When to Use)

  • 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为。
  • 代码中包含大量与对象状态有关的条件语句(if/elseswitch)。状态模式可以将这些分支逻辑分散到不同的状态类中。
  • 当操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态时。状态模式将每一个分支封装到一个独立的类中。
  • 当你希望代码更清晰地表达状态和状态转换时。

5. 优缺点 (Pros and Cons)

优点:

  1. 封装了与状态相关的行为:将所有与特定状态相关的行为都放入一个对象中,使得代码更加集中和易于维护。
  2. 使得状态转换明确:将状态转换逻辑封装在状态类或 Context 中,使得状态转换的规则更加清晰。
  3. 消除了大量的条件分支 :通过多态性替代了冗长的 if/elseswitch 语句,使代码更简洁,更易于理解和扩展。
  4. 易于增加新的状态:增加新的状态只需要添加一个新的 State 子类,并修改相关的转换逻辑,符合开闭原则。

缺点:

  1. 类数量增多:状态模式会导致系统中类的数量增加,每个状态都需要一个对应的类。
  2. 结构可能变得复杂:如果状态过多或者状态转换逻辑非常复杂,整个系统的结构可能会变得难以理解。
  3. Context 与 State 的耦合:State 对象通常需要访问 Context 对象来改变其状态或获取 Context 的数据,这可能导致一定的耦合。如果 State 对象也负责状态转换,那么 State 对象之间也可能产生耦合。

6. 实现方式 (Implementations)

让我们以一个简单的文档编辑器为例,它有草稿 (Draft)、审核中 (Moderation) 和已发布 (Published) 三种状态。

状态接口 (State)
go 复制代码
// 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 document
	GetContent() string
	SetContent(content string)
}

// Statelike 状态接口
type Statelike interface {
	Render(doc Documenter)
	Publish(doc Documenter)
	Review(doc Documenter) // New method for review process
	GetName() string
}
java 复制代码
// 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 process
    String getName();
}
具体状态 (DraftState, ModerationState, PublishedState)
go 复制代码
// document_state.go (continued)
package state

import "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 Publish
	fmt.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" }
java 复制代码
// DraftState.java
package com.example.state;

public class DraftState implements State {
    @Override
    public void render(Document document) {
        System.out.println("Draft: Rendering content - '" + document.getContent() + "' (can be edited)");
    }

    @Override
    public void publish(Document document) {
        System.out.println("Draft: Content submitted for review.");
        document.setState(new ModerationState()); // Transition to Moderation
    }

    @Override
    public void review(Document document) {
        System.out.println("Draft: Cannot review a draft directly. Submit for review first.");
    }

    @Override
    public String getName() {
        return "Draft";
    }
}

// ModerationState.java
package com.example.state;

public class ModerationState implements State {
    @Override
    public void render(Document document) {
        System.out.println("Moderation: Rendering content - '" + document.getContent() + "' (awaiting review, read-only)");
    }

    @Override
    public 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
    }

    @Override
    public 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.
    }

    @Override
    public String getName() {
        return "Moderation";
    }
}

// PublishedState.java
package com.example.state;

public class PublishedState implements State {
    @Override
    public void render(Document document) {
        System.out.println("Published: Displaying content - '" + document.getContent() + "' (live, read-only)");
    }

    @Override
    public void publish(Document document) {
        System.out.println("Published: Content is already published.");
    }

    @Override
    public void review(Document document) {
        System.out.println("Published: Content is already published, no further review needed.");
    }

    @Override
    public String getName() {
        return "Published";
    }
}
上下文 (Document - Context)
go 复制代码
// document.go (Context)
package context // Renamed package to avoid conflict with built-in context

import (
	"../state"
	"fmt"
)

// Document 上下文
type Document struct {
	currentState state.Statelike
	content      string
}

func NewDocument() *Document {
	doc := &Document{}
	doc.currentState = state.NewDraftState() // Initial state
	doc.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 state
		d.content = content
		fmt.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()
}
java 复制代码
// Document.java (Context)
package com.example.state;

public class Document {
    private State currentState;
    private String content;

    public Document() {
        this.currentState = new DraftState(); // Initial state
        this.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 state
        if (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 state
    public 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();
    }
}
客户端使用
go 复制代码
// main.go (示例用法)
/*
package main

import (
	"./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 content
	doc.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 process
	fmt.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 Moderation

	fmt.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()
}
*/
java 复制代码
// 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 explicitly
        System.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 Moderation

        System.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. 总结

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

相关推荐
英杰.王6 分钟前
设计模式-里氏替换原则(Liskov Substitution Principle, LSP)
设计模式·里氏替换原则
小巫程序Demo日记8 分钟前
Spark DAG、Stage 划分与 Task 调度底层原理深度剖析
java·spark
Java中文社群20 分钟前
ChatClient vs ChatModel:开发者必须知道的4大区别!
java·人工智能·后端
刘一说24 分钟前
资深Java工程师的面试题目(五)微服务
java·微服务·面试
boy快快长大28 分钟前
【JUC】显示锁
java
写bug写bug1 小时前
Spring Cloud中的@LoadBalanced注解实现原理
java·后端·spring cloud
青木川崎1 小时前
java获取天气信息
java
HoroMin1 小时前
在Spring Boot中自定义JSON返回日期格式的指南
java·开发语言·spring boot·注解
浮游本尊2 小时前
Java学习第6天 - 多线程编程基础
java
tq10862 小时前
值类:Kotlin中的零成本抽象
java·linux·前端