本篇文章,來(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)買(mǎi)商品的單價(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ú)再寫(xiě)一套計(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)的索引值,目前代碼中寫(xiě)了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)單工廠模式的不同。