1. 程式人生 > >設計模式(一)策略模式和狀態模式

設計模式(一)策略模式和狀態模式

今天要和大家一起分享下在《Head First 設計模式》學習到的內容,很實用的兩個模式:策略模式,狀態模式。

為什麼要說這兩個模式呢。在模式圖中,是一樣的,他倆就像孿生兄弟,但是目的卻不同。

狀態模式:

書中說明了列舉了下面情況。

糖果公司要求實現糖果機功能。狀態圖如下。

一般情況會用例項變數來持有目前狀態,然後定義每個狀態值。

final static int SOLD_OUT = 0;// 售罄
final static int NO_QUARTER = 1;// 沒有25分錢
final static int HAS_QUARTER = 2;// 有25分錢
final static int SOLD = 3;//  出售糖果

int state = SOLD_OUT;// 初始狀態為售罄
還有能對糖果機做的操作,如:投入25分錢,退回25分錢,轉動曲柄,發放糖果

針對每個動作都會做出一個函式,這些函式利用條件語句來決定每個狀態內什麼行為是什麼。

如“投入25分錢”

public void insertQuarter() {
        if (state == HAS_QUARTER) {
            System.out.println("You can't insert another quarter");
        } else if (state == NO_QUARTER) {
            state = HAS_QUARTER;// 轉換到另一個狀態
System.out.println("You inserted a quarter"); } else if (state == SOLD_OUT) { System.out.println("Yon 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"); } }

每個可能的狀態都需要條件語句檢查,然後對每一個可能的狀態展現適當的行為,但是也可以轉換到另一個狀態像狀態圖中所描繪的那樣。

例如:當狀態是NO_QUARTER的時候表示糖果機的初始狀態,等待投入25分錢硬幣。當呼叫insertQuarter方法的時候,狀態就會由NO_QUARTER轉換到HAS_QUARTER.

在程式碼中的操作如下

else if (state == NO_QUARTER) {
            state = HAS_QUARTER;// 轉換到另一個狀態
            System.out.println("You inserted a quarter");
        } 

並且針對其他的無效操作,做出相應的處理。一個函式中,每個狀態都會做出if判斷,來對每種狀態做出處理。

整體程式碼如下:

public class GumballMachine {

    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;
    int count = 0;

    public GumballMachine(int count) {
        this.count = count;
        if (count > 0){
            state = NO_QUARTER;
        }
    }

    public void insertQuarter() {
        if (state == HAS_QUARTER) {
            System.out.println("You can't insert another quarter");
        } else if (state == NO_QUARTER) {
            state = HAS_QUARTER;
            System.out.println("You inserted a quarter");
        } else if (state == SOLD_OUT) {
            System.out.println("Yon 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");
        }
    }

    public void turnCrank() {
        if (state == SOLD) {
            System.out.println("Turning twice doesn't get you another gumball");
        } else if (state == NO_QUARTER) {
            System.out.println("You turned but there's no quarter");
        } else if (state == SOLD_OUT) {
            System.out.println("You turned, but there's no gumballs");
        } else if (state == HAS_QUARTER) {
            System.out.println("You turned...");
            state = SOLD;
            dispense();
        }
    }

    //省略其他方法
}
 
 

 
 

當需求變動時,糖果機需要10%的概率可以發放兩粒糖果。

這可有的忙了。

先要在下面加上贏家狀態

final static int SOLD_OUT = 0;// 售罄
final static int NO_QUARTER = 1;// 沒有25分錢
final static int HAS_QUARTER = 2;// 有25分錢
final static int SOLD = 3;//  出售糖果
//新增一個贏家狀態 WINNER

int state = SOLD_OUT;// 初始狀態為售罄

public void insertQuarter() 等方法要加入一個新的條件判斷來處理“贏家”狀態。0.0 要是有100個方法怎麼辦。一個個老實加吧。
public void turnCrank() 方法有的忙了,你必須檢測顧客是否是贏家,還要決定切換到贏家狀態還是售出糖果狀態。


沒有需求不變的專案,尤其是奇葩客戶以及大公司的設計,式樣。0.0

只能讓我們程式碼變得有彈性,維護性更高,才能讓我們更高效的利用我們程式設計師寶貴的時間。否則費大勁改來改去,bug還一堆。以後很多程式碼都不敢動,重複的程式碼越來越多。很多都重寫一套。噩夢啊。


此時就要遵守一個設計原則,“封裝變化”。要讓變化影響的範圍越小越好。

針對上面的實現程式碼,不要維護現有的,重寫它以便於將狀態物件封裝在各自的類中,然後在動作發生時委託給當前狀態。

我們要做的是:

1.首先,我們先定義一個介面State。在這個介面中糖果機的每個動作都有一個對應的方法。

