C++設計模式之狀態模式(行爲型模式)

學習軟件設計,向OO高手邁進!
設計模式(Design pattern)是軟件開發人員在軟件開發過程中面臨的一般問題的解決方案。
這些解決方案是衆多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的。
是前輩大神們留下的軟件設計的"招式"或是"套路"。

什麼是狀態模式

在本文末尾會給出解釋,待耐心看完demo再看定義,相信你會有更深刻的印象

實例講解

背景

假設我們正在爲客戶開發一款糖果機產品,客戶不是軟件專家,他們只甩給了我們一個圖,他們認爲糖果機的控制器需要如下圖般這樣工作
在這裏插入圖片描述

趕緊腦補一下糖果機是什麼樣子的?
在這裏插入圖片描述

客戶給的是一個狀態圖,一共有4個狀態:沒有1塊錢、有1塊錢、售出糖果、糖果售罄
很容易想到的就是,寫一個 CandyMachine 類,4個狀態分別用一個整數代表,把系統中所有可能發生的動作整合起來

Version 1.0

CandyMachine 類

class CandyMachine {
public:
    CandyMachine(int i_nCount) : m_nState(SOLD_OUT) {
        m_nCount = i_nCount;
        if(m_nCount > 0) {
            m_nState = NO_ONE_YUAN;
        }
    }
    // 顧客試着投入1塊錢
    virtual void UserInsertOneYuan(void);
    // 顧客試着退回1塊錢
    virtual void UserEjectOneYuan(void);
    // 顧客試着轉動手柄
    virtual void UserTurnCrank(void);
    // 發放糖果
    virtual void Dispense(void);
    virtual void ShowCurrentState(void);
private:
    static const int NO_ONE_YUAN  = 0; // 沒有1塊錢
    static const int HAS_ONE_YUAN = 1; // 有1塊錢
    static const int SOLD         = 2; // 售出糖果
    static const int SOLD_OUT     = 3; // 糖果售罄
    int m_nState;
    int m_nCount;
};

對每一個動作,都創建一個對應的方法,這些方法利用條件語句來決定在每個狀態內什麼行爲是恰當的。比如對“投入1塊錢”這個動作來說,可以把對應方法寫成下面的樣子:
顧客試着投入1塊錢

void CandyMachine::UserInsertOneYuan(void) {
    if(m_nState == NO_ONE_YUAN) {
        printf("InsertOneYuan: You inserted one yuan\n");
        m_nState = HAS_ONE_YUAN;
    } else if(m_nState == HAS_ONE_YUAN) {
        printf("InsertOneYuan: You can't insert another one yuan\n");
    } else if(m_nState == SOLD) {
        printf("InsertOneYuan: Please wait, we're already gaving you a candy\n");
    } else if(m_nState == SOLD_OUT) {
        printf("InsertOneYuan: You can't insert one yuan, the machine is sold out\n");
    }
}

顧客試着退回1塊錢

void CandyMachine::UserEjectOneYuan(void) {
    if(m_nState == NO_ONE_YUAN) {
        printf("EjectOneYuan: You haven't inserted one yuan\n");
    } else if(m_nState == HAS_ONE_YUAN) {
        printf("EjectOneYuan: One yuan returned\n");
        m_nState = NO_ONE_YUAN;
    } else if(m_nState == SOLD) {
        printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
    } else if(m_nState == SOLD_OUT) {
        printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
    }
}

顧客試着轉動手柄

void CandyMachine::UserTurnCrank(void) {
    if(m_nState == NO_ONE_YUAN) {
        printf("TurnCrank: You turned, but there's no money\n");
    } else if(m_nState == HAS_ONE_YUAN) {
        printf("TurnCrank: You turned...\n");
        m_nState = SOLD;
        Dispense();
    } else if(m_nState == SOLD) {
        printf("TurnCrank: Turning twice doesn't get you another candy!\n");
    } else if(m_nState == SOLD_OUT) {
        printf("TurnCrank: You turned, but there are no candys\n");
    }
}

發放糖果

void CandyMachine::Dispense(void) {
    if(m_nState == NO_ONE_YUAN) {
        printf("You need to pay first\n");
    } else if(m_nState == HAS_ONE_YUAN) {
        printf("No candy dispensed\n");
    } else if(m_nState == SOLD) {
        printf("A candy comes rolling out the slot...\n");
        m_nCount--;
        if(m_nCount == 0) {
            printf("Oops, out of candys!\n");
            m_nState = SOLD_OUT;
        } else {
            m_nState = NO_ONE_YUAN;
        }
    } else if(m_nState == SOLD_OUT) {
        printf("No candy dispensed\n");
    }
}

