AtCoder Beginner Contest 222 D - Between Two Arrays
策略模式是圍繞可以互換的演算法來建立成功業務的,然而,狀態走的是更崇高的路,它通過改變物件內部的狀態來幫助物件控制自己的行為。
定義狀態模式
先看看定義:狀態模式允許物件在內部狀態改變時改變它的行為,物件看起來好像修改了它的類
問題引入
自動糖果售賣機,糖果機的控制器需要的工作流程如下圖
狀態機101
我們如何從狀態圖得到真正的程式碼呢?下面是一個實現狀態機(state machine)的簡單介紹。
1.首先,找到所有的狀態:一共四個狀態
2.接下來,建立一個例項變數來持有目前的狀態,然後定義每個狀態的值:
//每個狀態用不同的值表示 final static int SOLD_OUT=0;//售罄 final static int NO_QUARTER=1;//沒有投幣 final static int HAS_QUARTER=2;//已投幣 final static int SOLD=3;//售出糖果 //例項變數持有當前狀態,只要改變變數值狀態也會隨之改變 int state =SOLD_OUT;
3.現在,我們將所有系統中可以發生的動作整合起來:
4.現在我們建立一個類,它的作用就像是一個狀態機,每一個動作,我們都建立了一個對應的方法,這些方法利用條件語句來決定在每個狀態內什麼行為是恰當的。比如對“投入25分錢”這個動作來說,我們可以把對應方法寫成下面的樣子:
public void insertQuarter() { // 每一個可能的狀態都需要用條件語句檢查...... if (state == HAS_QUARTER) { // 然後對每一個可能的狀態展現適當的行為...... System.out.println("You can't insert another quarter"); } else if (state == SOLD_OUT) { System.out.println("You can't insert a quarter, the machine is sold out"); } else if (state == SOLD) { System.out.println("Please wait, we're already giving you a gumball"); } else if (state == NO_QUARTER) { // // 但是也可隊轉換到另一個狀態,像狀態圖中所描繪的那樣。 state = HAS_QUARTER; System.out.println("You inserted a quarter"); } }
技巧:如何對物件內的狀態建模——通過建立一個例項變數來持有狀態值,並在方法內書寫條件程式碼來處理不同狀態。
初步程式碼
class GumballMachine{ final static int SOLD_OUT=0; // 糖果售罄 final static int NO_QUARTER=1; // 沒有投入25分錢 final static int HAS_QUARTER=2; final static int SOLD=3; int state =SOLD_OUT; // 跟蹤當前狀態 int count =0; // 儲存糖果數量 public GumballMachine(int count){ this.count=count; if(count>0){ state=NO_QUARTER; } } //當有25分錢投入,就會執行這個方法 public void insertQuarter(){ if(state==HAS_QUARTER){ System.out.println("如果已投入過25分錢,我們就告訴顧客"); }else if(state==NO_QUARTER){ state=HAS_QUARTER; System.out.println("如果是在“沒有25分錢”的狀態下,我們就接收25分錢," +"並將狀態轉換到“有25分錢”的狀態"); }else if(state ==SOLD_OUT){ System.out.println("如果糖果已經售罄,我們就拒絕收錢"); }else if(state==SOLD){ System.out.println("如果顧客剛才買了糖果,就需要稍等一下,好讓狀態轉換完畢。" +"恢復到“沒有25分錢”的狀態"); state=NO_QUARTER; } } //如果顧客試著退回25分錢就執行這個方法 public void ejectQuarter(){ if(state==HAS_QUARTER){ System.out.println("如果有25分錢,我們就把錢退出來,回到“沒有25分錢”的狀態"); state=NO_QUARTER; }else if(state==NO_QUARTER){ System.out.println("如果沒有25分錢的話,當然不能退出25分錢"); }else if(state ==SOLD){ System.out.println("顧客已經轉動曲柄就不能再退錢了,他已經拿到糖果了"); }else if(state==SOLD_OUT){ System.out.println("如果糖果售罄,就不能接受25分錢,當然也不可能退錢"); } } //顧客試著轉動曲柄 public void turnCrank(){ if(state==SOLD){ System.out.println("別想騙過機器拿兩次糖果"); }else if(state==NO_QUARTER){ System.out.println("我們需要先投入25分錢"); }else if(state ==SOLD_OUT){ System.out.println("我們不能給糖果,已經沒有任何糖果了"); }else if(state==HAS_QUARTER){ System.out.println("成功,他們拿到糖果了," +"改變狀態到“售出糖果”然後呼叫機器的disoense()方法"); state=SOLD; dispense(); } } //呼叫此方法,發放糖果 public void dispense(){ if(state==SOLD){ System.out.println("我們正在“出售糖果”狀態,給他們糖果"); count=count-1; // 在這裡處理“糖果售罄”的情況, // 如果這是最後一個糖果,將機器的狀態設定到“糖果售罄”否則就回到“沒有25分錢”的狀態 if(count==0){ System.out.println(); state=SOLD_OUT; }else{ state=NO_QUARTER; } }else if(state==SOLD_OUT){ System.out.println("這些都不應該發生,但是如果做了,就得到錯誤提示"); }else if(state ==HAS_QUARTER){ System.out.println("這些都不應該發生,但是如果做了,就得到錯誤提示"); }else if(state==NO_QUARTER){ System.out.println("這些都不應該發生,但是如果做了,就得到錯誤提示"); } } }
新的設計
儘管程式完美執行,但還是躲不掉需求變更的命運
現在糖果公司要求:當曲柄被轉動時,有10%的機率掉下來的是兩個糖果。(氪金扭蛋)
再回看一下我們的初步程式碼,想要實現新的需求將會變得非常麻煩:
- 必須新增一箇中獎的“贏家”狀態。
- 必須在每一個方法新增新的判斷條件來處理“贏家”狀態。
- 轉動把手的方法中還需要檢查目前狀態是否是“贏家”再決定切換到“贏家”狀態行為還是正常出售行為。
在現有程式碼基礎上做增加將會很麻煩,也不利與以後的維護,擴充套件性差。
回顧一下第一章的策略模式中的設計原則:
找出應用中可能需要變化之處,把他們獨立出來
將狀態獨立出來,封裝成一個類,都實現State介面。
類圖
新的設計想法如下:
- 首先,我們定義一個State介面,在這個介面內,糖果機的每個動作都有一個對應的方法
- 然後為機器的每個狀態實現狀態類,這些類將負責在對應的狀態下進行機器的行為
- 最後,我們要擺脫舊的條件程式碼,取而代之的方式是,將動作委託到狀態類
程式碼
定義一個State介面
public interface State { // 沒有共同的功能可以放進抽象類中,就會使用介面。
public void insertQuarter();//投幣
public void ejectQuarter();//退幣
public void turnCrank();//轉動出貨把手
public void dispense();//出售
}
為機器的每個狀態實現狀態類:
//未投幣狀態
public class NoQuarterState implements State {
GumballMachine gumballMachine;
public NoQuarterState(GumballMachine gumballMachine) {
this.gumballMachine=gumballMachine;
}
public void insertQuarter() {
System.out.println("你投入一枚硬幣");
gumballMachine.setState(gumballMachine.getHasQuarterState());//狀態轉換為已投幣狀態
}
public void ejectQuarter() {
System.out.println("你未投幣,無法退錢");
}
public void turnCrank() {
System.out.println("未投幣,請先投幣");
}
public void dispense() {
System.out.println("請先投幣");
}
}
//已投幣狀態
public class HasQuarterState implements State {
// 首先我們增加一個隨機數產生器,產生10%贏的機會
Random randomWinner=new Random(System.currentTimeMillis());
GumballMachine gumballMachine;
public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine=gumballMachine;
}
public void insertQuarter() {
System.out.println("已投幣,無法再接收投幣");
}
public void ejectQuarter() {
System.out.println("已退幣");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}
public void turnCrank() {
System.out.println("已轉動把手,糖果出售中。。。。");
int winner=randomWinner.nextInt(10);//隨機數生成,用以標記“贏家-10%”狀態
if((winner==0)&&(gumballMachine.getCount()>1))
gumballMachine.setState(gumballMachine.getWinnerState());
else
gumballMachine.setState(gumballMachine.getSoldState());
}
public void dispense() {
System.out.println("機器中已經沒有糖果可以出售了!");
}
}
//出售狀態
public class SoldState implements State {
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine=gumballMachine;
}
public void insertQuarter() {
System.out.println("請等候,正在初始化機器中");
}
public void ejectQuarter() {
System.out.println("抱歉,您已轉動把手獲得了糖果,無法退幣");
}
public void turnCrank() {
System.out.println("您重複轉動把手,無法再獲取更多糖果");
}
public void dispense() {
gumballMachine.releaseBall();//出貨,糖果-1
if(gumballMachine.getCount()>0)
gumballMachine.setState(gumballMachine.getNoQuarterState());
else {
System.out.println("糖果已售完");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
//售罄狀態
public class SoldOutState implements State {
GumballMachine gumballMachine;
public SoldOutState(GumballMachine gumballMachine) {
this.gumballMachine=gumballMachine;
}
public void insertQuarter() {
System.out.println("此機器的糖果已售完,不接收投幣");
}
public void ejectQuarter() {
System.out.println("未投幣,退幣失敗");
}
public void turnCrank() {
System.out.println("糖果已售完,轉動把手也不會有糖果出來的");
}
public void dispense() {
System.out.println("機器中已無糖果");
}
}
//贏家狀態
public class WinnerState implements State {
GumballMachine gumballMachine;
public WinnerState(GumballMachine gumballMachine) {
this.gumballMachine=gumballMachine;
}
public void insertQuarter() {
System.out.println("請等候,正在初始化機器中");
}
public void ejectQuarter() {
System.out.println("抱歉,您已轉動把手獲得了糖果");
}
public void turnCrank() {
System.out.println("您重複轉動把手,無法再獲取更多糖果");
}
public void dispense() {
System.out.println("恭喜你成為幸運兒,你將額外獲得一個免費糖果");
gumballMachine.releaseBall();//出貨,糖果-1
if(gumballMachine.getCount()==0)
gumballMachine.setState(gumballMachine.getSoldOutState());
else {
gumballMachine.releaseBall();
if(gumballMachine.getCount()>0)
gumballMachine.setState(gumballMachine.getNoQuarterState());
else {
System.out.println("糖果已售完");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
}
糖果機類:
public class GumballMachine {
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State winnerState;
State state=soldOutState;
int count=0;
public GumballMachine(int numberGumballs) {//初始化
soldOutState=new SoldOutState(this);
noQuarterState=new NoQuarterState(this);
hasQuarterState=new HasQuarterState(this);
soldState=new SoldState(this);
winnerState=new WinnerState(this);
this.count=numberGumballs;
if(numberGumballs>0)
state=noQuarterState;//先判斷條件再改變狀態
}
//將動作委託到狀態類
public void insterQuarter() {
state.insertQuarter();
}
public void ejectQuarter() {
state.ejectQuarter();
}
public void turnCrank() {
state.turnCrank();
state.dispense();
//請注意,我們不需要在GumballMachine中準備一個dispense()的動作方法,
//因為這只是一個內部的動作;使用者不可以直接要求機器發放糖果。但我們是在狀態物件的
//tuznCtank()方法中呼叫dispense()方法的。
}
//獲取當前狀態
public State getHasQuarterState() {
return hasQuarterState;
}
//改變狀態
public void setState(State state) {
this.state=state;
}
public void releaseBall() {
System.out.println("糖果從出口售出");
if(count!=0)
count-=1;
}
public State getSoldOutState() {
return soldOutState;
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getSoldState() {
return soldState;
}
//獲取糖果機中糖果數量
public int getCount() {
return count;
}
public State getWinnerState() {
return winnerState;
}
public String toString() {
// TODO 自動生成的方法存根
String s="剩餘糖果:"+count;
return s;
}
}
該進之處
- 將每個狀態的行為區域性化到它自己的類中。將容易產生問題的if語句刪除,以方便日後的維護。
- 讓每一個狀態“對修改關閉”,讓糖果機“對擴充套件開放”,因為可以加入新的狀態類(我們馬上就這麼做)。
- 建立一個新的程式碼基和類結構,這更能對映萬能糖果公司的圖,而且更容易閱讀和理解。
狀態模式的類圖
狀態模式的類圖其實和策略模式完全一樣!
狀態模式與策略模式
這兩個模式的差別在於它們的“意圖”
- 以狀態模式而言,我們將一群行為封裝在狀態物件中,context的行為隨時可委託到那些狀態物件中的一個,隨著時間而流逝,當前狀態在狀態物件集合中游走改變,以反映出context內部的狀態,因此,context的行為也會跟著改變,但是context的客戶對於狀態物件瞭解不多,甚至根本是渾然不覺。
- 以策略模式而言,客戶通常主動指定Context所要組合的策略物件時哪一個。現在,固然策略模式讓我們具有彈性,能夠在執行時改變策略,但對於某個context物件來說,通常都只有一個最適當的策略物件。
- 一般的,我們把策略模式想成是除了繼承之外的一種彈性替代方案,如果你使用繼承定義了一個類的行為,你將被這個行為困住,是指要修改它都很難,有了策略模式,你可以通過組合不同的物件來改變行為。
- 我們把狀態模式想成是不用在context中放置許多條件判斷的替代方案,通過將行為包裝進狀態物件中,你可以通過在context內簡單地改變狀態物件來改變context的行為。
模式區分
- 狀態模式:封裝基於狀態的行為,並將行為委託到當前狀態
- 策略模式:將可以互換的行為封裝起來。然後使用委託的方法,覺得使用哪一個行為
- 模板方法模式:由子類決定如何實現演算法中的某些步驟
要點
(1)狀態模式允許一個物件基於內部狀態而擁有不同的行為。
(2)和程式狀態機(PSM)不同,狀態模式用類來表示狀態。
(3)Context會將行為委託給當前狀態物件。
(4)通過將每一個狀態封裝進一個類,我們把以後需要做的任何改變區域性化了。
(5)狀態模式和策略模式有相同的類圖,但是他們的意圖不同。
(6)策略模式通常會用行為或演算法配置Context類。
(7)狀態模式允許Context隨著狀態的改變而改變行為。
(8)狀態轉換可以有State類或Context類控制。
(9)使用狀態模式通常會導致設計中類的數目大量增加。
(10)狀態列可以被多個Context例項共享。