加入星計(jì)劃,您可以享受以下權(quán)益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴(kuò)散
  • 作品版權(quán)保護(hù)
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長(zhǎng)期合作伙伴
立即加入
  • 正文
    • 1 策略模式
    • 2 收銀軟件實(shí)例
    • 3 總結(jié)
  • 推薦器件
  • 相關(guān)推薦
  • 電子產(chǎn)業(yè)圖譜
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

《大話設(shè)計(jì)模式》解讀02-策略模式

06/24 10:35
543
閱讀需 28 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

本篇文章,來(lái)解讀《大話設(shè)計(jì)模式》的第2章——策略模式。并通過(guò)Qt和C++代碼實(shí)現(xiàn)實(shí)例代碼的功能。

1 策略模式

策略模式作為一種軟件設(shè)計(jì)模式,指對(duì)象有某個(gè)行為,但是在不同的場(chǎng)景中,該行為有不同的實(shí)現(xiàn)算法。

策略模式的特點(diǎn):

    定義了一組算法(業(yè)務(wù)規(guī)則)封裝了每個(gè)算法這一類的算法可互換代替

策略模式的組成:

    抽象策略角色(策略類): 通常由一個(gè)接口或者抽象類實(shí)現(xiàn)具體策略角色:包裝了相關(guān)的算法和行為環(huán)境角色(上下文):持有一個(gè)策略類的引用(或指針),最終給客戶端調(diào)用

策略模式(Strategy):它定義了算法家族,分別封裝起來(lái),讓它們之間可以互相替換,此模式讓算法的變化,不會(huì)影響到使用算法的客戶。

2 收銀軟件實(shí)例

題目:做一個(gè)商場(chǎng)收銀軟件,營(yíng)業(yè)員根據(jù)用戶所購(gòu)買商品的單價(jià)數(shù)量,向客戶收費(fèi)

我們聯(lián)想策略模式,對(duì)于收費(fèi)行為,在不同的場(chǎng)景中(正常收費(fèi)、打折收費(fèi)、滿減收費(fèi)),對(duì)應(yīng)不同的算法(或稱策略)實(shí)現(xiàn)。

下面先來(lái)看版本一,還未使用策略模式,僅實(shí)現(xiàn)基礎(chǔ)的收費(fèi)計(jì)算。

2.1 版本一:基礎(chǔ)收費(fèi)

這里使用Qt設(shè)計(jì)一個(gè)收費(fèi)系統(tǒng)的界面,每次可以輸入單價(jià)和數(shù)量,點(diǎn)確定按鈕之后,會(huì)在信息框中展示此次的合計(jì)價(jià)格,支持多個(gè)商品的多次計(jì)算,多次計(jì)算的總價(jià)在最下面的總計(jì)欄中展示。

對(duì)應(yīng)的代碼實(shí)現(xiàn)如下:

    on_okBtn_clicked 為Qt點(diǎn)擊確定按鈕后的槽函數(shù):該函數(shù)實(shí)現(xiàn)為,此次的價(jià)格合計(jì)等于價(jià)格x數(shù)量,多次的價(jià)格累加是總計(jì)價(jià)格。on_resetBtn_clicked 為Qt點(diǎn)擊重置按鈕后的槽函數(shù):該函數(shù)實(shí)現(xiàn)為,清空相關(guān)的顯示和各種數(shù)據(jù)
void Widget::on_okBtn_clicked()
{
    // 此次的價(jià)格合計(jì):價(jià)格*數(shù)量
    float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();
    // 總計(jì)
    m_fTotalPrice += thisPrice;
    
	// 窗口中展示明細(xì)
    ui->showPanel->append("price:" + ui->priceEdit->text()
                          + ", num:" + ui->numEdit->text()
                          + " -> (" + QString::number(thisPrice) + ")");
	// 顯示總計(jì)
    ui->totalShow->setText(QString::number(m_fTotalPrice));
}

void Widget::on_resetBtn_clicked()
{
    m_fTotalPrice = 0;
    ui->showPanel->clear();
    ui->totalShow->clear();
    ui->priceEdit->clear();
    ui->numEdit->clear();
}