main函數

int main(int argc, char *argv[])
{
    CandyMachine *p_objCandyMachine = new CandyMachine(5);

    p_objCandyMachine->ShowCurrentState();

    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();

    p_objCandyMachine->ShowCurrentState();

    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserEjectOneYuan();
    p_objCandyMachine->UserTurnCrank();

    p_objCandyMachine->ShowCurrentState();

    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();
    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();
    p_objCandyMachine->UserEjectOneYuan();

    p_objCandyMachine->ShowCurrentState();

    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();
    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();
    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();

    p_objCandyMachine->ShowCurrentState();

    return 0;
}

運行結果

m_nState = NO_ONE_YUAN

InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
m_nState = NO_ONE_YUAN

InsertOneYuan: You inserted one yuan
EjectOneYuan: One yuan returned
TurnCrank: You turned, but there's no money
m_nState = NO_ONE_YUAN

InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
EjectOneYuan: You haven't inserted one yuan
m_nState = NO_ONE_YUAN

InsertOneYuan: You inserted one yuan
InsertOneYuan: You can't insert another one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
Oops, out of candys!
InsertOneYuan: You can't insert one yuan, the machine is sold out
TurnCrank: You turned, but there are no candys
m_nState = SOLD_OUT

測試正常
在這裏插入圖片描述

該來的躲不掉…變更需求!

客戶認爲,他們想將購買糖果這件事變成一個遊戲,當手柄被轉動時,有10%的概率掉下來的是兩顆糖果(多送你一個,你就是贏家),相信這個改變可以大大增加他們的銷售量!
在這裏插入圖片描述

思考改進

Version 1.0的代碼,看起來不太好擴展。我們要添加一個贏家的狀態,然後在下面每個方法中加入一個新的條件判斷來處理“贏家”狀態,這聽起來就挺麻煩的。而且未來因新需求而加入的代碼也很可能會導致 bug

void UserInsertOneYuan(void);
void UserEjectOneYuan(void);
void UserTurnCrank(void);
void Dispense(void);

如果我們將每個狀態的行爲都放在各自的類中(而不是放在 CandyMachine 類中),那麼每個狀態只要實現它自己的動作就可以了。然後糖果機(CandyMachine 類)只需要委託給代表當前狀態的對象去做事情即可,這不正是**“多用組合,少用繼承”**的體現嗎?具體是:

  1. 首先我們定義一個 State 接口,在這個接口內,糖果機的每個動作(UserInsertOneYuan、UserEjectOneYuan、UserTurnCrank、Dispense)都有一個對應的方法
  2. 然後爲機器中的每個狀態實現狀態類(NoOneYuanState、HasOneYuanState、SoldState、SoldOutState),這些類將負責在對應的狀態下進行機器的行爲
  3. 最後,我們要擺脫舊的條件代碼,取而代之的是,將動作委託給狀態類

類圖
在這裏插入圖片描述

我們先來完成替換 Version 1.0 的代碼,後面再來處理“贏家”的事

Version 2.0

State 接口

class State {
public:
    // 顧客試着投入1塊錢
    virtual void InsertOneYuan(void) = 0;
    // 顧客試着退回1塊錢
    virtual void EjectOneYuan(void) = 0;
    // 顧客試着轉動手柄
    virtual void TurnCrank(void) = 0;
    // 發放糖果(屬於糖果機內部行爲, 顧客不可控)
    virtual void Dispense(void) = 0;
};

我們要做的事情,是去實現這個狀態的所有行爲。在某些條件下,這個行爲會讓糖果機(CandyMachine 類)的狀態改變
NoOneYuanState 類

class NoOneYuanState : public State {
public:
    NoOneYuanState(CandyMachine *i_pCandyMachine) {
        m_pCandyMachine = i_pCandyMachine;
    }
    virtual void InsertOneYuan(void) {
        printf("InsertOneYuan: You inserted one yuan\n");
        m_pCandyMachine->SetState(m_pCandyMachine->GetHasOneYuanState());
    }
    virtual void EjectOneYuan(void) {
        printf("EjectOneYuan: You haven't inserted one yuan\n");
    }
    virtual void TurnCrank(void) {
        printf("TurnCrank: You turned, but there's no money\n");
    }
    virtual void Dispense(void) {
        printf("You need to pay first\n");
    }
private:
    CandyMachine *m_pCandyMachine;
};

HasOneYuanState 類

