State
状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。
状态模式与有限状态机的概念紧密相关。
其主要思想是程序在任意时刻仅可处于几种有限的状态中。 在任何一个特定状态中, 程序的行为都不相同, 且可瞬间从一个状态切换到另一个状态。 不过, 根据当前状态, 程序可能会切换到另外一种状态, 也可能会保持当前状态不变。 这些数量有限且预先定义的状态切换规则被称为转移。
这里是核心 Java 程序库中一些状态模式的示例:
- javax.faces.lifecycle.LifeCycle#execute() (由 FacesServlet控制: 行为依赖于当前 JSF 生命周期的阶段 (状态))
识别方法: 状态模式可通过受外部控制且能根据对象状态改变行为的方法来识别。
真实世界类比
智能手机的按键和开关会根据设备当前状态完成不同行为:
- 当手机处于解锁状态时, 按下按键将执行各种功能。
- 当手机处于锁定状态时, 按下任何按键都将解锁屏幕。
- 当手机电量不足时, 按下任何按键都将显示充电页面。
状态模式结构
样例
媒体播放器
在本例中, 状态模式允许媒体播放器根据当前的回放状态进行不同的控制行为。 播放器主类包含一个指向状态对象的引用, 它将完成播放器的绝大部分工作。 某些行为可能会用一个状态对象替换另一个状态对象, 改变播放器对用户交互的回应方式。
通用状态接口
package behavioral.state.states;
import behavioral.state.ui.Player;
public abstract class State {
Player player;
State(Player player){
this.player = player;
}
public abstract String onLock();
public abstract String onPlay();
public abstract String onNext();
public abstract String onPrevious();
}
LockedState、ReadyState、PlayingState
package behavioral.state.states;
import behavioral.state.ui.Player;
public class LockedState extends State {
LockedState(Player player) {
super(player);
player.setPlaying(false);
}
@Override
public String onLock() {
if (player.isPlaying()) {
player.changeState(new ReadyState(player));
return "Stop playing";
} else {
return "Locked...";
}
}
@Override
public String onPlay() {
player.changeState(new ReadyState(player));
return "Ready";
}
@Override
public String onNext() {
return "Locked...";
}
@Override
public String onPrevious() {
return "Locked...";
}
}
package behavioral.state.states;
import behavioral.state.ui.Player;
public class ReadyState extends State{
public ReadyState(Player player){
super(player);
}
@Override
public String onLock() {
player.changeState(new LockedState(player));
return "Locked...";
}
@Override
public String onPlay() {
String action = player.startPlayback();
player.changeState(new PlayingState(player));
return action;
}
@Override
public String onNext() {
return "Locked...";
}
@Override
public String onPrevious() {
return "Locked...";
}
}
package behavioral.state.states;
import behavioral.state.ui.Player;
public class PlayingState extends State {
PlayingState(Player player){
super(player);
}
@Override
public String onLock() {
player.changeState(new LockedState(player));
player.setCurrentTrackAfterStop();
return "Stop playing";
}
@Override
public String onPlay() {
player.changeState(new ReadyState(player));
return "Paused...";
}
@Override
public String onNext() {
return player.nextTrack();
}
@Override
public String onPrevious() {
return player.previousTrack();
}
}
Player : 播放器的主要代码
package behavioral.state.ui;
import behavioral.state.states.ReadyState;
import behavioral.state.states.State;
import java.util.ArrayList;
import java.util.List;
public class Player {
private State state;
private boolean playing = false;
private List<String> playList = new ArrayList<>();
private int currentTrack = 0;
public Player() {
this.state = new ReadyState(this);
setPlaying(true);
for (int i = 1; i <= 12; i++) {
playList.add("Track " + i);
}
}
public void changeState(State state) {
this.state = state;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
public void setPlaying(boolean playing) {
this.playing = playing;
}
public boolean isPlaying() {
return playing;
}
public String startPlayback() {
return "Playing " + playList.get(currentTrack);
}
public String nextTrack() {
currentTrack++;
if (currentTrack > playList.size() - 1) {
currentTrack = 0;
}
return "Playing " + playList.get(currentTrack);
}
public String previousTrack() {
currentTrack--;
if (currentTrack < 0) {
currentTrack = playList.size() - 1;
}
return "Playing " + playList.get(currentTrack);
}
public void setCurrentTrackAfterStop() {
this.currentTrack = 0;
}
}
UI
package behavioral.state.ui;
import javax.swing.*;
import java.awt.*;
public class UI {
private static JTextField textField = new JTextField();
private Player player;
public UI(Player player) {
this.player = player;
}
public void init() {
JFrame frame = new JFrame("Test player");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel context = new JPanel();
context.setLayout(new BoxLayout(context, BoxLayout.Y_AXIS));
frame.getContentPane().add(context);
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.CENTER));
context.add(textField);
context.add(buttons);
// Context delegates handling user's input to a state object. Naturally,
// the outcome will depend on what state is currently active, since all
// states can handle the input differently.
JButton play = new JButton("Play");
play.addActionListener(e -> textField.setText(player.getState().onPlay()));
JButton stop = new JButton("Stop");
stop.addActionListener(e -> textField.setText(player.getState().onLock()));
JButton next = new JButton("Next");
next.addActionListener(e -> textField.setText(player.getState().onNext()));
JButton prev = new JButton("Prev");
prev.addActionListener(e -> textField.setText(player.getState().onPrevious()));
frame.setVisible(true);
frame.setSize(300, 100);
buttons.add(play);
buttons.add(stop);
buttons.add(next);
buttons.add(prev);
}
}
测试
package behavioral.state;
import behavioral.state.ui.Player;
import behavioral.state.ui.UI;
public class Demo {
public static void main(String[] args) {
Player player = new Player();
UI ui = new UI(player);
ui.init();
}
}
适用场景
-
如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话, 可使用状态模式。
模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。
-
如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时,可使用该模式。
状态模式会将这些条件语句的分支抽取到相应状态类的方法中。 同时, 你还可以清除主要类中与特定状态相关的临时成员变量和帮手方法代码。
-
当相似状态和基于条件的状态机转换中存在许多重复代码时, 可使用状态模式。
状态模式让你能够生成状态类层次结构, 通过将公用代码抽取到抽象基类中来减少重复。
实现方式
-
确定哪些类是上下文。 它可能是包含依赖于状态的代码的已有类; 如果特定于状态的代码分散在多个类中, 那么它可能是一个新的类。
-
声明状态接口。 虽然你可能会需要完全复制上下文中声明的所有方法, 但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
-
为每个实际状态创建一个继承于状态接口的类。 然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
在将代码移动到状态类的过程中, 你可能会发现它依赖于上下文中的一些私有成员。 你可以采用以下几种变通方式:
- 将这些成员变量或方法设为公有。
- 将需要抽取的上下文行为更改为上下文中的公有方法, 然后在状态类中调用。 这种方式简陋却便捷, 你可以稍后再对其进行修补。
- 将状态类嵌套在上下文类中。 这种方式需要你所使用的编程语言支持嵌套类。
-
在上下文类中添加一个状态接口类型的引用成员变量, 以及一个用于修改该成员变量值的公有设置器。
-
再次检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。
-
为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文、各种状态或客户端中完成这项工作。 无论在何处完成这项工作, 该类都将依赖于其所实例化的具体类。
状态模式优点
- 单一职责原则。 将与特定状态相关的代码放在单独的类中。
- 开闭原则。 无需修改已有状态类和上下文就能引入新状态。
- 通过消除臃肿的状态机条件语句简化上下文代码。
状态模式缺点
- 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。