實(shí)際的演示效果如下,僅實(shí)現(xiàn)單價(jià)x數(shù)量功能:

如果在此基礎(chǔ)上,需要增加打折收費(fèi)功能,需要怎么做呢?下面來(lái)看版本二。

2.2 版本二:增加打折

對(duì)于打折功能,在界面上,只需要增加一個(gè)打折率的下拉框即可,然后在計(jì)算公式上在加一步乘以打折率即可,代碼改動(dòng)不大:

void Widget::on_okBtn_clicked()
{
    // 根據(jù)下拉框獲取對(duì)應(yīng)的打折率
    float rebate = 1.0;
    if (ui->calcSelect->currentIndex() == 1) rebate = 0.8;
    else if (ui->calcSelect->currentIndex() == 2) rebate = 0.7;
    else if (ui->calcSelect->currentIndex() == 3) rebate = 0.5;

    // 此次的價(jià)格合計(jì):價(jià)格*數(shù)量*打折率
    float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt() * rebate;
    // 總計(jì)
    m_fTotalPrice += thisPrice;

    // 窗口中展示明細(xì)
    ui->showPanel->append("price:" + ui->priceEdit->text()
                          + ", num:" + ui->numEdit->text()
                          + ", rebate:" + QString::number(rebate)
                          + " -> (" + QString::number(thisPrice) + ")");
    // 顯示總計(jì)
    ui->totalShow->setText(QString::number(m_fTotalPrice));
}

演示效果如下,可以支持正常收費(fèi)、八折收費(fèi)、七折收費(fèi)和五折收費(fèi)。

目前看起來(lái)代碼也還可以,但如果此時(shí)需要增加滿減活動(dòng)呢?比如滿300減100這種。

因?yàn)闈M減這種方式,不像打折那樣簡(jiǎn)單的乘以一個(gè)打折率就行了,它需要兩個(gè)參數(shù),滿減的價(jià)格條件,的,滿減的優(yōu)惠值,,對(duì)于滿300減100的方式,如果是700,滿足了2次,就要減200了,這種計(jì)算方式需要單獨(dú)再寫一套計(jì)算邏輯。

下面來(lái)看版本三是如何實(shí)現(xiàn)的。

2.3 版本三:簡(jiǎn)單工廠

聯(lián)想上次介紹的簡(jiǎn)單工廠模式,對(duì)于目前收費(fèi)的需求,實(shí)際可以將其分類三類:

    正常收費(fèi)類:不需要參數(shù)打折收費(fèi)類:需要1個(gè)參數(shù)(打折率)滿減收費(fèi)類(返利收費(fèi)類):需要2次參數(shù)(滿減的價(jià)格條件的滿減的優(yōu)惠值)

因此,可以將這3鐘方式分別封裝為單獨(dú)的收費(fèi)類,并通過(guò)簡(jiǎn)單工廠的方式,在不同的收費(fèi)需求下,實(shí)例化對(duì)應(yīng)的收費(fèi)計(jì)算對(duì)象,進(jìn)行收費(fèi)的計(jì)算。

2.3.1 收費(fèi)類相關(guān)代碼

對(duì)應(yīng)的代碼如下,設(shè)計(jì)了現(xiàn)金收費(fèi)類CashSuper以及對(duì)應(yīng)的具體子類:

    正常收費(fèi)類:CashNormal,將原價(jià)原路返回打折收費(fèi)類:CashRebate,初始化時(shí)輸入打折率,計(jì)算時(shí)返回打折后的價(jià)格返利收費(fèi)類:CashReturn,初始化時(shí)輸入滿減的條件和滿減的值,計(jì)算時(shí)返回滿減后的值
// 現(xiàn)金收費(fèi)類
class CashSuper
{
public:
    virtual float acceptCash(float money)
    {
        return money;
    }
};

// 正常收費(fèi)類
class CashNormal : public CashSuper
{
public:
    // 原價(jià)返回
    float acceptCash(float money)
    {
        return money;
    }
};

