# 狀態模式 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 參與者的部份。
## 架構

* 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}]"}