class HasOneYuanState : public State {
public:
    HasOneYuanState(CandyMachine *i_pCandyMachine) {
        m_pCandyMachine = i_pCandyMachine;
    }
    virtual void InsertOneYuan(void) {
        printf("InsertOneYuan: You can't insert another one yuan\n");
    }
    virtual void EjectOneYuan(void) {
        printf("EjectOneYuan: One yuan returned\n");
        m_pCandyMachine->SetState(m_pCandyMachine->GetNoOneYuanState());
    }
    virtual void TurnCrank(void) {
        printf("TurnCrank: You turned...\n");
        m_pCandyMachine->SetState(m_pCandyMachine->GetSoldState());
    }
    virtual void Dispense(void) {
        printf("No candy dispensed\n");
    }
private:
    CandyMachine *m_pCandyMachine;
};

SoldState 類

class SoldState : public State {
public:
    SoldState(CandyMachine *i_pCandyMachine) {
        m_pCandyMachine = i_pCandyMachine;
    }
    virtual void InsertOneYuan(void) {
        printf("InsertOneYuan: Please wait, we're already gaving you a candy\n");
    }
    virtual void EjectOneYuan(void) {
        printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
    }
    virtual void TurnCrank(void) {
        printf("TurnCrank: Turning twice doesn't get you another candy!\n");
    }
    virtual void Dispense(void) {
        m_pCandyMachine->ReleaseCandy();
        if(m_pCandyMachine->GetCount() > 0) {
            m_pCandyMachine->SetState(m_pCandyMachine->GetNoOneYuanState());
        } else {
            printf("Oops, out of candys!\n");
            m_pCandyMachine->SetState(m_pCandyMachine->GetSoldOutState());
        }
    }
private:
    CandyMachine *m_pCandyMachine;
};

SoldOutState 類

class SoldOutState : public State {
public:
    SoldOutState(CandyMachine *i_pCandyMachine) {
        m_pCandyMachine = i_pCandyMachine;
    }
    virtual void InsertOneYuan(void) {
        printf("InsertOneYuan: You can't insert one yuan, the machine is sold out\n");
    }
    virtual void EjectOneYuan(void) {
        printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
    }
    virtual void TurnCrank(void) {
        printf("TurnCrank: You turned, but there are no candys\n");
    }
    virtual void Dispense(void) {
        printf("No candy dispensed\n");
    }
private:
    CandyMachine *m_pCandyMachine;
};

接着還有 CandyMachine 類

class CandyMachine {
public:
    CandyMachine(int i_nCount);

    // 現在這3個動作變得很容易實現了, 我們只需委託到當前狀態即可
    virtual void UserInsertOneYuan(void);
    virtual void UserEjectOneYuan(void);
    virtual void UserTurnCrank(void);

    virtual void ReleaseCandy(void);
    virtual void SetState(State *i_pState);
    virtual State *GetNoOneYuanState(void);
    virtual State *GetHasOneYuanState(void);
    virtual State *GetSoldState(void);
    virtual State *GetSoldOutState(void);
    virtual int GetCount(void);
    virtual void ShowCurrentState(void);

private:
    State *m_pNoOneYuanState;
    State *m_pHasOneYuanState;
    State *m_pSoldState;
    State *m_pSoldOutState;

    State *m_pState;
    int m_nCount;
};

CandyMachine 的具體實現,變得簡單了,糖果機將動作行爲直接委託給當前的狀態就可以了

CandyMachine::CandyMachine(int i_nCount) {
    m_pNoOneYuanState  = new NoOneYuanState(this);
    m_pHasOneYuanState = new HasOneYuanState(this);
    m_pSoldState       = new SoldState(this);
    m_pSoldOutState    = new SoldOutState(this);
    m_pState = m_pSoldOutState;
    m_nCount = i_nCount;
    if(m_nCount > 0) {
        m_pState = m_pNoOneYuanState;
    }
}
void CandyMachine::UserInsertOneYuan(void) {
    m_pState->InsertOneYuan();
}
void CandyMachine::UserEjectOneYuan(void) {
    m_pState->EjectOneYuan();
}
void CandyMachine::UserTurnCrank(void) {
    m_pState->TurnCrank();
    m_pState->Dispense();
}
void CandyMachine::ReleaseCandy(void) {
    printf("A candy comes rolling out the slot...\n");
    if(m_nCount != 0) {
        m_nCount--;
    }
}
void CandyMachine::SetState(State *i_pState) {
    m_pState = i_pState;
}
State *CandyMachine::GetNoOneYuanState(void) {
    return m_pNoOneYuanState;
}
State *CandyMachine::GetHasOneYuanState(void) {
    return m_pHasOneYuanState;
}
State *CandyMachine::GetSoldState(void) {
    return m_pSoldState;
}
State *CandyMachine::GetSoldOutState(void) {
    return m_pSoldOutState;
}
int CandyMachine::GetCount(void) {
    return m_nCount;
}