// 打折收費(fèi)類
class CashRebate : public CashSuper
{
private:
    float m_fMoneyRebate = 1.0;

public:
    // 初始化時(shí)輸入打折率
    CashRebate(float rebate)
    {
        m_fMoneyRebate = rebate;
    }

    // 返回打折后的價(jià)格
    float acceptCash(float money)
    {
        return money * m_fMoneyRebate;
    }
};

// 返利收費(fèi)類
class CashReturn : public CashSuper
{
private:
    float m_fMoneyCondition = 0;
    float m_fMoneyReturn    = 0;
public:
    // 初始化時(shí)輸入滿減的條件和滿減的值
    CashReturn(float moneyCondition, float moneyReturn)
    {
        m_fMoneyCondition = moneyCondition;
        m_fMoneyReturn    = moneyReturn;
    }

public:
    // 返回滿減后的值(滿足滿減倍數(shù),按倍數(shù)滿減)
    float acceptCash(float money)
    {
        float result = money;
        if (money >= m_fMoneyCondition)
        {
            result -= ((int) money / (int) m_fMoneyCondition) * m_fMoneyReturn;
        }
        return result;
    }
};

//現(xiàn)金收費(fèi)工廠類
class CashFactory
{
public:
    CashSuper *createCashAccept(int combIdx) // 參數(shù)為下拉列表中的索引
    {
        CashSuper *pCS = nullptr;
        switch (combIdx)
        {
            case 0: // "正常收費(fèi)"
            {
                pCS = (CashSuper *)(new CashNormal());
                break;
            }
            case 1: // "打8折"
            {
                pCS = (CashSuper *)(new CashRebate(float(0.8)));
                break;
            }
            case 2: // "滿300返100"
            {
                pCS = (CashSuper *)(new CashReturn(float(300), float(100)));
                break;
            }
            default:
                break;

        }
        return pCS;
    }
};

2.3.2 Qt界面上點(diǎn)擊確定的槽函數(shù)的修改

Qt界面上點(diǎn)擊確定,客戶端的處理邏輯如下:

    • 計(jì)算此次的價(jià)格原價(jià):價(jià)格x數(shù)量根據(jù)下拉框當(dāng)前選擇的策略,獲取對(duì)應(yīng)的索引值,目前代碼中寫了3種:

      • 索引0:正常收費(fèi)索引1:打8折索引2:滿300返100

調(diào)用現(xiàn)金計(jì)算工廠,傳入索引值,實(shí)例化對(duì)應(yīng)的現(xiàn)金計(jì)算對(duì)象調(diào)用現(xiàn)金計(jì)算對(duì)象,得到此次的計(jì)算結(jié)果,展示在窗口明細(xì)中計(jì)算總計(jì)值,顯示在總計(jì)框

void Widget::on_okBtn_clicked()
{
    // 此次的價(jià)格原價(jià):價(jià)格*數(shù)量
    float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();

    // 下拉框不同計(jì)算策略的索引值
    int idx = ui->calcSelect->currentIndex();

    // 現(xiàn)金計(jì)算工廠
    CashFactory cashFactory;
    CashSuper *pCS = cashFactory.createCashAccept(idx);
    if (pCS != nullptr)
    {
        // 傳入原價(jià),根據(jù)結(jié)算規(guī)則,得到計(jì)算后的實(shí)際價(jià)格
        thisPrice = pCS->acceptCash(thisPrice);
        delete pCS;
    }

    // 總計(jì)
    m_fTotalPrice += thisPrice;

    // 窗口中展示明細(xì)
    ui->showPanel->append("price:" + ui->priceEdit->text()
                          + ", num:" + ui->numEdit->text()
                          + ", method:" +  ui->calcSelect->currentText()
                          + " -> (" + QString::number(thisPrice) + ")");

    // 顯示總計(jì)
    ui->totalShow->setText(QString::number(m_fTotalPrice));
}

演示效果如下,可以支持正常收費(fèi)、八折收費(fèi)、滿300減100收費(fèi)。

