容許一個對象在其內部狀態改變時改變它的行爲。對象看起來彷佛修改了它的類java
狀態模式與有限狀態機的概念緊密相關。其主要思想是程序在任意時刻僅可處於幾種有限的狀態中。在任何一個特定狀態中,程序的行爲都不相同,且可瞬間從一個狀態切換到另外一個狀態。不過,根據當前狀態,程序可能會切換到另一種狀態,也可能會保持當前狀態不變。這些數量有限且預先定義的狀態切換規則被稱爲轉移 ide
假如你有一個 文檔
Document類。文檔可能會處於草稿
Draft 、 審閱中
Moderation和 已發佈
Published三種狀態中的一種。文檔的 publish
發佈方法在不一樣狀態下的行爲略有不一樣:fetch
草稿
狀態時,它會將文檔轉移到審閱中狀態審閱中
狀態時,若是當前用戶是管理員,會公開發布文檔已發佈
狀態時,它不會進行任何操做
狀態機一般由衆多條件運算符( if
或 switch
)實現,可根據對象的當前狀態選擇相應的行爲。「狀態」 一般只是對象中的一組成員變量值。以下僞碼所示:ui
class Document is field state: string // ... method publish() is switch (state) "draft": state = "moderation" break "moderation": if (currentUser.role == 'admin') state = "published" break "published": // 什麼也不作。 break // ...
當咱們逐步在文檔類中添加更多狀態和依賴於狀態的行爲後,基於條件語句的狀態機就會暴露其最大的弱點。爲了能根據當前狀態選擇完成相應行爲的方法,絕大部分方法中會包含複雜的條件語句。修改其轉換邏輯可能會涉及到修改全部方法中的狀態條件語句,致使代碼的維護工做很是艱難。這個問題會隨着項目進行變得愈加嚴重。咱們很難在設計階段預測到全部可能的狀態和轉換。隨着時間推移,最初僅包含有限條件語句的簡潔狀態機可能會變的臃腫而難以維護。 this
狀態模式建議爲對象的全部可能狀態新建一個類,而後將全部狀態的對應行爲抽取到這些類中。原始對象被稱爲上下文(context),它並不會自行實現全部行爲,而是會保存一個指向表示當前狀態的狀態對象的引用,且將全部與狀態相關的工做委派給該對象spa
如需將上下文轉換爲另一種狀態,則需將當前活動的狀態對象替換爲另一個表明新狀態的對象。採用這種方式是有前提的:全部狀態類都必須遵循一樣的接口,並且上下文必須僅經過接口與這些對象進行交互。這個結構可能看上去與策略模式類似,但有一個關鍵性的不一樣——在狀態模式中,特定狀態知道其餘全部狀態的存在,且能觸發從一個狀態到另外一個狀態的轉換;策略則幾乎徹底不知道其餘策略的存在設計
1. 經過消除臃腫的狀態機條件語句簡化上下文代碼code
2. 將與特定狀態相關的行爲局部化,而且將不一樣狀態的行爲分割開來(單一職責原則)對象
3. 無需修改已有狀態類和上下文就能引入新狀態(開閉原則)blog
4. State對象可被共享 各Context對象能夠共享一個State對象。當狀態以這種方式被共享時,他們必然是沒有內部狀態而只有行爲的輕量級對象(Flyweight)
本例中,狀態模式容許媒體播放器根據當前的回放狀態進行不一樣的控制行爲。播放器主類包含一個指向狀態對象的引用,它將完成播放器的絕大部分工做。某些行爲可能會用一個狀態對象替換另外一個狀態對象,改變播放器對用戶交互的迴應方式。
states/State.java: 通用狀態接口
package state.states; import state.ui.Player; /** * @author GaoMing * @date 2021/7/26 - 8:02 */ public abstract class State { Player player; /** * Context passes itself through the state constructor. This may help a * state to fetch some useful context data if needed. */ State(Player player) { this.player = player; } public abstract String onLock(); public abstract String onPlay(); public abstract String onNext(); public abstract String onPrevious(); }
states/LockedState.java
package state.states; import state.ui.Player; /** * @author GaoMing * @date 2021/7/26 - 8:04 * Concrete states provide the special implementation for all interface methods. */ 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..."; } }
states/ReadyState.java
package state.states; import state.ui.Player; /** * @author GaoMing * @date 2021/7/26 - 8:06 * They can also trigger state transitions in the context. */ 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..."; } }
states/PlayingState.java
package state.states; import state.ui.Player; /** * @author GaoMing * @date 2021/7/26 - 8:06 */ 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(); } }
ui/Player.java: 播放器的主要代碼
package state.ui; import state.states.ReadyState; import state.states.State; import java.util.ArrayList; import java.util.List; /** * @author GaoMing * @date 2021/7/26 - 8:03 */ 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 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/UI.java: 播放器的 GUI
package state.ui; import javax.swing.*; import java.awt.*; /** * @author GaoMing * @date 2021/7/26 - 8:07 */ public class UI { private Player player; private static JTextField textField = new JTextField(); 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); } }
Demo.java: 客戶端代碼
package state; import state.ui.Player; import state.ui.UI; /** * @author GaoMing * @date 2021/7/26 - 8:08 */ public class Demo { public static void main(String[] args) { Player player = new Player(); UI ui = new UI(player); ui.init(); } }
運行結果
實現State模式時的考慮:
1) 誰定義狀態轉換? State模式不指定哪一個參與者定義狀態轉換準則。若是該準則是固定的,那麼它們可在Context中徹底實現。而後若讓State子類自身指定它們的後繼者狀態以及什麼時候進行轉換,一般更加靈活。更合適。這須要Context增長一個接口,讓State對象顯式的設定Context的當前狀態。用這種方法分散轉換邏輯能夠很容易地定義新的State子類來修改和擴展該邏輯。這樣作有一個缺點,一個State子類至少擁有一個其餘子類的信息,這就在各子類之間產生了依賴
2) 建立和銷燬State對象 什麼時候建立State對象?以及什麼時候銷燬他們? 是須要時再建立State對象,並隨後銷燬他們仍是,提早建立它們而且始終不銷燬它們。當要進入的狀態是運行時不可知的,而且上下文不常常改變時,用第一種較爲合適。若是State對象存儲了大量信息,當狀態改變很頻繁時,第二種方法較好
使用示例:在Java語言中,狀態模式一般被用於將基於switch語句的大型狀態機轉換爲對象 核心Java程序庫中一些狀態模式的示例: javax.faces.lifecycle.LifeCycle#execute() (由 FacesServlet控制:行爲依賴於當前 JSF 生命週期的階段 (狀態)) 識別方法: 方法受外部控制且能根據對象狀態改變行爲