main函數,跟 Version 1.0 一樣,不用改
運行結果,也跟 Version 1.0 一樣

檢查一下,到目前爲止我們已經做了哪些事情…

你現在有了一個糖果機的實現,它在結構上和前一個版本差異頗大,但是功能上卻是一樣的。通過從結構上改變實現,你已經做到了以下幾點:

  1. 將每個狀態的行爲局部化到它自己的類中

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

  3. 讓每一個狀態“對修改關閉”,讓糖果機“對擴展開放”,因爲可以方便的加入新的狀態類

Version 2.1

我們立刻加入新的狀態類——“贏家”狀態,應該很簡單了
類圖
在這裏插入圖片描述

首先,我們要在 CandyMachine 類中加入一個成員:State *m_pWinnerState;

然後來實現 WinnerState 類,其實它跟 SoldState 類很像,只是多了一行代碼,多釋放一個糖果:
m_pCandyMachine->ReleaseCandy();

class WinnerState : public State {
public:
    WinnerState(CandyMachine *i_pCandyMachine) {
        m_pCandyMachine = i_pCandyMachine;
    }
    virtual void InsertOneYuan(void) {
        printf("InsertOneYuan: Please wait, we're already gaving you a candy\n");
    }
    virtual void EjectOneYuan(void) {
        printf("EjectOneYuan: You can't eject, you haven't inserted one yuan yet\n");
    }
    virtual void TurnCrank(void) {
        printf("TurnCrank: Turning twice doesn't get you another candy!\n");
    }
    virtual void Dispense(void) {
        printf("YOU'RE A WINNER! You get two candys\n");
        m_pCandyMachine->ReleaseCandy();
        m_pCandyMachine->ReleaseCandy();
        if(m_pCandyMachine->GetCount() > 0) {
            m_pCandyMachine->SetState(m_pCandyMachine->GetNoOneYuanState());
        } else {
            printf("Oops, out of candys!\n");
            m_pCandyMachine->SetState(m_pCandyMachine->GetSoldOutState());
        }
    }
private:
    CandyMachine *m_pCandyMachine;
};

最後,我們要增加一個進入 WinnerState 狀態的轉換,由前面的代碼可知,進入 SoldState 狀態的是在 HasOneYuanState 狀態的 TurnCrank 方法裏面,所以接下來要修改一下 HasOneYuanState 類。同時把產生 10% 概率的方法也放在 HasOneYuanState 類中

    virtual void TurnCrank(void) {
        printf("TurnCrank: You turned...\n");
        if(IsWinner() && (m_pCandyMachine->GetCount() > 1)) {
            m_pCandyMachine->SetState(m_pCandyMachine->GetWinnerState());
        } else {
            m_pCandyMachine->SetState(m_pCandyMachine->GetSoldState());
        }
    }
    // 10%的概率返回true
    virtual bool IsWinner(void) {
        int nRandom = 0;
        srand((int)time(0));
        nRandom = rand() % 100;
        if(nRandom < 10) {
            return true;
        }
        return false;
    }

main 函數

int main(int argc, char *argv[])
{
    CandyMachine *p_objCandyMachine = new CandyMachine(5);

    p_objCandyMachine->ShowCurrentState();

    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();

    p_objCandyMachine->ShowCurrentState();
    sleep(1);

    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();

    p_objCandyMachine->ShowCurrentState();
    sleep(1);

    p_objCandyMachine->UserInsertOneYuan();
    p_objCandyMachine->UserTurnCrank();

    p_objCandyMachine->ShowCurrentState();

    return 0;
}

運行結果

m_pState = NO_ONE_YUAN

InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
m_pState = NO_ONE_YUAN

InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
YOU'RE A WINNER! You get two candys
A candy comes rolling out the slot...
A candy comes rolling out the slot...
m_pState = NO_ONE_YUAN

InsertOneYuan: You inserted one yuan
TurnCrank: You turned...
A candy comes rolling out the slot...
m_pState = NO_ONE_YUAN

運行了幾次,還是有機會成爲“贏家”的!
在這裏插入圖片描述

狀態模式定義

現在,我們來說下什麼是狀態模式?沒錯,我們上面的實例用的就是狀態模式。
狀態模式允許對象在內部狀態改變時改變它的行爲,對象看起來好像修改了它的類