上述代碼,使用了簡(jiǎn)單工廠模式后,如果再需要增加一種新類型的促銷手段,比如滿100元?jiǎng)t有10個(gè)積分,則只需要再增加一個(gè)現(xiàn)在收費(fèi)類即可,接收2個(gè)參數(shù)(滿足積分的條件和對(duì)應(yīng)的積分值),繼承于CashSuper類。

不過(guò),雖然簡(jiǎn)單工廠模式實(shí)現(xiàn)了對(duì)不同的收費(fèi)計(jì)算對(duì)象的創(chuàng)建管理,但對(duì)于本案例,商場(chǎng)可能經(jīng)常更改打折的額度和返利額度,而每次維護(hù)或擴(kuò)展收費(fèi)方式都要改動(dòng)這個(gè)工廠,然后代碼需要重新編譯部署,好像不是一種很好的方式。

下面來(lái)看版本四是如何實(shí)現(xiàn)的。

2.4 版本四:策略模式

版本四用到了本篇的主題——策略模式。

策略模式(Strategy):它定義了算法家族,分別封裝起來(lái),讓它們之間可以互相替換,此模式讓算法的變化,不會(huì)影響到使用算法的客戶。

對(duì)于本例,商場(chǎng)的促銷手段:打折、返利這些,對(duì)應(yīng)的就是算法。

用工廠來(lái)生成算法對(duì)象,本身也沒(méi)有問(wèn)題,但算法只是一種策略,而這些策略是隨時(shí)可能互相替換的,這就是變化點(diǎn)。

策略模式的作用就是來(lái)封裝變化點(diǎn),設(shè)計(jì)的UML類圖如下,與簡(jiǎn)單工廠的主要區(qū)別是將簡(jiǎn)單工廠類換成了上下文類

    上下文類,或稱環(huán)境類,維護(hù)對(duì)具體策略的引用現(xiàn)金收費(fèi)類,在這里對(duì)應(yīng)的是策略類(父類)3種具體收費(fèi)類,在這里對(duì)應(yīng)的是具體的策略類(子類)

策略模式和簡(jiǎn)單工廠模式初看可能比較像,下面來(lái)看下代碼實(shí)現(xiàn)的區(qū)別。

2.4.1 現(xiàn)金收費(fèi)上下文類

收費(fèi)類相關(guān)代碼。相比較版本三,收費(fèi)類和具體的收費(fèi)類都不需要?jiǎng)?,只需要把?jiǎn)單工廠類改為現(xiàn)金收費(fèi)上下文類即可

現(xiàn)金收費(fèi)上下文類有一個(gè)CashSuper的指針,實(shí)現(xiàn)對(duì)具體策略的引用

在初始化CashContext時(shí),傳入CashSuper的指針的指針,通過(guò)其提供的GetResult方法,可以得到其算法的計(jì)算結(jié)果。

這里的GetResult方法,調(diào)用的是具體策略的acceptCash方法。

//現(xiàn)金收費(fèi)上下文類
class CashContext
{
private:
    CashSuper *m_pCS = nullptr;
    
public:
    CashContext(CashSuper *pCsuper)
    {
       m_pCS = pCsuper;
    }

    ~CashContext()
    {
        if (m_pCS) delete m_pCS;
    }

    float GetResult(float money)
    {
        return m_pCS->acceptCash(money);
    }
};

2.4.2 Qt界面上點(diǎn)擊確定的槽函數(shù)的修改

Qt界面上點(diǎn)擊確定,客戶端的處理邏輯如下:

    • 計(jì)算此次的價(jià)格原價(jià):價(jià)格x數(shù)量根據(jù)下拉框當(dāng)前選擇的策略,獲取對(duì)應(yīng)的索引值(0:正常收費(fèi),1:打8折,2:滿300返100)

然后將具體的算法類作為參數(shù)來(lái)創(chuàng)建一個(gè)上下文類再調(diào)用上下文類的GetResult方法,得到此次的計(jì)算結(jié)果

    ,展示在窗口明細(xì)中計(jì)算總計(jì)值,顯示在總計(jì)框
