# 狀態模式 State 當一個物件的內在狀態改變時允許改變其行為,這個物件看起來像是改變了其類別。 >[name=貴全] 先開出State的功能,並實作相關的功能,把Context整個傳給State。 更改State的狀態應該寫在ConcreteState中,Context應該有一個可以去呼叫更改的method。 ## 思考 State跟Visitor雖然同樣都是把自己丟給其他class去實作功能。 State跟Visitor的差別,在於Visitor只有一個accept,State要在指定method下再去把自己當作參數傳入State的method。 ## 角色 **State 參與者** State 表示狀態。規定不同狀態下做不同處理的介面。這個介面等於是一個不同狀態所做處理的所有方法的集合。例如 State 介面。 **ConcreteState 參與者** ConcreteState 是表示具體的不同狀態,具體實作在 State 所規定的介面。例如 DayState 以及 NightState 類別,白天和晚上會有不同狀態。 **Context 參與者** Context 具有表示現在狀態的 ConcreteState,而且還規定 State Pattern 的利用者所需要的介面。例如 Context 介面以及 SafeFrame 類別。 Context 介面負責規定介面的部份,SafeFrame 類別則負責具有 ConcreteState 參與者的部份。 ## 架構 ![](https://i.imgur.com/LDGAm9k.png) * Context將與狀態有關的訊息委託給目前持有的ConcreteState去處理。 * Context可能把自己當作傳參傳給State。 * Client可用State物件來設定Context的組態,設好之後便不再直接插手State物件。 * 可令Context或ConcreteState的子類別決定在什麼情況下該切換成什麼狀態 ## 範例 假設現在有一個會隨著時間改變警備狀態的金庫保全系統: * 有一個金庫 * 金庫有跟保全中心連線 * 金庫有警鈴和一般通話用的電話 * 金庫有時鐘,監視目前的時間 * 白天是9:00-16:59,晚間為17:00-23:59以及0:00-8:59 * 只有白天才能使用金庫 * 在白天使用金庫時,保全中心會保留使用紀錄 * 若晚間使用金庫時,保全中心會接到發生異常現象的通知 * 警鈴是24小時都可以使用 * 一旦使用警鈴,保全中心會接收到警鈴通知 * 一般通話用的電話是24小時都可以使用(但晚間只有答錄機服務) * 在白天使用電話時,就會呼叫保全中心 * 若晚間使用電話時,則會呼叫保全中心的答錄機 ```java= public interface Context { public abstract void setClock(int hour); // 設定時間 public abstract void changeState(State state); // 狀態變化 public abstract void callSecurityCenter(String msg); // 呼叫保全中心 public abstract void recordLog(String msg); // 保全中心保留記錄 } public interface State { public abstract void doClock(Context context, int hour); // 設定時間 public abstract void doUse(Context context); // 使用金庫 public abstract void doAlarm(Context context); // 警鈴 public abstract void doPhone(Context context); // 一般通話 } //表示白天狀態的類別 public class DayState implements State { private static DayState singleton = new DayState(); private DayState() { // 建構子為private } public static State getInstance() { // 取得唯一的個體 return singleton; } public void doClock(Context context, int hour) { // 設定時間 if (hour < 9 || 17 <= hour) { context.changeState(NightState.getInstance()); } } public void doUse(Context context) { // 使用金庫 context.recordLog("使用金庫(白天)"); } public void doAlarm(Context context) { // 警鈴 context.callSecurityCenter("警鈴(白天)"); } public void doPhone(Context context) { // 一般通話 context.callSecurityCenter("一般的通話(白天)"); } public String toString() { // 輸出字串 return "[白天]"; } } public class NightState implements State { private static NightState singleton = new NightState(); private NightState() { // 建構子為private } public static State getInstance() { // 取得唯一的個體 return singleton; } public void doClock(Context context, int hour) { // 設定時間 if (9 <= hour && hour < 17) { context.changeState(DayState.getInstance()); } } public void doUse(Context context) { // 使用金庫 context.callSecurityCenter("異常:晚間使用金庫!"); } public void doAlarm(Context context) { // 警鈴 context.callSecurityCenter("警鈴(晚間)"); } public void doPhone(Context context) { // 一般通話 context.recordLog("晚間的通話錄音"); } public String toString() { // 輸出字串 return "[晚間]"; } } public class SafeFrame extends Frame implements ActionListener, Context { private TextField textClock = new TextField(60); // 輸出現在時間 private TextArea textScreen = new TextArea(10, 60); // 輸出保全中心 private Button buttonUse = new Button("使用金庫"); // 使用金庫的按鍵 private Button buttonAlarm = new Button("警鈴"); // 警鈴的按鍵 private Button buttonPhone = new Button("一般通話"); // 一般通話的按鍵 private Button buttonExit = new Button("結束"); // 結束的按鍵 private State state = DayState.getInstance(); // 現在狀態 // 建構子 public SafeFrame(String title) { super(title); setBackground(Color.lightGray); setLayout(new BorderLayout()); // 輸出textClock add(textClock, BorderLayout.NORTH); textClock.setEditable(false); // 輸出textScreen add(textScreen, BorderLayout.CENTER); textScreen.setEditable(false); // 把按鍵放到面板上 Panel panel = new Panel(); panel.add(buttonUse); panel.add(buttonAlarm); panel.add(buttonPhone); panel.add(buttonExit); // 輸出面板 add(panel, BorderLayout.SOUTH); // 輸出到畫面上 pack(); show(); // 設定聽命令者 buttonUse.addActionListener(this); buttonAlarm.addActionListener(this); buttonPhone.addActionListener(this); buttonExit.addActionListener(this); } // 若有人按下按鍵,則跳到這裡 public void actionPerformed(ActionEvent e) { System.out.println("" + e); //不檢查是黑夜還是白天 if (e.getSource() == buttonUse) { // 使用金庫的按鍵 state.doUse(this); } else if (e.getSource() == buttonAlarm) { // 警鈴的按鍵 state.doAlarm(this); } else if (e.getSource() == buttonPhone) { // 一般通話的按鍵 state.doPhone(this); } else if (e.getSource() == buttonExit) { // 結束的按鍵 System.exit(0); } else { System.out.println("?"); } } // 設定時間 public void setClock(int hour) { String clockstring = "現在時間是"; if (hour < 10) { clockstring += "0" + hour + ":00"; } else { clockstring += hour + ":00"; } System.out.println(clockstring); textClock.setText(clockstring); state.doClock(this, hour); } // 狀態變化 public void changeState(State state) { System.out.println("狀態已經從" + this.state + "變成" + state + "。"); this.state = state; } // 呼叫保全中心 public void callSecurityCenter(String msg) { textScreen.append("call! " + msg + "\n"); } // 保全中心保留記錄 public void recordLog(String msg) { textScreen.append("record ... " + msg + "\n"); } } public class Main extends Thread { public static void main(String[] args) { SafeFrame frame = new SafeFrame("State Sample"); while (true) { for (int hour = 0; hour < 24; hour++) { frame.setClock(hour); // 設定時間,每秒代表一個小時 try { Thread.sleep(1000); } catch (InterruptedException e) { } } } } } ``` ## 如何寫 除了以上例子,還可以用在特定時間的商品折扣上,例如在某時段有可以買一送一的情形,如果有用這個pattern,那只要更換state,就可以在不更改class程式碼的情況下繼續使用,這兩個state分別是原價跟買一送一,那這麼簡單的範例為甚麼不直接去更改class的程式,因為這樣會違反OCP原則,那如果再增加打7折或是9折的需求,那這樣後期可能會很難維護,如果是使用這個state pattern,每一個實作出來的class也可以專注完成當前的任務。 ## State與Strategy 思考 State 是有一個class,可以更換途中更換concreteState。 Strategy 會有class的interface(簡單的也許沒有,所以就變成跟上面的一樣了),然後在設定實作。 這兩個都有透過設定實作的class去完成擴充的意思。 但最大的差別應該是在於 Strategy再設定的時候已經有最佳解了(像是鴨子不會突然就會飛),而State可以依照時間或是使用者的操作去更換concreteState。 >[name=閔致] 另外,**Strategy通常不會知道其他Strategy**,也不會有方式可以reference context來修改context。 **但是State pattern通常知道其他state的存在**,也能夠通知context去修改 context的內容以及改變context的state。 >[name=貴全]
{"metaMigratedAt":"2023-06-16T11:21:18.728Z","metaMigratedFrom":"YAML","title":"狀態模式 State","breaks":true,"contributors":"[{\"id\":\"1880e957-c482-42c0-815b-4a0ded0c27a0\",\"add\":7358,\"del\":138},{\"id\":\"63f87db7-492b-4b20-85a8-f9c9ecb34124\",\"add\":211,\"del\":10},{\"id\":\"82eaa412-2ff3-4bfe-bbd7-5ff190dd3326\",\"add\":30,\"del\":0}]"}
Expand menu