《Head First 設計模式》筆記9
阿新 • • 發佈:2018-12-31
狀態模式(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());
}
}
// ...