狀態模式是一種行為設計模式, 讓你能在一個對象的內部狀態變化時改變其行為, 使其看上去就像改變了自身所屬的類一樣。
狀態模式與有限狀態機的概念緊密相關。
其主要思想是程序在任意時刻僅可處於幾種有限的狀態中。 在任何一個特定狀態中, 程序的行為都不相同, 且可瞬間從一個狀態切換到另一個狀態。 不過, 根據當前狀態, 程序可能會切換到另外一種狀態, 也可能會保持當前狀態不變。 這些數量有限且預先定義的狀態切換規則被稱為轉移。
這里是核心 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();
}
}
適用場景
-
如果對象需要根據自身當前狀態進行不同行為, 同時狀態的數量非常多且與狀態相關的代碼會頻繁變更的話, 可使用狀態模式。
模式建議你將所有特定於狀態的代碼抽取到一組獨立的類中。 這樣一來, 你可以在獨立於其他狀態的情況下添加新狀態或修改已有狀態, 從而減少維護成本。
-
如果某個類需要根據成員變量的當前值改變自身行為, 從而需要使用大量的條件語句時,可使用該模式。
狀態模式會將這些條件語句的分支抽取到相應狀態類的方法中。 同時, 你還可以清除主要類中與特定狀態相關的臨時成員變量和幫手方法代碼。
-
當相似狀態和基於條件的狀態機轉換中存在許多重復代碼時, 可使用狀態模式。
狀態模式讓你能夠生成狀態類層次結構, 通過將公用代碼抽取到抽象基類中來減少重復。
實現方式
-
確定哪些類是上下文。 它可能是包含依賴於狀態的代碼的已有類; 如果特定於狀態的代碼分散在多個類中, 那么它可能是一個新的類。
-
聲明狀態接口。 雖然你可能會需要完全復制上下文中聲明的所有方法, 但最好是僅把關注點放在那些可能包含特定於狀態的行為的方法上。
-
為每個實際狀態創建一個繼承於狀態接口的類。 然后檢查上下文中的方法並將與特定狀態相關的所有代碼抽取到新建的類中。
在將代碼移動到狀態類的過程中, 你可能會發現它依賴於上下文中的一些私有成員。 你可以采用以下幾種變通方式:
- 將這些成員變量或方法設為公有。
- 將需要抽取的上下文行為更改為上下文中的公有方法, 然后在狀態類中調用。 這種方式簡陋卻便捷, 你可以稍后再對其進行修補。
- 將狀態類嵌套在上下文類中。 這種方式需要你所使用的編程語言支持嵌套類。
-
在上下文類中添加一個狀態接口類型的引用成員變量, 以及一個用於修改該成員變量值的公有設置器。
-
再次檢查上下文中的方法, 將空的條件語句替換為相應的狀態對象方法。
-
為切換上下文狀態, 你需要創建某個狀態類實例並將其傳遞給上下文。 你可以在上下文、各種狀態或客戶端中完成這項工作。 無論在何處完成這項工作, 該類都將依賴於其所實例化的具體類。
狀態模式優點
- 單一職責原則。 將與特定狀態相關的代碼放在單獨的類中。
- 開閉原則。 無需修改已有狀態類和上下文就能引入新狀態。
- 通過消除臃腫的狀態機條件語句簡化上下文代碼。
狀態模式缺點
- 如果狀態機只有很少的幾個狀態, 或者很少發生改變, 那么應用該模式可能會顯得小題大作。