1. 程式人生 > >《Head First 設計模式》筆記9

《Head First 設計模式》筆記9

狀態模式(State)

允許物件在內部狀態改變時改變它的行為,物件看起來好像修改了它的類。

栗子

現在有一臺糖果機,它的狀態(挺複雜的):
- 沒有 25 分錢 -> 投入 25 分錢 -> 有 25 分錢
- 有 25 分錢 -> 轉動曲柄 -> 售出糖果(數量不為0) | 糖果售罄(數量為0)
- 有 25 分錢 -> 退錢按鈕 -> 退出 25 分錢
- 售出糖果 -> 沒有 25 分錢

從上面的狀態實現程式碼的步驟:
1. 找出所有狀態 -> 共四種:沒有 25 分錢、有 25 分錢、售出糖果、糖果售罄。
2. 建立一個持有當前狀態的例項變數 state。
3. 寫出所有可能發生的動作判斷。

class GumballMachine {
    private final static int SOLD_OUT = 0;
    private final static int NO_QUARTER = 1;
    private final static int HAS_QUARTER = 2;
    private final static int SOLD = 3;

    private int state = SOLD_OUT; // 當前狀態
    private int count = 0; // 糖果數目

    public GumballMachine(int
count) { this.count = count; // 初始糖果機的糖果數 if (count > 0) { // 大於0表示等待別人投幣 state = NO_QUARTER; } } // 投進25分錢 public void insertQuarter() { if (state == HAS_QUARTER) { System.out.println("你已經投過25分錢了,請不要重複投幣"); } else
if (state == NO_QUARTER) { System.out.println("投進25分錢"); state = HAS_QUARTER; } else if (state == SOLD_OUT) { System.out.println("沒有糖果了,不要投幣"); } else if (state == SOLD) { System.out.println("請稍等,正在出糖果"); } } // 客戶嘗試退錢 public void ejectQuarter() { if (state == HAS_QUARTER) { System.out.println("已退還25分錢"); state = NO_QUARTER; } else if (state == NO_QUARTER) { System.out.println("你還沒投幣呢"); } else if (state == SOLD_OUT) { System.out.println("沒有糖果時無法投幣,不要騙錢"); } else if (state == SOLD) { System.out.println("你已經轉動曲柄了,無法退錢"); } } // 客戶轉動曲柄 public void turnCrank() { if (state == HAS_QUARTER) { System.out.println("你轉動了曲柄"); state = SOLD; dispense(); } else if (state == NO_QUARTER) { System.out.println("你還沒投幣呢"); } else if (state == SOLD_OUT) { System.out.println("已經沒有糖果了"); } else if (state == SOLD) { System.out.println("轉幾次都只能拿一次"); } } // 糖果機發糖果 public void dispense() { if (state == SOLD) { System.out.println("糖果已發出"); count--; if (count == 0) { System.out.println("已經沒有糖果嘍"); state = SOLD_OUT; } else { state = NO_QUARTER; } } else if (state == NO_QUARTER) { System.out.println("你還沒投幣呢"); } else if (state == SOLD_OUT) { System.out.println("沒有糖果發"); } else if (state == SOLD) { System.out.println("沒有糖果發"); } } }

多麼縝密的判斷,基本上沒什麼漏洞了。

新需求

現在產品經理提需求來了:當曲柄被轉動時,有 10% 的機率掉出兩個糖果。

看回上面縝密的程式碼,是不是有點無從入手?這個需求真是要了命了……如果需求再有變更,上面的程式碼基本上就得推倒重來了,因為程式碼邏輯判斷太複雜。

滿足需求

我們可以把這四個狀態當作一個實體,比如糖果機當前處在沒投幣的狀態,那麼在該狀態下,可以通過投幣動作使糖果機轉移到投了幣狀態,其他狀態也是這樣,基本上可以把 if else 這些判斷語句分離出來。

定義狀態介面:

interface State {
    void insertQuarter();

    void ejectQuarter();

    void turnCrank();

    void dispense();
}

然後每種狀態都實現該介面(先看總體):

class SoldState implements State

class SoldOutState implements State

class NoQuarterState implements State

class HasQuarterState implements State

// 新需求中的“中獎狀態”
class WinnerState implements State

還有新的糖果機(還沒新增中獎狀態):

class GumballMachine {
    private State soldState;
    private State soldOutState;
    private State noQuarterState;
    private State hasQuarterState;

    private State state = soldState;
    private int count = 0;

    public GumballMachine(int count) {
        soldState = new SoldState(this);
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        this.count = count;
        if (count > 0) {
            state = noQuarterState;
        }
    }

    public void insertQuarter() {
        state.insertQuarter();
    }

    public void ejectQuarter() {
        state.ejectQuarter();
    }