2.然後為機器中的每個狀態實現狀態類。這些類負責在對應的狀態下進行機器的行為。

3.最後,我們要擺脫舊的條件程式碼,取而代之的方式是,將動作委託到狀態類。

我們把一個狀態的所有行為放在一個類中。這樣以來我們將行為區域性化了,並使得事情容易改變和理解。

定義狀態介面



此時糖果機變成了

public class GumballMachine2 {
    State soldOutState;
    State noQuarterState;
    State hasQuarterState;
    State soldState;

    State state = soldOutState;
    int count = 0;

    public int getCount() {
        return count;
    }

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

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

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

    public void turnCrank(){
        state.turnCrank();
        state.dispense();
    }

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

    void releaseBall(){
        System.out.println("A gumball comes rolling out the slot...");
        if(count != 0){
            count = count - 1;
        }
    }

    public State getNoQuarterState(){
        return noQuarterState;
    }

    public State getHasQuarterState() {
        return hasQuarterState;
    }

    public State getSoldState() {
        return soldState;
    }

    public State getSoldOutState() {
        return soldOutState;
    }
}
具體的行為,只用類中的state變數處理就可以。初始狀態是noQuarterState。

public class NoQuarterState implements State{

    GumballMachine2 gumballMachine;

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

    @Override
    public void insertQuarter() {
        System.out.println("Yon inserted a quarter");
        gumballMachine.setState(gumballMachine.getHasQuarterState());
    }

    @Override
    public void ejectQuarter() {
        System.out.println("You haven't inserted a quarter");
    }

    @Override
    public void turnCrank() {
        System.out.println("You turned, but there is no quarter");
    }

    @Override
    public void dispense() {
        System.out.println("You need to pay first");
    }
}

呼叫insertQuarter函式後,把糖果機gumballMachine的狀態變為 hasQuarter。


同一個行為不同狀態會做出不同的處理。並且行為也會改變狀態。

把當初的if判斷拆分成了類結構,具體的操作在類中修改。以封裝變化的部分為宗旨。

我們做到了什麼:

* 將每個狀態的行為區域性化到它自己的類中。

* 將容易產生問題的if語句刪除,以方便日後的維護。

* 讓每個狀態“對修改關閉”,讓糖果機“對擴充套件開放”,因為可以加入新的狀態類

* 建立一個新的程式碼基和類結構,這更能對映糖果公司的圖,而且更容易閱讀和理解。

我們來看看狀態模式的定義:

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

乍一看這個定義是什麼啊。簡直崩潰!!我來解釋下。

第一句“允許物件在內部狀態改變時改變它的行為”,此處的物件就是糖果機GumballMachine,狀態就是它持有的當前狀態的引用 State state。因為這個模式將狀態封裝成為獨立的類,

並將動作委託到代表當前狀態的物件,我們知道行為會隨著內部狀態而改變。以上例子中:當糖果機在No_QUARTER和HAS_QUARTER兩種不同的狀態時,你投入25分錢,

就會得到不同的行為(機器接受25分錢和機器拒絕收錢)

第二句“物件看起來好像修改了它的類”,從客戶的角度來看,如果說你使用的物件能夠完全改變它的行為,那麼你會覺得,這個實際上是從別的類例項化而來的。然而實際上,

你知道我們是在使用組合通過簡單的引用不同的狀態物件來造成類改變的假象。


狀態模式就介紹到這裡,具體可以參考《Head First設計模式》,還有其他文章。學習模式我覺得比較好的方式就是對比。下面我們來介紹下和它差不多的策略模式。

對比之後就會加深我們對模式的理解。對於應用場景的問題,很多同學想問,這個模式能用在哪裡。這個還要看具體的業務,我無法給除準確的回答。但是,學習

設計模式到一定程度之後,已經心中有很多設計原則,即使不用設計模式,也會寫出擴充套件性很高的程式碼。其實各個模式的中心思想都是一樣的,“封裝變化”,“多用組合少用繼承”,

“類應該只有一個改變的理由”等設計原則。具體設計原則可以去搜索下,放在腦子裡,才會運用自如。而不是就看到糖果機,才知道用狀態模式。0.0

策略模式:

場景是鴨子應用。類圖如下:


這是我們經常處理問題的方式。找到型別的共同點,抽出個抽象模型。Duck。可以保證程式碼的複用。

一切看起來是那麼好,但是需求變更的時候,噩夢就來了。

假如客戶要求鴨子有飛的行為。我們首先想到的是在父類上加fly方法就可以搞定。


如果子類有橡皮鴨,根本不會飛的鴨子。擁有這個方法就沒有意義了。有的人解決方案是,fly裡面什麼也不處理就可以啦。但是有另一個子類,不會quack,也不會fly呢。

那這兩個方法都要什麼都不做。如果還要在父類上加上N個行為的方法呢。所有的子類都要改變一下。有的做出具體的操作,有的把方法做出空實現。

