1. 程式人生 > 實用技巧 >策略模式 VS 狀態模式

策略模式 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;
    }

}

可以發現,這種設計下,糖果機根本不需要清楚狀態的改變,它只用呼叫狀態的方法就行。狀態的改變是在狀態內部發生的。這就是"狀態模式"。

如果此時再增加一種狀態,糖果機不需要做任何改變,我們只需要再增加一個狀態類,然後在相關的狀態類方法裡面增加轉換的過程即可。

https://www.runoob.com/w3cnote/state-vs-strategy.html