1. 程式人生 > 其它 >AtCoder Beginner Contest 222 D - Between Two Arrays

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介面。

類圖


新的設計想法如下:

  1. 首先,我們定義一個State介面,在這個介面內,糖果機的每個動作都有一個對應的方法
  2. 然後為機器的每個狀態實現狀態類,這些類將負責在對應的狀態下進行機器的行為
  3. 最後,我們要擺脫舊的條件程式碼,取而代之的方式是,將動作委託到狀態類

程式碼

定義一個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例項共享。