void Widget::on_okBtn_clicked()
{
    // 此次的價(jià)格原價(jià):價(jià)格*數(shù)量
    float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();
    
    // 下拉框不同計(jì)算策略的索引值
    int idx = ui->calcSelect->currentIndex();
    CashContext *pCC = nullptr;
    switch (idx)
    {
        case 0: // "正常收費(fèi)"
        {
            pCC = new CashContext(new CashNormal());
            break;
        }
        case 1: // "打8折"
        {
            pCC = new CashContext(new CashRebate(float(0.8)));
            break;
        }
        case 2: // "滿300返100"
        {
            pCC = new CashContext(new CashReturn(float(300), float(100)));
            break;
        }
        default:
            break;
    }

    // 計(jì)算后的價(jià)格
    if (pCC != nullptr)
    {
        // 傳入原價(jià),根據(jù)結(jié)算規(guī)則,得到計(jì)算后的實(shí)際價(jià)格
        thisPrice = pCC->GetResult(thisPrice);
        delete pCC;
    }

    // 總計(jì)
    m_fTotalPrice += thisPrice;

    // 窗口中展示明細(xì)
    ui->showPanel->append("price:" + ui->priceEdit->text()
                          + ", num:" + ui->numEdit->text()
                          + ", method:" +  ui->calcSelect->currentText()
                          + " -> (" + QString::number(thisPrice) + ")");

    // 顯示總計(jì)
    ui->totalShow->setText(QString::number(m_fTotalPrice));
}

該代碼的演示效果和版本三的一樣,這里不再貼圖。

下面再來(lái)分析下版本四的策略模式和版本三的簡(jiǎn)單工廠模式的區(qū)別:

簡(jiǎn)單工廠模式

    • :通過(guò)簡(jiǎn)單工廠來(lái)得到具體的計(jì)算對(duì)應(yīng)對(duì)象,調(diào)用具體對(duì)象的acceptCash方法得到結(jié)果。

策略模式

    :通過(guò)上下文類來(lái)維護(hù)對(duì)具體策略的引用,調(diào)用上下文類的GetResult方法得到結(jié)果(本質(zhì)也是調(diào)用其維護(hù)的具體策略的acceptCash方法)。

對(duì)比發(fā)現(xiàn),兩種模式區(qū)別就在于;

    簡(jiǎn)單工廠模式是,根據(jù)你的需求,給你創(chuàng)建一個(gè)對(duì)應(yīng)的收費(fèi)計(jì)算對(duì)象,后續(xù)的收費(fèi)計(jì)算你和這個(gè)對(duì)象來(lái)對(duì)接即可。而策略模式是,根據(jù)你的需求,上下文類幫你和具體的策略對(duì)象對(duì)接,你需要計(jì)算時(shí),仍然通過(guò)上下文類的接口獲取即可。

對(duì)于版本四的代碼,Qt界面上客戶端的處理代碼又變得復(fù)雜了,如何將客戶端的那些判斷邏輯移走呢?下面來(lái)看版本五。

2.5 版本五:策略模式+簡(jiǎn)單工廠

版本四的代碼,CashContext上下文類在初始化時(shí),接收的參數(shù)是具體的策略類的指針。

在版本五中,將參數(shù)改為Qt界面收費(fèi)類型下拉框的索引值,然后在CashContext內(nèi)部,根據(jù)索引值,利用簡(jiǎn)單工廠模式,CashContext自己創(chuàng)建對(duì)應(yīng)的策略對(duì)象,代碼如下;

2.5.1 在策略模式內(nèi)加入簡(jiǎn)單工廠

//現(xiàn)金收費(fèi)上下文類
class CashContext
{
private:
    CashSuper *m_pCS = nullptr;

public:
    CashContext(int combIdx)
    {
        switch (combIdx)
        {
            case 0: // "正常收費(fèi)"
            {
                m_pCS = (CashSuper *)(new CashNormal());
                break;
            }
            case 1: // "打8折"
            {
                m_pCS = (CashSuper *)(new CashRebate(float(0.8)));
                break;
            }
            case 2: // "滿300返100"
            {
                m_pCS = (CashSuper *)(new CashReturn(float(300), float(100)));
                break;
            }
            default:
                break;
        }
    }

