目錄
- 1 模板方法
- 1.1 模板方法基本概念
- 1.2 實驗
- 1.2.1 未使用模板方法實現代碼
- 1.2.2 使用模板方法的代碼
- 2 觀察者模式
- 2.1 觀察者模式基本概念
- 2.2 實驗
- 3 策略模式
- 3.1 策略模式基本概念
- 3.2 實驗
1 模板方法
1.1 模板方法基本概念
- 定義:一個操作中的算法的骨架 ,而將一些步驟延遲到子類中。 Template Method使得子類可以不
改變一個算法的結構即可重定義該算法的某些特定步驟。 - 要點:
- 最常用的設計模式,子類可以復寫父類子流程,使父類的骨架流程豐富
- 父類 protected 保護子類需要復寫的子流程;這樣子類的子流程只能父類來調用
1.2 實驗
背景
某個品牌動物園,有一套固定的表演流程,但是其中有若干個表演子流程可創新替換,以嘗試迭代更新表演流程;
1.2.1 未使用模板方法實現代碼
#include <iostream>
using namespace std;#if 0
class ZooShow {
public:void Show0() {cout << "show0" << endl;}void Show2() {cout << "show2" << endl;}
};class ZooShowEx {
public:void Show1() {cout << "show1" << endl;}void Show3() {cout << "show3" << endl;}
};// 不滿足單一職責 , 開 擴展 修改閉原則
// 動物園固定流程,迭代創新
// 穩定和變化 一定的方向上變化
#else if 2
class ZooShow {
public:ZooShow(int type = 1) : _type(type) {}public:void Show() {if (Show0())PlayGame(); // 里氏替換Show1();Show2();Show3();}// 接口隔離 不要讓用戶去選擇它們不需要的接口
private:void PlayGame() {cout << "after Show0, then play game" << endl;}private:bool Show0() {cout << _type << " show0" << endl;return true;}void Show1() {if (_type == 1) {cout << _type << " Show1" << endl;} else if (_type == 2) {cout << _type << " Show1" << endl;} else if (_type == 3) {}}void Show2() {if (_type == 20) {}cout << "base Show2" << endl;}void Show3() {if (_type == 1) {cout << _type << " Show1" << endl;} else if (_type == 2) {cout << _type << " Show1" << endl;}}
private:int _type;
};#endifint main () {
#if 0ZooShow *zs = new ZooShow;ZooShowEx *zs1 = new ZooShowEx;zs->Show0();zs1->Show1();zs->Show2();zs1->Show3();
#else if 2ZooShow *zs = new ZooShow(1);zs->Show();
#endifreturn 0;
}
分析:
- 1.缺乏可擴展性
在當前代碼里,ZooShow 類的 Show 方法內包含了很多 if - else 條件判斷,用來依據不同的 _type 值執行不同的邏輯。要是需要新增一種表演類型,就得修改 Show 方法,添加新的 if - else 分支。這違背了開閉原則(對擴展開放,對修改關閉),代碼的可擴展性較差。例如,如果要增加 _type == 4 的表演流程,就得在各個方法里添加對應的判斷邏輯。 - 2.代碼復用性低
每個表演類型的邏輯都被硬編碼在 ZooShow 類的各個方法中,不同表演類型之間的公共邏輯沒有得到很好的復用。如果某些表演類型有相似的流程,當前代碼無法有效復用這些公共部分,會造成代碼冗余。 - 3.違反單一職責原則
ZooShow 類承擔了過多的職責,它不僅定義了表演的流程,還包含了每種表演類型的具體邏輯**。當表演類型增多或者表演流程發生變化時,這個類會變得越來越復雜,難以維護**。 - 4.可讀性和可維護性差
大量的 if - else 條件判斷使得代碼的邏輯變得復雜,降低了代碼的可讀性。而且,當需要修改或添加新的表演類型時,需要在多個方法中查找和修改相關邏輯,增加了維護的難度。
1.2.2 使用模板方法的代碼
學習設計模式 我們可以通過這些 要點
- 定義:定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。Template Method 使得子類可以不改變一個算法的結構即可重定義該算法。
- 解決的問題
確定點:算法骨架
變化點:子流程需要變化 - 代碼結構
基類中存骨架流程接口
所有子流程對子類開放并且是虛函數
多態使用方式 - 符合哪些設計原則
單一職責
開閉
依賴倒置:子類擴展時,需要依賴基類的虛函數實現;使用者只依賴接口
封裝變化點:protected
接口隔離
最小知道原則 - 如何擴展
實現子類繼承基類,重寫子流程
通過多態調用方式使用
#include <iostream>
using namespace std;// 抽象基類,定義表演流程的模板
class ZooShowTemplate {
public:// 模板方法,定義表演的整體流程void Show() {if (Show0())PlayGame();Show1();Show2();Show3();}virtual ~ZooShowTemplate() {}protected:// 具體步驟的虛函數,可在子類中重寫virtual bool Show0() {cout << "show0" << endl;return true;}virtual void PlayGame() {cout << "after Show0, then play game" << endl;}virtual void Show1() = 0;virtual void Show2() = 0;virtual void Show3() = 0;
};// 具體表演類型 1
class ZooShowType1 : public ZooShowTemplate {
protected:void Show1() override {cout << "1 Show1" << endl;}void Show2() override {cout << "base Show2" << endl;}void Show3() override {cout << "1 Show3" << endl;}
};// 具體表演類型 2
class ZooShowType2 : public ZooShowTemplate {
protected:void Show1() override {cout << "2 Show1" << endl;}void Show2() override {cout << "base Show2" << endl;}void Show3() override {cout << "2 Show3" << endl;}
};int main() {ZooShowTemplate* zs1 = new ZooShowType1();zs1->Show();ZooShowTemplate* zs2 = new ZooShowType2();zs2->Show();delete zs1;delete zs2;return 0;
}
解決的問題
- 確定點(算法骨架):代碼里 ZooShowTemplate 類的 Show 方法確定了表演流程這個算法骨架,固定了表演的整體執行順序。
- 變化點(子流程需要變化):不同類型的表演,如 ZooShowType1 和 ZooShowType2,它們的 Show1、Show2、Show3 具體實現是不同的,這就是子流程的變化點,通過繼承基類并重寫相應虛函數來實現不同的子流程
代碼結構
- 基類中存骨架流程接口:ZooShowTemplate 類中定義的 Show 方法就是骨架流程接口,它包含了整個表演流程的邏輯。
- 所有子流程對子類開放并且是虛函數:Show0、PlayGame、Show1、Show2、Show3 這些子流程方法在 ZooShowTemplate 類中都被定義為虛函數,方便子類重寫,實現不同的子流程邏輯。
- 多態使用方式:在 main 函數中,通過基類指針 ZooShowTemplate* 分別指向 ZooShowType1 和 ZooShowType2 的對象,然后調用 Show 方法,運行時根據實際指向的子類對象調用相應子類重寫的方法,體現了多態性。
符合的設計原則
- 單一職責:ZooShowTemplate 類負責定義表演流程骨架,各個子類負責具體表演類型的子流程實現,職責劃分清晰 ,每個類只專注于自己的職責。
- 開閉:當需要新增一種表演類型時,如 ZooShowType3,只需創建一個新類繼承 ZooShowTemplate 并重寫相關虛函數,不需要修改 ZooShowTemplate 類的代碼,對擴展開放,對修改關閉。
- 依賴倒置:子類擴展時依賴基類的虛函數實現,比如 ZooShowType1 和 ZooShowType2 依賴 ZooShowTemplate 中定義的虛函數;使用者(main 函數)只依賴 ZooShowTemplate 這個抽象基類接口,而不是具體子類,降低了耦合度。
- 封裝變化點:在 ZooShowTemplate 類中,將一些可能變化的子流程方法(如 Show1、Show2 等)定義為虛函數,并且訪問修飾符為 protected,對外部隱藏了具體實現細節,同時方便子類重寫來實現變化。
- 接口隔離:雖然代碼中未明顯體現接口隔離的典型場景,但從某種程度上,每個子類只實現自己需要的虛函數,沒有被迫依賴不需要的接口方法。
- 最小知道原則:子類只需要了解與自己相關的基類虛函數,不需要了解基類中其他不必要的實現細節,減少了類之間的信息交互。
如何擴展
- 實現子類繼承基類,重寫子流程:如代碼中 ZooShowType1 和 ZooShowType2 繼承自 ZooShowTemplate,并重寫了 Show1、Show2、Show3 等子流程方法,實現不同表演類型的定制。
- 通過多態調用方式使用:在 main 函數中,通過基類指針調用 Show 方法,利用多態性根據實際子類對象調用相應的重寫方法,實現不同表演類型的流程執行。
2 觀察者模式
2.1 觀察者模式基本概念
- 定義:對象間的一種一對多(變化)的依賴關系,以便當一個對象(Subject)的狀態發生改變時,所有依賴于它的對象都得到通知并自動更新
- 特性
- 松耦合:主題和觀察者相互獨立,主題無需知道觀察者具體細節,只知其實現了觀察者接口 。比如電商系統中商品信息(主題)變化,不同的展示模塊(觀察者)可獨立更新,互不干擾。
- 動態性:觀察者可隨時添加或移除,不影響系統其他部分 。例如新聞推送系統,新用戶(觀察者)可隨時訂閱(添加)或退訂(移除)頻道(主題)。
2.2 實驗
背景:
氣象站發布氣象資料給數據中心,數據中心經過處理,將氣象信息更新到兩個不同的顯示終端(A和B)
實現偽代碼
#include <vector>//
class IDisplay {
public:virtual void Show(float temperature) = 0;virtual ~IDisplay() {}
};class DisplayA : public IDisplay {
public:virtual void Show(float temperature);
private:void jianyi();
};class DisplayB : public IDisplay{
public:virtual void Show(float temperature);
};class DisplayC : public IDisplay{
public:virtual void Show(float temperature);
};class WeatherData {
};class DataCenter {
public:void Attach(IDisplay * ob);void Detach(IDisplay * ob);void Notify() {float temper = CalcTemperature();for (auto iter = obs.begin(); iter != obs.end(); iter++) {(*iter)->Show(temper);}}// 接口隔離
private:virtual WeatherData * GetWeatherData();virtual float CalcTemperature() {WeatherData * data = GetWeatherData();// ...float temper/* = */;return temper;}std::vector<IDisplay*> obs;
};int main() {DataCenter *center = new DataCenter;IDisplay *da = new DisplayA();IDisplay *db = new DisplayB();IDisplay *dc = new DisplayC();center->Attach(da);center->Attach(db);center->Attach(dc);center->Notify();//-----center->Detach(db);center->Notify();return 0;
}
定義與解決的問題
- 定義體現:觀察者模式定義對象間一對多依賴關系,代碼中 DataCenter 類相當于主題(被觀察者),IDisplay 及其派生類**(DisplayA、DisplayB、DisplayC )相當于觀察者** 。DataCenter 維護著多個 IDisplay 指針(觀察者),當溫度數據計算出來(主題狀態變化)時,通知所有注冊的觀察者,符合一對多依賴關系的定義。
- 穩定點與變化點
- 穩定點:“一” 對應的是 DataCenter 類,它是主題,在系統中相對穩定,負責管理觀察者和通知邏輯。
- 變化點:“多” 對應的是 IDisplay 的不同派生類實例,如 DisplayA、DisplayB、DisplayC ,可以隨時增加新的顯示類(“多” 增加 ),或者移除已有的顯示類(“多” 減少 ),比如在 main 函數中通過 Attach 和 Detach 方法進行添加和移除操作。
代碼結構
-代碼定義了主題類 DataCenter ,通過 std::vector<IDisplay*> 來存儲觀察者。觀察者通過抽象接口 IDisplay 定義,具體觀察者類 DisplayA、DisplayB、DisplayC 實現該接口。DataCenter 有 Attach、Detach 方法管理觀察者,Notify 方法用于通知觀察者。結構上滿足觀察者模式中主題與觀察者的基本組織形式。
符合的設計原則
- 面向接口編程:代碼中定義了抽象接口 IDisplay ,DataCenter 類依賴 IDisplay 接口來管理觀察者,而不是具體的觀察者類(如 DisplayA、DisplayB 等 )。例如 DataCenter 的 Attach 方法接收 IDisplay * 類型參數,體現了面向接口編程,降低了耦合度。
- 接口隔離:IDisplay 接口只定義了 Show 方法,每個具體的顯示類只需要實現這個與自身顯示功能相關的方法,沒有被迫實現不必要的接口方法。并且 DataCenter 類內部也有一些方法(如 GetWeatherData 等 )相對隔離,符合接口隔離原則。
- 封裝變化點
attach:在代碼中對應 Attach 方法,用于將觀察者(IDisplay 的派生類實例 )添加到 DataCenter 的觀察者列表中,封裝了增加觀察者這個變化點。
detach:對應 Detach 方法,用于從觀察者列表中移除觀察者,封裝了減少觀察者這個變化點。
如何擴展
若要擴展系統,比如增加新的顯示方式(新的觀察者 ),可以創建一個新的類繼承自 IDisplay ,實現 Show 方法,然后在 main 函數中通過 DataCenter 的 Attach 方法將其添加到觀察者列表中,就能參與到溫度數據的顯示通知流程中。
3 策略模式
3.1 策略模式基本概念
定義與核心思想
- 定義一系列算法:把相關算法進行歸納整理,形成一個算法集合。比如電商系統中計算商品折扣,有固定折扣、滿減折扣、會員專屬折扣等不同算法 。
- 封裝每個算法:將每個算法獨立封裝在對應的策略類中。以支付場景為例,支付寶支付、微信支付、銀行卡支付等,每種支付方式的實現細節都封裝在各自的策略類里,外部無需了解具體支付流程(如網絡請求、加密處理等 )。
- 算法可相互替換:在運行時,可根據實際情況靈活切換不同策略,而不影響使用算法的客戶端。例如地圖導航,用戶可在 “最短距離”“最快速度”“避開擁堵” 等不同路徑規劃策略間切換 。
組成部分 - 策略接口(抽象策略角色):定義算法的公共接口,規定算法應具備的方法簽名。比如定義一個計算稅費的策略接口,其中包含計算稅費的抽象方法 calculateTax() 。
- 具體策略類:實現策略接口,提供具體算法的實現。如上述計算稅費的場景,有針對不同地區稅率計算稅費的具體策略類,像 “北京地區稅費計算類”“上海地區稅費計算類” 等,分別實現 calculateTax() 方法。
- 上下文類:持有策略接口的引用,負責在合適時機調用策略對象的算法。例如電商系統中結算模塊作為上下文類,它持有稅費計算策略接口的引用,在結算時調用具體策略類(如根據收貨地區選擇對應地區的稅費計算策略 )來計算稅費。
3.2 實驗
背景:某商場節假日有固定促銷活動,為了加大促銷力度,現提升國慶節促銷活動規格
實現
class Context {};class ProStategy {
public:virtual double CalcPro(const Context &ctx) = 0;virtual ~ProStategy();
};
// cpp
class VAC_Spring : public ProStategy {
public:virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_QiXi : public ProStategy {
public:virtual double CalcPro(const Context &ctx){}
};
class VAC_QiXi1 : public VAC_QiXi {
public:virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_Wuyi : public ProStategy {
public:virtual double CalcPro(const Context &ctx){}
};
// cpp
class VAC_GuoQing : public ProStategy {
public:virtual double CalcPro(const Context &ctx){}
};class VAC_Shengdan : public ProStategy {
public:virtual double CalcPro(const Context &ctx){}
};class Promotion {
public:Promotion(ProStategy *sss) : s(sss){}~Promotion(){}double CalcPromotion(const Context &ctx){return s->CalcPro(ctx);}
private:ProStategy *s;
};int main () {Context ctx;ProStategy *s = new VAC_QiXi1();Promotion *p = new Promotion(s);p->CalcPromotion(ctx);return 0;
}
解決的問題
- 穩定點:在該商場促銷場景中,“客戶端與算法的調用關系” 是穩定點。從代碼看,Promotion 類(客戶端)通過構造函數接收 ProStategy 指針(算法相關),并在 CalcPromotion 方法中固定地調用 s->CalcPro(ctx) 來執行促銷算法,這種調用關系相對穩定。
- 變化點
- 新加算法:當商場要增加新的促銷活動時,比如后續可能新增某個節日的促銷策略,就需要添加新的具體策略類。像代碼中如果要新增 “元旦促銷策略”,可以創建新類繼承自 ProStategy 并實現 CalcPro 方法。
- 算法內容改變:已有的促銷策略(如 VAC_QiXi 等類 ),其內部的促銷計算邏輯(CalcPro 方法的實現 )可能會根據商場需求進行調整和改變。
代碼結構
定義了抽象策略類 ProStategy ,其中聲明了純虛函數 CalcPro ,用于定義促銷算法的接口。一系列具體策略類(VAC_Spring、VAC_QiXi 等 )繼承自 ProStategy ,并實現 CalcPro 方法,各自代表不同節日的促銷算法。Context 類目前為空,但在策略模式中通常用于存儲上下文相關信息,供策略類使用。
Promotion 類作為上下文類,持有 ProStategy 指針,通過構造函數進行依賴注入(接收具體的策略對象 ),并在 CalcPromotion 方法中調用策略對象的 CalcPro 方法來執行促銷計算。
設計原則
- 接口隔離
- 依賴注入:Promotion 類通過構造函數 Promotion(ProStategy *sss) 將具體的促銷策略對象注入進來,實現了類與具體策略的解耦。比如在 main 函數中可以靈活地將不同的 ProStategy 子類對象(VAC_QiXi1 等 )注入到 Promotion 中。
- 解決通過一個接口解決兩個類的依賴:ProStategy 接口將 Promotion 類和具體的促銷策略類(如 VAC_QiXi 等 )解耦,使得 Promotion 類只依賴于抽象接口,而不依賴具體策略類的實現細節,解決了它們之間的依賴問題。
- 面向接口編程:整個代碼圍繞 ProStategy 接口展開,Promotion 類依賴 ProStategy 接口,而不是具體的策略類。在 main 函數中也是通過 ProStategy 指針來操作具體的策略對象,體現了面向接口編程,降低了代碼間的耦合度。
- 開閉原則:當需要新增促銷策略(如新增節日促銷 )時,只需創建新的類繼承 ProStategy 并實現 CalcPro 方法,無需修改現有的 Promotion 類以及其他已有的策略類代碼,對擴展開放,對修改關閉。
其實還有一個組合優于繼承的原則
如何擴展代碼
若要擴展代碼,比如增加新的促銷策略,只需創建一個新的類繼承自 ProStategy ,并實現 CalcPro 方法。然后在 main 函數中,創建該新策略類的對象,并將其注入到 Promotion 類中,即可使用新的促銷策略。例如新增 “中秋促銷策略”,可以編寫一個新類繼承 ProStategy ,實現 CalcPro 方法來定義中秋促銷的計算邏輯,然后在 main 函數中按現有方式使用這個新策略。
怎么調用
ProStategy *s = new VAC_QiXi1();Promotion *p = new Promotion(s);p->CalcPromotion(ctx);
多態調用