這個描述的第一部分是什麼意思呢?狀態模式將狀態封裝成爲獨立的類,並將動作委託到代表當前狀態的對象,所以行爲會隨着內部狀態而改變
例如:當糖果機是在 NoOneYuanState 或 HasOneYuanState 兩種不同的狀態時,顧客投入1塊錢,就會得到不同的行爲(機器接受1塊錢或機器拒絕1塊錢)

這個描述的第二部分又是什麼意思呢?從客戶的視角來看:如果你使用的對象能夠完全改變它的行爲,那麼你會覺得,這個對象實際上是從別的類實例化而來的。但實際上,我們是在使用組合並利用多態來引用不同的狀態對象來實現的

看一下狀態模式的類圖
在這裏插入圖片描述

沒錯,策略模式的圖和這張圖是一樣的!
基本常識:策略模式和狀態模式是雙胞胎,在出生時才分開。你已經知道了,策略模式是圍繞可以互換的算法來創建業務的,然而,狀態走的是更崇高的路,它通過改變對象內部的狀態來幫助對象控制自己的行爲。這兩個模式的差別在於它們的“意圖”

以狀態模式而言,我們將一羣行爲封裝在狀態對象中,Context 的行爲隨時可委託到那些狀態對象中的一個。隨着時間的流逝,當前狀態在狀態對象集合中游走改變,以反映出 Context 內部的狀態,因此,Context 的行爲也會跟着改變。但是Context 的客戶(main 函數)對於狀態對象瞭解不多,甚至根本是渾然不覺

而以策略模式而言,客戶(main 函數)通常主動指定 Context (殭屍例子中的紅頭殭屍)所要組合的策略對象(殭屍例子中的速度和攻擊方式)是哪一個。固然策略模式讓我們具有彈性,能夠在運行時改變策略,但對於某個 Context 對象來說,通常都只有一個最適當的策略對象(一般不會像狀態模式裏的 Context 對象的狀態變來變去)

一般來說,我們把策略模式想成是除了繼承之外的一種彈性替代方案。如果你使用繼承定義了一個類的行爲,你將被這個行爲困住,甚至要修改它都很難。有了策略模式,你可以通過組合不同的對象來改變行爲

我們把狀態模式想成是不用在 Context 中放置許多條件判斷的替代方案。通過將行爲包裝進狀態對象中,你可以通過在 Context 內簡單的改變對象來改變 Context 的行爲

狀態模式的優缺點

無論哪種模式都有其優缺點,當然我們每次在編寫代碼的時候需要考慮下其利弊
狀態模式的優點:

  1. 每個狀態都是一個子類,只要增加狀態就要增加子類,修改狀態,只修改一個子類即可
  2. 結構清晰,避免了過多的 switch…case 或 if…else 語句的使用,避免了程序的複雜性,提高可維護性
  3. 外界調用不知道 Context 內部的狀態改變,只要調用其方法即可

狀態模式的缺點:

  1. 狀態模式的使用必然會增加系統類和對象的個數。由於所有的狀態都是一個類,有的 Context 對象可能會有非常多的狀態,這個時候使用狀態模式就會導致類特別多,不利於維護

總結

在軟件開發過程中,應用程序可能會根據不同的情況作出不同的處理。最直接的解決方案是將這些所有可能發生的情況全都考慮到。然後使用 if ellse 語句來做狀態判斷進行不同情況的處理。但是對複雜狀態的判斷就顯得“力不從心了”。隨着增加新的狀態或者修改一個狀態(if else 或 switch case 語句的增多或者修改)可能會引起很大的修改,而程序的可讀性,擴展性也會變得很弱,維護也會很麻煩,這時就要考慮只修改自身狀態的模式

在狀態模式中,Context 是持有狀態的對象,但是 Context 自身並不處理跟狀態相關的行爲,而是把處理狀態的行爲委託給了對應的狀態處理類來處理

在具體的狀態處理類中經常需要獲取 Context 自身的數據,甚至在必要的時候會回調 Context 的方法,因此,通常將 Context 自身當作一個參數傳遞給具體的狀態處理類(如 NoOneYuanState 的構造函數所需的參數就是 CandyMachine 的指針)

客戶(main 函數)一般只和 Context 交互。客戶可以用狀態對象來配置一個 Context,一旦配置完畢,就不再需要和狀態對象打交道了。客戶通常不負責運行期間狀態的維護,也不負責決定後續到底使用哪一個具體的狀態對象
在這裏插入圖片描述

參考資料

https://blog.csdn.net/qq_31984879/article/details/85199258

Head+First設計模式(中文版).pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章