🔥 核心
状态模式让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
一个行为使其从一种状态转移到另一种状态,这和「有限状态自动机」极其相似。
🙁 问题场景
你正在给一个 音乐播放器(MusicPlayer)
的按键编写逻辑。
这个 音乐播放器
看样子很简单,它只有四个按键(锁定键(lock)
、播放键(play)
、上一首键(prev)
、下一首键(next)
),以及三种状态(锁定状态(locked)
、就绪状态(ready)
、播放中状态(playing)
)。
很简单嘛!你立即开始为 锁定键
绑定事件逻辑:
1)if
原本处于锁定状态,则解锁为就绪状态
2)else if
原本处于就绪状态,则重新锁定为锁定状态
3)else if
原本处于播放中状态,则直接锁定为锁定状态
最为一名经验丰富的开发者,你立即意识到问题潜在的严重性:大量的if-elseif逻辑。
这个问题会随着项目进行变得愈加严重。人们很难在设计阶段预测到所有可能的状态及其转换,甚至还有可能为其增加新的按键。随着时间推移,最初仅包含有限条件语句的简洁状态机可能会变成臃肿的一团乱麻!
🙂 解决方案
有限状态自动机!
我们之前的思路是针对 动作
,下面我们转而针对 状态
。状态模式建议为对象的所有可能状态新建一个类,然后将所有状态可能发生的动作啊、转换逻辑啊,抽取到这些类中。
基于这样的思想,就很容易理清思路了:
Locked锁定状态:
1)clickLock:解锁播放器,转化为Ready就绪状态
2)clickPlay:无效操作,因为播放器已锁定,依旧为Locked锁定状态
3)clickPrev:无效操作,因为播放器已锁定,依旧为Locked锁定状态
4)clickNext:无效操作,因为播放器已锁定,依旧为Locked锁定状态
Ready就绪状态:
1)clickLock:直接锁定播放器,转化为Locked锁定状态
2)clickPlay:播放音乐,转化为Playing播放中状态
3)clickPrev:无效操作,因为没有播放记录,依旧为Ready就绪状态
4)clickNext:无效操作,因为没有播放记录,依旧为Ready就绪状态
Playing播放中状态:
1)clickLock:直接锁定播放器,转化为Locked锁定状态
2)clickPlay:暂停音乐,转化为Ready就绪状态
3)clickPrev:播放上一首,依旧为Playing播放中状态
4)clickNext:播放下一首,依旧为Playing播放中状态
🌈 有趣的例子
我们分析了这么久音乐播放器的例子,不妨大胆实现出来吧!
状态(抽象类)
abstract class State {
protected MusicPlayer musicPlayer;
public State(MusicPlayer musicPlayer) {
this.musicPlayer = musicPlayer;
}
public abstract void clickLock();
public abstract void clickPlay();
public abstract void clickPrev();
public abstract void clickNext();
}
锁定状态
class LockedState extends State {
public LockedState(MusicPlayer musicPlayer) {
super(musicPlayer);
}
@Override
public void clickLock() {
System.out.println("已解锁播放器!");
musicPlayer.changeState(new ReadyState(musicPlayer));
}
@Override
public void clickPlay() {
System.out.println("无效操作,因为播放器已锁定 >_<");
}
@Override
public void clickPrev() {
System.out.println("无效操作,因为播放器已锁定 >_<");
}
@Override
public void clickNext() {
System.out.println("无效操作,因为播放器已锁定 >_<");
}
}
就绪状态
class ReadyState extends State {
public ReadyState(MusicPlayer musicPlayer) {
super(musicPlayer);
}
@Override
public void clickLock() {
System.out.println("已锁定播放器!");
musicPlayer.changeState(new LockedState(musicPlayer));
}
@Override
public void clickPlay() {
System.out.println("播放音乐~");
musicPlayer.changeState(new PlayingState(musicPlayer));
}
@Override
public void clickPrev() {
System.out.println("无效操作,因为没有播放记录 >_<");
}
@Override
public void clickNext() {
System.out.println("无效操作,因为没有播放记录 >_<");
}
}
播放中状态
class PlayingState extends State {
public PlayingState(MusicPlayer musicPlayer) {
super(musicPlayer);
}
@Override
public void clickLock() {
System.out.println("已锁定播放器!");
musicPlayer.changeState(new LockedState(musicPlayer));
}
@Override
public void clickPlay() {
System.out.println("暂停音乐~");
musicPlayer.changeState(new ReadyState(musicPlayer));
}
@Override
public void clickPrev() {
System.out.println("正在播放上一首音乐...");
}
@Override
public void clickNext() {
System.out.println("正在播放下一首音乐...");
}
}
音乐播放器
class MusicPlayer {
// 默认处于锁定状态
private State state = new LockedState(this);
// 获取当前状态
public State getCurrentState() {
return state;
}
// 进行状态转化
public void changeState(State state) {
this.state = state;
}
}
public class StatePatternDemo {
public static void main(String[] args) {
// 获得一个音乐播放器
MusicPlayer musicPlayer = new MusicPlayer();
// 进行一系列动作
// 并同时进行着状态的转化
musicPlayer.getCurrentState().clickPlay();
musicPlayer.getCurrentState().clickLock();
musicPlayer.getCurrentState().clickPrev();
musicPlayer.getCurrentState().clickNext();
musicPlayer.getCurrentState().clickPlay();
musicPlayer.getCurrentState().clickPrev();
musicPlayer.getCurrentState().clickNext();
musicPlayer.getCurrentState().clickPlay();
musicPlayer.getCurrentState().clickLock();
}
}
无效操作,因为播放器已锁定 >_<
已解锁播放器!
无效操作,因为没有播放记录 >_<
无效操作,因为没有播放记录 >_<
播放音乐~
正在播放上一首音乐...
正在播放下一首音乐...
暂停音乐~
已锁定播放器!
☘️ 使用场景
◾️如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
模式建议你将所有特定于状态的代码抽取到一组独立的类中。这样一来,你可以在独立于其他状态的情况下添加新状态或修改已有状态,从而减少维护成本。
◾️如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
状态模式会将这些条件语句的分支抽取到相应状态类的方法中。同时,你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。
◾️当相似状态和基于条件的状态机转换中存在许多重复代码时,可使用状态模式。
状态模式让你能够生成状态类层次结构,通过将公用代码抽取到抽象基类中来减少重复。
🧊 实现方式
(1)确定哪些类是上下文。它可能是包含依赖于状态的代码的已有类;如果特定于状态的代码分散在多个类中,那么它可能是一个新的类。
(2)声明状态接口。虽然你可能会需要完全复制上下文中声明的所有方法,但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
(3)为每个实际状态创建一个继承于状态接口的类。然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。在将代码移动到状态类的过程中,你可能会发现它依赖于上下文中的一些私有成员。你可以采用以下几种变通方式:
- 将这些成员变量或方法设为公有。
- 将需要抽取的上下文行为更改为上下文中的公有方法,然后在状态类中调用。这种方式简陋却便捷,你可以稍后再对其进行修补。
- 将状态类嵌套在上下文类中。这种方式需要你所使用的编程语言支持嵌套类。
(4)在上下文类中添加一个状态接口类型的引用成员变量,以及一个用于修改该成员变量值的公有设置器。
(5)再次检查上下文中的方法,将空的条件语句替换为相应的状态对象方法。
(6)为切换上下文状态,你需要创建某个状态类实例并将其传递给上下文。你可以在上下文、各种状态或客户端中完成这项工作。无论在何处完成这项工作,该类都将依赖于其所实例化的具体类。
🎲 优缺点
➕ 通过消除臃肿的状态机条件语句简化上下文代码。
➕ 单一职责原则。将与特定状态相关的代码放在单独的类中。
➕ 开闭原则。无需修改已有状态类和上下文就能引入新状态。
➖ 状态模式的使用必然会增加系统类和对象的个数。
➖ 如果状态机只有很少的几个状态,或者很少发生改变,那么应用该模式可能会显得小题大作。
🌸 补充
如果你系统学习过《编译原理》这门课程,理解状态模式将轻而易举。
如果你没有学过那也无妨,把本文的“音乐播放器”自己实现出来,也可以完全理解状态模式!