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

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

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

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

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

1 策略模式

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

策略模式的特點:

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

策略模式的組成:

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

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

2 收銀軟件實例

題目:做一個商場收銀軟件,營業(yè)員根據(jù)用戶所購買商品的單價數(shù)量,向客戶收費

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

下面先來看版本一,還未使用策略模式,僅實現(xiàn)基礎(chǔ)的收費計算。

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

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

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

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

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

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

2.2 版本二:增加打折

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

void Widget::on_okBtn_clicked()
{
    // 根據(jù)下拉框獲取對應(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;

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

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

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

目前看起來代碼也還可以,但如果此時需要增加滿減活動呢?比如滿300減100這種。

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

下面來看版本三是如何實現(xiàn)的。

2.3 版本三:簡單工廠

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

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

因此,可以將這3鐘方式分別封裝為單獨的收費類,并通過簡單工廠的方式,在不同的收費需求下,實例化對應(yīng)的收費計算對象,進行收費的計算。

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

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

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

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

// 打折收費類
class CashRebate : public CashSuper
{
private:
    float m_fMoneyRebate = 1.0;

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

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

// 返利收費類
class CashReturn : public CashSuper
{
private:
    float m_fMoneyCondition = 0;
    float m_fMoneyReturn    = 0;
public:
    // 初始化時輸入滿減的條件和滿減的值
    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)金收費工廠類
class CashFactory
{
public:
    CashSuper *createCashAccept(int combIdx) // 參數(shù)為下拉列表中的索引
    {
        CashSuper *pCS = nullptr;
        switch (combIdx)
        {
            case 0: // "正常收費"
            {
                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界面上點擊確定的槽函數(shù)的修改

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

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

      • 索引0:正常收費索引1:打8折索引2:滿300返100

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

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

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

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

    // 總計
    m_fTotalPrice += thisPrice;

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

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

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

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

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

下面來看版本四是如何實現(xiàn)的。

2.4 版本四:策略模式

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

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

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

用工廠來生成算法對象,本身也沒有問題,但算法只是一種策略,而這些策略是隨時可能互相替換的,這就是變化點。

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

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

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

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

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

現(xiàn)金收費上下文類有一個CashSuper的指針,實現(xiàn)對具體策略的引用

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

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

//現(xiàn)金收費上下文類
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界面上點擊確定的槽函數(shù)的修改

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

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

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

    ,展示在窗口明細中計算總計值,顯示在總計框
void Widget::on_okBtn_clicked()
{
    // 此次的價格原價:價格*數(shù)量
    float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();
    
    // 下拉框不同計算策略的索引值
    int idx = ui->calcSelect->currentIndex();
    CashContext *pCC = nullptr;
    switch (idx)
    {
        case 0: // "正常收費"
        {
            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;
    }

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

    // 總計
    m_fTotalPrice += thisPrice;

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

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

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

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

簡單工廠模式

    • :通過簡單工廠來得到具體的計算對應(yīng)對象,調(diào)用具體對象的acceptCash方法得到結(jié)果。

策略模式

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

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

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

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

2.5 版本五:策略模式+簡單工廠

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

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

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

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

public:
    CashContext(int combIdx)
    {
        switch (combIdx)
        {
            case 0: // "正常收費"
            {
                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界面上點擊確定的槽函數(shù)的修改

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

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

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

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

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

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

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

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

    // 總計
    m_fTotalPrice += thisPrice;

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

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

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

3 總結(jié)

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

推薦器件

更多器件
器件型號 數(shù)量 器件廠商 器件描述 數(shù)據(jù)手冊 ECAD模型 風(fēng)險等級 參考價格 更多信息
KSZ8895RQXIA 1 Microchip Technology Inc DATACOM, ETHERNET TRANSCEIVER, PQFP128

ECAD模型

下載ECAD模型
$7.22 查看
TF202P32K7680R 1 CTS Corporation Parallel - Fundamental Quartz Crystal, 0.032768MHz Nom,
$3.82 查看
XUL516156.250000I 1 Integrated Device Technology Inc LVDS Output Clock Oscillator
$52.37 查看

相關(guān)推薦

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

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