    ~CashContext()
    {
        if (m_pCS) delete m_pCS;
    }

    float GetResult(float money)
    {
        if (m_pCS)
        {
            return m_pCS->acceptCash(money);
        }
        return money;
    }
};

2.5.2 Qt界面上點(diǎn)擊確定的槽函數(shù)的修改

Qt界面上點(diǎn)擊確定,客戶端的處理邏輯如下:

    • 計(jì)算此次的價(jià)格原價(jià):價(jià)格x數(shù)量根據(jù)下拉框當(dāng)前選擇的策略,獲取對(duì)應(yīng)的索引值(0:正常收費(fèi),1:打8折,2:滿300返100)

然后將索引值作為參數(shù)來(lái)創(chuàng)建一個(gè)上下文類

    再調(diào)用上下文類的GetResult方法,得到此次的計(jì)算結(jié)果,展示在窗口明細(xì)中計(jì)算總計(jì)值,顯示在總計(jì)框

可以看到如下代碼中,版本五的Qt確定按鈕的邏輯,又變得清爽起來(lái)。

但實(shí)際上,只是把這部分判斷的代碼移動(dòng)到了CashContext中,如果后續(xù)需要新增一種算法,還是要修改CashContext中的判斷的,但有需求就會(huì)有修改,任何需求的變更都是有成本的,只是變更成本高低的不同,繼續(xù)降低目前CashContext的修改成本,可以利用反射技術(shù),這在后續(xù)介紹抽象工廠模式時(shí)會(huì)提到。

void Widget::on_okBtn_clicked()
{
    // 此次的價(jià)格原價(jià):價(jià)格*數(shù)量
    float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();
    
    // 下拉框不同計(jì)算策略的索引值
    int idx = ui->calcSelect->currentIndex();
    CashContext cc = CashContext(idx);

    // 傳入原價(jià),根據(jù)結(jié)算規(guī)則,得到計(jì)算后的實(shí)際價(jià)格
    thisPrice = cc.GetResult(thisPrice);

    // 總計(jì)
    m_fTotalPrice += thisPrice;

    // 窗口中展示明細(xì)
    ui->showPanel->append("price:" + ui->priceEdit->text()
                          + ", num:" + ui->numEdit->text()
                          + ", method:" +  ui->calcSelect->currentText()
                          + " -> (" + QString::number(thisPrice) + ")");

    // 顯示總計(jì)
    ui->totalShow->setText(QString::number(m_fTotalPrice));
}

版本五的演示結(jié)果與版本三、版本四的效果一樣,這里不再貼圖。

3 總結(jié)

本篇介紹了設(shè)計(jì)模式中的策略模式,并通過(guò)商場(chǎng)收費(fèi)計(jì)算軟件的實(shí)例,使用Qt和C++編程,從基礎(chǔ)的收費(fèi)功能到后續(xù)需求的增加,一步步修改代碼,來(lái)學(xué)習(xí)策略模式的使用,以及對(duì)比策略模式與簡(jiǎn)單工廠模式的不同。

推薦器件

更多器件
器件型號(hào) 數(shù)量 器件廠商 器件描述 數(shù)據(jù)手冊(cè) ECAD模型 風(fēng)險(xiǎn)等級(jí) 參考價(jià)格 更多信息
74HC595BQ,115 1 NXP Semiconductors 74HC(T)595 - 8-bit serial-in, serial or parallel-out shift register with output latches; 3-state QFN 16-Pin
$0.41 查看
CMWX1ZZABZ-078 1 Murata Manufacturing Co Ltd LORA MODULE

ECAD模型

下載ECAD模型
$16.04 查看
24LC256-I/ST 1 Microchip Technology Inc 32K X 8 I2C/2-WIRE SERIAL EEPROM, PDSO8, 4.40 MM, PLASTIC, TSSOP-8

ECAD模型

下載ECAD模型
$1.05 查看

相關(guān)推薦

電子產(chǎn)業(yè)圖譜

控制科學(xué)與工程碩士,日常分享單片機(jī)、嵌入式、C/C++、Linux等學(xué)習(xí)經(jīng)驗(yàn)干貨~