在软件开发中,很多对象的行为会随着其内部状态的变化而改变。如果将所有状态逻辑写在一个类中,代码不仅臃肿而且难以维护。**状态模式(State Pattern)**正是为了解决这个问题而设计的。通过将对象的状态封装成独立的状态类,并将状态相关的行为转移到这些状态类中,状态模式让对象在内部状态发生变化时自动切换行为,达到了将状态转换与行为实现分离的目的。
本文将详细介绍状态模式的核心思想、基本结构与优缺点,并通过多个示例展示状态模式在不同场景下的应用,包括交通信号灯、自动售货机、UI 按钮状态以及游戏角色状态等多个方面。
状态模式简介
状态模式是一种行为型设计模式,其主要思想是将对象在不同状态下的行为封装到不同的状态类中,并通过上下文(Context)对象维护当前状态。当上下文状态发生变化时,它会将请求委托给当前状态对象,从而使得对象的行为随着状态的改变而改变。
例如,一个交通信号灯对象,根据当前状态(红灯、绿灯、黄灯)的不同,会执行不同的操作;而一个自动售货机在“无硬币”、“有硬币”和“售出商品”等状态下,其响应行为也各不相同。状态模式通过分离状态逻辑,使得代码更易维护和扩展。
状态模式的结构与特点
主要角色:
-
上下文(Context)
维护一个对当前状态对象的引用,并委托状态对象处理请求。 -
状态接口(State Interface)
定义状态对象必须实现的方法,通常是与上下文相关的行为。 -
具体状态(Concrete State)
实现状态接口的类,每个类代表一种状态,并封装在该状态下的行为。
特点:
- 状态封装:将与状态相关的行为封装到独立的状态类中,使得状态变化与行为实现解耦。
- 行为动态切换:上下文对象通过持有状态对象的引用,根据内部状态的变化动态切换行为。
- 易于扩展:增加新状态只需增加新的具体状态类,而无需修改上下文类代码。
JavaScript 实现示例
下面通过多个示例展示如何在 JavaScript 中使用状态模式。
示例 1:交通信号灯
交通信号灯根据当前状态(红灯、绿灯、黄灯)的不同,执行不同的动作,并在状态之间循环切换。
// 定义状态接口及具体状态类
class TrafficLightState {constructor(context) {this.context = context;}handle() {throw new Error('子类必须实现 handle 方法');}
}class RedState extends TrafficLightState {handle() {console.log('红灯:停止');// 状态转换到绿灯this.context.setState(new GreenState(this.context));}
}class GreenState extends TrafficLightState {handle() {console.log('绿灯:通行');// 状态转换到黄灯this.context.setState(new YellowState(this.context));}
}class YellowState extends TrafficLightState {handle() {console.log('黄灯:注意');// 状态转换到红灯this.context.setState(new RedState(this.context));}
}// 上下文类:交通信号灯
class TrafficLight {constructor() {// 初始状态设为红灯this.state = new RedState(this);}setState(state) {this.state = state;}change() {this.state.handle();}
}// 客户端调用
const trafficLight = new TrafficLight();
trafficLight.change(); // 输出:红灯:停止 -> 状态切换为绿灯
trafficLight.change(); // 输出:绿灯:通行 -> 状态切换为黄灯
trafficLight.change(); // 输出:黄灯:注意 -> 状态切换为红灯
示例 2:自动售货机
自动售货机在不同状态下的行为不同,如“无硬币”、“有硬币”、“售出商品”状态。状态模式可以帮助我们将这些逻辑拆分为独立的状态类。
// 状态接口
class VendingState {constructor(machine) {this.machine = machine;}insertCoin() {throw new Error('insertCoin 方法未实现');}selectProduct() {throw new Error('selectProduct 方法未实现');}dispenseProduct() {throw new Error('dispenseProduct 方法未实现');}
}// 具体状态:无硬币状态
class NoCoinState extends VendingState {insertCoin() {console.log('硬币已投入');this.machine.setState(new HasCoinState(this.machine));}selectProduct() {console.log('请先投入硬币');}dispenseProduct() {console.log('请先投入硬币');}
}// 具体状态:有硬币状态
class HasCoinState extends VendingState {insertCoin() {console.log('已投入硬币,请勿重复投币');}selectProduct() {console.log('产品已选择');this.machine.setState(new SoldState(this.machine));}dispenseProduct() {console.log('请先选择产品');}
}// 具体状态:售出状态
class SoldState extends VendingState {insertCoin() {console.log('正在处理当前交易,请稍候');}selectProduct() {console.log('产品已选择,请稍候');}dispenseProduct() {console.log('产品已出货');// 交易完成后恢复为无硬币状态this.machine.setState(new NoCoinState(this.machine));}
}// 上下文类:自动售货机
class VendingMachine {constructor() {this.state = new NoCoinState(this);}setState(state) {this.state = state;}insertCoin() {this.state.insertCoin();}selectProduct() {this.state.selectProduct();}dispenseProduct() {this.state.dispenseProduct();}
}// 客户端调用
const vendingMachine = new VendingMachine();
vendingMachine.selectProduct(); // 输出:请先投入硬币
vendingMachine.insertCoin(); // 输出:硬币已投入
vendingMachine.insertCoin(); // 输出:已投入硬币,请勿重复投币
vendingMachine.selectProduct(); // 输出:产品已选择
vendingMachine.dispenseProduct(); // 输出:产品已出货
示例 3:UI 按钮状态切换
在前端 UI 中,一个按钮可能处于“启用”、“禁用”以及“加载中”等状态,不同状态下的按钮表现不同。状态模式可以使按钮行为与状态解耦。
// 状态接口
class ButtonState {constructor(button) {this.button = button;}click() {throw new Error('click 方法未实现');}render() {throw new Error('render 方法未实现');}
}// 具体状态:启用状态
class EnabledState extends ButtonState {click() {console.log('按钮被点击,开始加载数据...');this.button.setState(new LoadingState(this.button));}render() {console.log('渲染启用按钮');}
}// 具体状态:禁用状态
class DisabledState extends ButtonState {click() {console.log('按钮处于禁用状态,无法点击');}render() {console.log('渲染禁用按钮');}
}// 具体状态:加载中状态
class LoadingState extends ButtonState {click() {console.log('加载中,无法再次点击');}render() {console.log('渲染加载中按钮');}
}// 上下文类:UI 按钮
class Button {constructor() {// 默认状态为启用状态this.state = new EnabledState(this);}setState(state) {this.state = state;this.state.render();}click() {this.state.click();}
}// 客户端调用
const uiButton = new Button();
uiButton.click(); // 输出:按钮被点击,开始加载数据... 并切换到加载中状态
uiButton.click(); // 输出:加载中,无法再次点击
uiButton.setState(new DisabledState(uiButton));
uiButton.click(); // 输出:按钮处于禁用状态,无法点击
示例 4:游戏角色状态
在游戏中,一个角色可能有“空闲”、“奔跑”、“攻击”、“跳跃”等不同状态,每个状态下角色的行为和表现各不相同。利用状态模式,可以轻松管理角色状态及其转换。
// 状态接口
class CharacterState {constructor(character) {this.character = character;}handle() {throw new Error('handle 方法未实现');}
}// 具体状态:空闲状态
class IdleState extends CharacterState {handle() {console.log('角色处于空闲状态');// 根据条件可以切换到其他状态// this.character.setState(new RunningState(this.character));}
}// 具体状态:奔跑状态
class RunningState extends CharacterState {handle() {console.log('角色正在奔跑');// this.character.setState(new JumpingState(this.character));}
}// 具体状态:跳跃状态
class JumpingState extends CharacterState {handle() {console.log('角色正在跳跃');// 完成跳跃后恢复到空闲状态this.character.setState(new IdleState(this.character));}
}// 上下文类:游戏角色
class GameCharacter {constructor() {this.state = new IdleState(this);}setState(state) {this.state = state;}performAction() {this.state.handle();}
}// 客户端调用
const hero = new GameCharacter();
hero.performAction(); // 输出:角色处于空闲状态
hero.setState(new RunningState(hero));
hero.performAction(); // 输出:角色正在奔跑
hero.setState(new JumpingState(hero));
hero.performAction(); // 输出:角色正在跳跃,然后状态切换回空闲状态
hero.performAction(); // 输出:角色处于空闲状态
状态模式的优缺点
优点
- 清晰的状态分离:将每个状态的行为封装在独立的状态类中,便于管理和扩展。
- 简化上下文逻辑:上下文类只需维护当前状态,通过委托状态对象处理请求,避免了大量的条件判断。
- 动态状态切换:在运行时可根据条件动态切换状态,灵活应对复杂业务逻辑。
缺点
- 类数量增多:每种状态都需要单独的类,对于状态较多的系统可能会导致类的数量较多。
- 状态转换逻辑分散:状态之间的转换逻辑可能会分布在不同状态类中,维护时需要特别注意状态之间的依赖关系。
总结
状态模式通过将对象的行为封装在各个状态类中,使得对象能够根据内部状态的变化自动调整行为,避免了大量条件判断和耦合代码。本文详细介绍了状态模式的基本概念、结构和优缺点,并通过交通信号灯、自动售货机、UI 按钮状态切换以及游戏角色状态等多个示例,展示了状态模式在不同场景下的应用。希望这些示例能帮助你在实际项目中更好地管理状态转换和行为变化,提高代码的灵活性与可维护性。
欢迎在评论区分享你的理解与实践经验!