策略模式 VS 狀態模式
在行為類設計模式中,狀態模式和策略模式是親兄弟,兩者非常相似,我們先看看兩者的通用類圖,把兩者放在一起比較一下,如圖所示:
二者是不是很像,光看這個 UML 我們看不出什麼端倪來,接下來我們結合例子,來對比一下二者之間的區別。下面的例子是《Head First 設計模式》中的例子。
策略模式
策略模式定義了演算法族,分別封裝起來,讓他們之間可以互相替換,此模式讓演算法的變化獨立於使用演算法的客戶。
某公司開發了一個鴨子游戲,裡面會出現各種特點的鴨子:綠頭鴨,紅頭鴨,橡皮鴨……用程式如何實現這些鴨子? 看到這個場景問題,很容易想到,先定義一個所有鴨子的超類Duck,把公共方法和屬性封裝進去。例如,鴨子都會游泳,鴨子都有各自的外貌:
例項
public abstract class Duck { public void swim(){ System.out.println("All duck can swim!"); } public abstract void display(); public void fly(){ System.out.println("飛~~~"); } public void quack(){ System.out.println("呱呱呱~"); } }
但是很快,我們發現這個超類的定義有問題。不是所有鴨子都會飛,不是所有鴨子都是呱呱叫(假設橡皮鴨是吱吱叫)。只要繼承了這個超類,那所有的鴨子都會飛了,所有的鴨子都是呱呱叫了。
怎麼辦呢?
Solution 1
第一個想到的解決方法是:子類進行方法覆蓋。很簡單,不會飛的子類覆蓋fly方法,重寫不就行了?
但是,弊端很明顯,所有不會飛的子類豈不都是要覆蓋,假設50種鴨子都不會飛,那重寫的工作量和維護量得有多大?
Solution 2
好的,所以我們想到第二個方法:繼續設計子抽象類,例如會飛不會叫的抽象子類FlyNoQuackDuck,會叫不會非的抽象子類QuackNoFlyDuck,不會叫不會飛的抽象子類NoQuackNoFlyDuck,又會飛又會叫的抽象子類FlyAndQuackDuck……
寫著寫著我們發現這種方法也不行,太不靈活,而且改變的部分越多,這種抽象子類得定義的越多。
Solution 3
那什麼方法才好呢?我們思考,之所以出現上面的問題,是因為我們習慣性的總想用繼承來實現改變的部分,實際我們可以將改變的部分抽離出來,用組合來實現。
這裡用到了三個設計原則:
- 找出應用中可能需要變化之處,把他們獨立出來。
- 針對介面程式設計,而不是針對實現
- 多用組合,少用繼承
運用第一個設計原則,我們將改變的方法fly()和quack()獨立出來,封裝成兩個行為類介面。然後根據不同的需求,設計出實現介面的不同行為類。
運用第二個和第三個設計原則,我們在Duck類中組合兩個成員變數的介面,在子類中動態的賦值。
整體的"類圖"如下:
總結
這種解決方法很完美的解決了我們的問題,運用"策略模式"的思想,將變化的部分抽離出來,組合進類中,根據不同的子類,可以"set"不同的行為子類進行,實現動態改變行為。
程式碼實現
兩個行為介面類
例項
public interface FlyBehavior { public void fly(); } public interface QuackBehavior { public void quack(); }
實現飛行介面的不同行為類
例項
public class FlyNoWay implements FlyBehavior{ public void fly(){ System.out.println("我不能飛……"); } } public class FlyWithWings implements FlyBehavior{ public void fly(){ System.out.println("飛~~~"); } } public class FlyWithRocket implements FlyBehavior{ public void fly(){ System.out.println("帶上火箭筒,飛~~~"); } }
實現鴨叫的不同行為類
例項
public class Quack implements QuackBehavior{ public void quack(){ System.out.println("呱呱呱~"); } } public class Squeak implements QuackBehavior{ public void quack(){ System.out.println("吱吱吱~"); } } public class MuteQuack implements QuackBehavior{ public void quack(){ System.out.println("我不會叫……"); } }
組合了實現介面的超類
例項
public abstract class Duck { protected FlyBehavior flyBehavior; protected QuackBehavior quackBehavior; public void swim(){ System.out.println("All duck can swim!"); } public abstract void display(); /** * 動態改變飛行行為 */ public void setFlyBehavior(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; } /** * 動態改變鴨叫行為 */ public void setQuackBehavior(QuackBehavior quackBehavior) { this.quackBehavior = quackBehavior; } public void performFly(){ flyBehavior.fly(); } public void performQuack(){ quackBehavior.quack(); } }
不同的鴨子類
例項
/** * 綠頭鴨 */ public class MallarDuck extends Duck{ public MallarDuck() { //可飛 flyBehavior = new FlyWithWings(); //呱呱叫 quackBehavior = new Quack(); } @Override public void display() { System.out.println("看著像綠頭鴨"); } }
/** * 綠頭鴨 */ public class RedHeadDuck extends Duck{ public RedHeadDuck() { //可飛 flyBehavior = new FlyWithWings(); //呱呱叫 quackBehavior = new Quack(); } @Override public void display() { System.out.println("看著像紅頭鴨"); } }
/** * 橡皮鴨 */ public class RubberDuck extends Duck{ public RubberDuck() { //不會飛 flyBehavior = new FlyNoWay(); //吱吱叫 quackBehavior = new Squeak(); } @Override public void display() { System.out.println("看著像橡皮鴨"); } }
狀態模式
狀態模式允許物件在內部狀態改變時改變它的行為,物件看起來好像修改了它的類
狀態模式策略模式很相似,也是將類的"狀態"封裝了起來,在執行動作時進行自動的轉換,從而實現,類在不同狀態下的同一動作顯示出不同結果。它與策略模式的區別在於,這種轉換是"自動","無意識"的。
狀態模式的類圖如下
狀態模式的類圖與策略模式一模一樣,區別在於它們的意圖。策略模式會控制物件使用什麼策略,而狀態模式會自動改變狀態。看完下面的案例應該就清楚了。
現在有一個糖果機的需求擺在你面前,需要用Java實現。
我們分析一下,糖果機的功能可以分為下圖所示的四個動作和四個狀態:
在不同狀態下,同樣的動作結果不一樣。例如,在"投了25分錢"的狀態下"轉動曲柄",會售出糖果;而在"沒有25分錢"的狀態下"轉動曲柄"會提示請先投幣。
簡單思考後,我們寫出如下的糖果機實現程式碼
例項
public class NoPatternGumballMachine{ /* * 四個狀態 */ /**沒有硬幣狀態*/ private final static int NO_QUARTER = 0; /**投幣狀態*/ private final static int HAS_QUARTER = 1; /**出售糖果狀態*/ private final static int SOLD = 2; /**糖果售盡狀態*/ private final static int SOLD_OUT = 3; private int state = SOLD_OUT; private int candyCount = 0; public NoPatternGumballMachine(int count) { this.candyCount = count; if(candyCount > 0) state = NO_QUARTER; } /* * 四個動作 */ /** * 投幣 */ public void insertQuarter() { if(NO_QUARTER == state){ System.out.println("投幣"); state = HAS_QUARTER; } else if(HAS_QUARTER == state){ System.out.println("請不要重複投幣!"); returnQuarter(); } else if(SOLD == state){ System.out.println("已投幣,請等待糖果"); returnQuarter(); }else if(SOLD_OUT == state){ System.out.println("糖果已經售盡"); returnQuarter(); } } /** * 退幣 */ public void ejectQuarter() { if(NO_QUARTER == state){ System.out.println("沒有硬幣,無法彈出"); } else if(HAS_QUARTER == state){ returnQuarter(); state = NO_QUARTER; } else if(SOLD == state){ System.out.println("無法退幣,正在發放糖果,請等待"); }else if(SOLD_OUT == state){ System.out.println("沒有投幣,無法退幣"); } } /** * 轉動出糖曲軸 */ public void turnCrank() { if(NO_QUARTER == state){ System.out.println("請先投幣"); } else if(HAS_QUARTER == state){ System.out.println("轉動曲軸,準備發糖"); state = SOLD; } else if(SOLD == state){ System.out.println("已按過曲軸,請等待"); }else if(SOLD_OUT == state){ System.out.println("糖果已經售盡"); } } /** * 發糖 */ public void dispense() { if(NO_QUARTER == state){ System.out.println("沒有投幣,無法發放糖果"); } else if(HAS_QUARTER == state){ System.out.println("this method don't support"); } else if(SOLD == state){ if(candyCount > 0){ System.out.println("分發一顆糖果"); candyCount --; state = NO_QUARTER; } else{ System.out.println("抱歉,糖果已售盡"); state = SOLD_OUT; } }else if(SOLD_OUT == state){ System.out.println("抱歉,糖果已售盡"); } } /** * 退還硬幣 */ protected void returnQuarter() { System.out.println("退幣……"); } }
從程式碼裡面可以看出,糖果機根據此刻不同的狀態,而使對應的動作呈現不同的結果。這份程式碼已經可以滿足我們的基本需求,但稍微思考一下,你會覺得這種實現程式碼似乎,功能太複雜了,擴充套件性很差,沒有面向物件的風格。
假設由於新需求,要增加一種狀態,那每個動作方法我們都需要修改,都要重新增加一條else語句。而如果需求變更,某個狀態下的動作需要修改,我們也要同時改動四個方法。這樣的工作將是繁瑣而頭大的。
怎麼辦? 六大設計原則之一
找出應用中可能需要變化之處,把他們獨立出來。
在糖果機中,狀態就是一直在變化的部分,不同的狀態動作不一樣。我們完全可以將其抽離出來
新的設計想法如下:
首先,我們定義一個State介面,在這個介面內,糖果機的每個動作都有一個對應的方法
然後為機器的每個狀態實現狀態類,這些類將負責在對應的狀態下進行機器的行為
最後,我們要擺脫舊的條件程式碼,取而代之的方式是,將動作委託到狀態類
定義一個State介面
例項
public abstract class State { /** * 投幣 */ public abstract void insertQuarter(); /** * 退幣 */ public abstract void ejectQuarter(); /** * 轉動出糖曲軸 */ public abstract void turnCrank(); /** * 發糖 */ public abstract void dispense(); /** * 退還硬幣 */ protected void returnQuarter() { System.out.println("退幣……"); } }
為機器的每個狀態實現狀態類
例項
/*** 沒有硬幣的狀態
*/
public class NoQuarterState extends State{ GumballMachine gumballMachine; public NoQuarterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("你投入了一個硬幣"); //轉換為有硬幣狀態 gumballMachine.setState(gumballMachine.hasQuarterState); } @Override public void ejectQuarter() { System.out.println("沒有硬幣,無法彈出"); } @Override public void turnCrank() { System.out.println("請先投幣"); } @Override public void dispense() { System.out.println("沒有投幣,無法發放糖果"); } }
/**
* 投硬幣的狀態
*/
public class HasQuarterState extends State{ GumballMachine gumballMachine; public HasQuarterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("請不要重複投幣!"); returnQuarter(); } @Override public void ejectQuarter() { returnQuarter(); gumballMachine.setState(gumballMachine.noQuarterState); } @Override public void turnCrank() { System.out.println("轉動曲軸,準備發糖"); gumballMachine.setState(gumballMachine.soldState); } @Override public void dispense() { System.out.println("this method don't support"); } }
/**
* 出售的狀態
*/
public class SoldState extends State{ GumballMachine gumballMachine; public SoldState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("已投幣,請等待糖果"); returnQuarter(); } @Override public void ejectQuarter() { System.out.println("無法退幣,正在發放糖果,請等待"); } @Override public void turnCrank() { System.out.println("已按過曲軸,請等待"); } @Override public void dispense() { int candyCount = gumballMachine.getCandyCount(); if(candyCount > 0){ System.out.println("分發一顆糖果"); candyCount--; gumballMachine.setCandyCount(candyCount); if(candyCount > 0){ gumballMachine.setState(gumballMachine.noQuarterState); return; } } System.out.println("抱歉,糖果已售盡"); gumballMachine.setState(gumballMachine.soldOutState); } }
/**
* 售盡的狀態
*/
public class SoldOutState extends State{ GumballMachine gumballMachine; public SoldOutState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("糖果已經售盡"); returnQuarter(); } @Override public void ejectQuarter() { System.out.println("沒有投幣,無法退幣"); } @Override public void turnCrank() { System.out.println("糖果已經售盡"); } @Override public void dispense() { System.out.println("糖果已經售盡"); } }
將糖果機的動作委託到狀態類
例項
public class GumballMachine extends State{ public State noQuarterState = new NoQuarterState(this); public State hasQuarterState = new HasQuarterState(this); public State soldState = new SoldState(this); public State soldOutState = new SoldOutState(this); private State state = soldOutState; private int candyCount = 0; public GumballMachine(int count) { this.candyCount = count; if(count > 0) setState(noQuarterState); } @Override public void insertQuarter() { state.insertQuarter(); } @Override public void ejectQuarter() { state.ejectQuarter(); } @Override public void turnCrank() { state.turnCrank(); } @Override public void dispense() { state.dispense(); } public void setState(State state) { this.state = state; } public State getState() { return state; } public void setCandyCount(int candyCount) { this.candyCount = candyCount; } public int getCandyCount() { return candyCount; } }
可以發現,這種設計下,糖果機根本不需要清楚狀態的改變,它只用呼叫狀態的方法就行。狀態的改變是在狀態內部發生的。這就是"狀態模式"。
如果此時再增加一種狀態,糖果機不需要做任何改變,我們只需要再增加一個狀態類,然後在相關的狀態類方法裡面增加轉換的過程即可。