    // 轉動曲柄後
    // 具體是否能發出糖果就需要當前狀態是 hasQuarterState
    public void turnCrank() {
        state.turnCrank();
        state.dispense();
    }

    public void setState(State state) {
        this.state = state;
    }

    // 不再通過 dispense 發放糖果
    public void releaseBall() {
        System.out.println("糖果已發出");
        if (count != 0) {
            count--;
        }
    }

    // getter
    public State getState() {
        return state;
    }

    public int getCount() {
        return count;
    }

    public State getSoldState() {
        return soldState;
    }

    public State getSoldOutState() {
        return soldOutState;
    }

    public State getNoQuarterState() {
        return noQuarterState;
    }

    public State getHasQuarterState() {
        return hasQuarterState;
    }
}

分別實現的狀態類

沒投幣的狀態:

class NoQuarterState implements State {
    private GumballMachine gumballMachine;

    public NoQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("投進25分錢");
        gumballMachine.setState(gumballMachine.getHasQuarterState());
    }

    @Override
    public void ejectQuarter() {
        System.out.println("你還沒投幣呢,怎麼退錢");
    }

    @Override
    public void turnCrank() {
        System.out.println("你轉動了曲柄,但你還沒投幣呢");
    }

    @Override
    public void dispense() {
        System.out.println("你要先投幣");
    }
}

投了幣的狀態:

class HasQuarterState implements State {
    private GumballMachine gumballMachine;

    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("你已經投過25分錢了,請不要重複投幣");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("已退還25分錢");
        gumballMachine.setState(gumballMachine.getNoQuarterState());
    }

    @Override
    public void turnCrank() {
        System.out.println("你轉動了曲柄");
        gumballMachine.setState(gumballMachine.getSoldState());
    }

    @Override
    public void dispense() {
        System.out.println("沒有糖果發");
    }
}

售出糖果的狀態:

class SoldState implements State {
    private GumballMachine gumballMachine;

    public SoldState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("請稍等,正在出糖果");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("你已經轉動曲柄了,無法退錢");
    }

    @Override
    public void turnCrank() {
        System.out.println("轉幾次都只能拿一次");
    }

    @Override
    public void dispense() {
        gumballMachine.releaseBall();
        if (gumballMachine.getCount() > 0) {
            gumballMachine.setState(gumballMachine.getNoQuarterState());
        } else {
            System.out.println("已經沒有糖果嘍");
            gumballMachine.setState(gumballMachine.getSoldOutState());
        }
    }
}

糖果售罄的狀態:

class SoldOutState implements State {
    private GumballMachine gumballMachine;

    public SoldOutState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("沒有糖果了,不要投幣");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("沒有糖果時無法投幣,不要騙錢");
    }

    @Override
    public void turnCrank() {
        System.out.println("你轉動了曲柄,但是已經沒有糖果了");
    }

    @Override
    public void dispense() {
        System.out.println("沒有糖果發");
    }
}

新需求中的中獎狀態:

class WinnerState implements State {
    private GumballMachine gumballMachine;

    public WinnerState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }

    @Override
    public void insertQuarter() {
        System.out.println("請稍等,正在出糖果");
    }

    @Override
    public void ejectQuarter() {
        System.out.println("你已經轉動曲柄了,無法退錢");
    }

    @Override
    public void turnCrank() {
        System.out.println("轉幾次都只能拿一次");
    }

    // 除了發糖外,其他資訊和售出糖果的狀態一樣
    @Override
    public void dispense() {
        System.out.println("恭喜!你拿到了兩顆糖");
        // 第一顆糖
        gumballMachine.releaseBall();
        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());
            }
        }
    }
}

需求還沒解決完

上面的程式碼只是解決了狀態的問題,但還沒有完全滿足需求,10% 的隨機中獎還沒寫。(其實也沒什麼難度了)

給糖果機加上中獎狀態:

class GumballMachine {
    // ...
    private State winnerState;

    public GumballMachine(String location, int count) {
        winnerState = new WinnerState(this);
        // ...
    }

    public State getWinnerState() {
        return winnerState;
    }

    // ...
}

因為只有在投了幣的情況下才能轉動曲柄 -> 發糖 | 中獎,所以要在投了幣的狀態加入這 10% 隨機:

class HasQuarterState implements State {
    // ...

    @Override
    public void turnCrank() {
        System.out.println("你轉動了曲柄");
        Random randomWinner = new Random(System.currentTimeMillis());
        // 中獎
        // 10次裡面有一次是0,即 1/10
        int winner = randomWinner.nextInt(10);
        if (winner == 0 && gumballMachine.getCount() > 1) {
            gumballMachine.setState(gumballMachine.getWinnerState());
        } else {
            gumballMachine.setState(gumballMachine.getSoldState());
        }
    }

    // ...