這樣看啦,繼承不是我們想要的解決方案。

聰明的程式設計師想到了,把fly從超類中抽出來。放到一個flyable介面中。只有會飛的鴨子實現此介面。


看起來不錯的設計,但是還是有問題。如果有48個子類,每個子類的飛行行為有相同的有不同的,那麼每個鴨子子類都要實現飛行行為,重複程式碼增多。

綜上我們再回顧下問題:

並非所有的子類都有飛行和呱呱叫行為,所以繼承並不適合我們。雖然flyable和quackable可以解決部分問題,但是程式碼無法複用。

此時來了一個設計原則幫助我們。找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的程式碼混在一起。

如果每次新的需求一來,都會使某方面的程式碼發生變化,你可以確定這部分程式碼需要被抽出來,和其他穩定的程式碼有所區分。

這個原則可以換成這種說法,把會變化的部分取出並封裝起來,以便以後可以輕易地改動或擴充此部分,而不影響不需要變化的其他部分。

這幾乎是每個設計模式背後的精神所在。所有的模式都提供了一套方法讓“系統中某部分改變不會影響其他部分”。

該是把鴨子的行為從Duck中取的時候了!

鴨子的行為將被放在分開的類中,此類專門提供某行為介面的實現。這樣,鴨子就不需要知道行為的實現細節。

我們利用介面代表每個行為,比方說,FlyBehavior和QuackBehavior,而行為的每個實現都將實現其中一個介面。所以鴨子類不會負責實現FlyBehavior和QuackBehavior,

反而是由我們製造一組其他類專門實現FlyBehavior和QuackBehavior,這就成為“行為”類。由行為類而不是Duck類來實現行為介面。

以前的做法是:行為來自Duck超類的具體實現,或是繼承某個介面並由子類自行實現而來。這兩種做法都是依賴於“實現”,我們被實現綁得死死的,沒辦法改行為。


這樣的設計,可以讓飛行和呱呱叫的動作被其他的物件複用,因為這些行為已經與鴨子類無關了。

而我們可以新增一些行為,不會影響到既有的行為類,也不會影響“使用”到飛行行為的鴨子類。這樣一來有了繼承的“複用”好處,卻沒有了繼承所帶來的包袱。

鴨子會將飛行和呱呱叫動作“委託”別人處理,而不是使用定義在Duck類(或子類)內的呱呱叫和飛行方法。

在Duck類中加入兩個例項變數,分別為flyBehavior和quackBehavior,宣告為介面型別,每個鴨子物件都會動態的設定這些變數以在執行時引用正確的行為型別。

用方法performFly和performQuack取代Duck類中的fly和quack。


上程式碼
public abstract class Duck {

    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public Duck() {
    }

    public abstract void display();

    public void performFly(){
        flyBehavior.fly();
    }

    public void performQuack(){
        quackBehavior.quack();
    }

    public void setFlyBehavior(FlyBehavior flyBehavior){
        this.flyBehavior = flyBehavior;
    }

    public void setQuackBehavior(QuackBehavior quackBehavior){
        this.quackBehavior = quackBehavior;
    }

    public void swim(){
        System.out.println("All ducks can swim, even decoys!");
    }
}
 
 

public interface FlyBehavior {

    public void fly();
}
public class FlyWithWings implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("I'm flying!");
    }
}
public class FlyNoWay implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("I can not fly!");
    }
}
public interface QuackBehavior {

    public void quack();
}
public class Quack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("Quack!");
    }
}
public class Squeak implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("Squeak");
    }
}
其中Duck中的
public void setFlyBehavior(FlyBehavior flyBehavior){
    this.flyBehavior = flyBehavior;
}

public void setQuackBehavior(QuackBehavior quackBehavior){
    this.quackBehavior = quackBehavior;
}
 方法是用來動態設定行為的 
 

此時我們又多了一個設計原則,多用在組合,少用繼承。Duck中的flyBehavior和quackBehavior變數就是組合形式。

現在大家來看看策略模式的定義

策略模式 定義演算法族,分別封裝起來,讓它們之間可以互相替換,此模式讓演算法的變化獨立於使用演算法的客戶。

看看和狀態模式的類圖是不是很像。但是這兩個模式的意圖不同。

總結:

狀態模式在開始的時候就會設定狀態值,從什麼狀態開始。然後隨著時間改變自己的狀態。雖然策略模式也可以通過宣告set行為的方法來替換行為,但是狀態模式的狀態改變

是都定義好的,改變行為是建立在狀態模式的方案中。也就是說,狀態的轉換已經在狀態模式的方案中定義好了。

而策略模式會控制物件使用什麼策略。

今天我們學到的原則

OO原則:

封裝變化

多用組合,少用繼承