---
# System prepended metadata

title: 狀態模式 State
tags: [Behavioral Patterns]

---

---
tags: Behavioral Patterns
---

# 狀態模式 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=貴全]

