1. 意圖
允許一個物件在其內部狀態改變時改變它的行為,物件看起來似乎修改了它的類
2. 動機
狀態模式與有限狀態機的概念緊密相關,其主要思想是程式在任意時刻僅可處于幾種有限的狀態中,在任何一個特定狀態中,程式的行為都不相同,且可瞬間從一個狀態切換到另一個狀態,不過,根據當前狀態,程式可能會切換到另外一種狀態,也可能會保持當前狀態不變,這些數量有限且預先定義的狀態切換規則被稱為轉移
假如你有一個 檔案Document類,檔案可能會處于草稿Draft 、 ?審閱中Moderation和 已發布Published三種狀態中的一種,檔案的 publish發布方法在不同狀態下的行為略有不同:
- 處于
草稿狀態時,它會將檔案轉移到審閱中狀態 - 處于
審閱中狀態時,如果當前用戶是管理員,會公開發布檔案 - 處于
已發布狀態時,它不會進行任何操作
狀態機通常由眾多條件運算子( if或 switch )實作,可根據物件的當前狀態選擇相應的行為,“狀態” 通常只是物件中的一組成員變數值,如下偽碼所示:
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 // ...
當我們逐步在檔案類中添加更多狀態和依賴于狀態的行為后,基于條件陳述句的狀態機就會暴露其最大的弱點,為了能根據當前狀態選擇完成相應行為的方法,絕大部分方法中會包含復雜的條件陳述句,修改其轉換邏輯可能會涉及到修改所有方法中的狀態條件陳述句,導致代碼的維護作業非常艱難,這個問題會隨著專案進行變得越發嚴重,我們很難在設計階段預測到所有可能的狀態和轉換,隨著時間推移,最初僅包含有限條件陳述句的簡潔狀態機可能會變的臃腫而難以維護,
狀態模式建議為物件的所有可能狀態新建一個類,然后將所有狀態的對應行為抽取到這些類中,原始物件被稱為背景關系(context),它并不會自行實作所有行為,而是會保存一個指向表示當前狀態的狀態物件的參考,且將所有與狀態相關的作業委派給該物件
如需將背景關系轉換為另外一種狀態,則需將當前活動的狀態物件替換為另外一個代表新狀態的物件,采用這種方式是有前提的:所有狀態類都必須遵循同樣的介面,而且背景關系必須僅通過介面與這些物件進行互動,這個結構可能看上去與策略模式相似,但有一個關鍵性的不同——在狀態模式中,特定狀態知道其他所有狀態的存在,且能觸發從一個狀態到另一個狀態的轉換;策略則幾乎完全不知道其他策略的存在
3. 適用性
- 一個物件的行為取決于它的狀態,并且它必須在運行時根據狀態改變它的行為
- 一個操作中含有龐大的多分支的條件陳述句,且這些分支依賴于該物件的狀態,這個狀態通常用一個或多個列舉常量表示,通常,有多個操作包含這一相同的條件結構,State模式將每一個條件分支放入一個獨立的類中,這樣可以根據物件自身的情況將物件的狀態作為一個物件,這一物件可以不依賴于其他物件而獨立變化
4. 結構
5. 效果
1. 通過消除臃腫的狀態機條件陳述句簡化背景關系代碼
2. 將與特定狀態相關的行為區域化,并且將不同狀態的行為分割開來(單一職責原則)
3. 無需修改已有狀態類和背景關系就能引入新狀態(開閉原則)
4. State物件可被共享 各Context物件可以共享一個State物件,當狀態以這種方式被共享時,他們必然是沒有內部狀態而只有行為的輕量級物件(Flyweight)
6. 代碼實作
本例中,狀態模式允許媒體播放器根據當前的回放狀態進行不同的控制行為,播放器主類包含一個指向狀態物件的參考,它將完成播放器的絕大部分作業,某些行為可能會用一個狀態物件替換另一個狀態物件,改變播放器對用戶互動的回應方式,
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(); } }
運行結果
7. 實作
實作State模式時的考慮:
1) 誰定義狀態轉換? State模式不指定哪個參與者定義狀態轉換準則,如果該準則是固定的,那么它們可在Context中完全實作,然后若讓State子類自身指定它們的后繼者狀態以及何時進行轉換,通常更加靈活,更合適,這需要Context增加一個介面,讓State物件顯式的設定Context的當前狀態,用這種方法分散轉換邏輯可以很容易地定義新的State子類來修改和擴展該邏輯,這樣做有一個缺點,一個State子類至少擁有一個其他子類的資訊,這就在各子類之間產生了依賴
2) 創建和銷毀State物件 何時創建State物件?以及何時銷毀他們? 是需要時再創建State物件,并隨后銷毀他們還是,提前創建它們并且始終不銷毀它們,當要進入的狀態是運行時不可知的,并且背景關系不經常改變時,用第一種較為合適,如果State物件存盤了大量資訊,當狀態改變很頻繁時,第二種方法較好
8. 與其他模式的關系
- 狀態可被視為策略的擴展,兩者都基于組合機制:它們都通過將部分作業委派給 “幫手” 物件來改變其在不同情景下的行為,策略使得這些物件相互之間完全獨立,它們不知道其他物件的存在,但狀態模式沒有限制具體狀態之間的依賴,且允許它們自行改變在不同情景下的狀態
9. 已知應用
使用示例:在Java語言中,狀態模式通常被用于將基于switch陳述句的大型狀態機轉換為物件
核心Java程式庫中一些狀態模式的示例:
javax.faces.lifecycle.LifeCycle#execute() (由 Faces-Servlet控制:行為依賴于當前 JSF 生命周期的階段 (狀態))
識別方法: 方法受外部控制且能根據物件狀態改變行為
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/308451.html
標籤:設計模式
上一篇:模板方法(學習筆記)
下一篇:觀察者模式(學習筆記)
