本篇文章,來解讀《大話設(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í)策略模式的使用,以及對比策略模式與簡單工廠模式的不同。