C++ 設計模式 4:行為型模式
0 行為型模式
類或物件怎樣互動以及怎樣分配職責,這些設計模式特別關注物件之間的通訊。
1 模板模式
模板模式(Template Pattern)定義:一個抽象類公開定義了執行它的方法的方式/模板。它的子類可以按需要重寫方法實現,但呼叫將以抽象類中定義的方式進行。
意圖:定義一個操作中的演算法的骨架,而將一些步驟延遲到子類中。模板模式使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟。
1.1 模板模式中的角色和職責
AbstractClass(抽象類):在抽象類中定義了一系列基本操作(PrimitiveOperations),這些基本操作可以是具體的,也可以是抽象的,每一個基本操作對應演算法的一個步驟,在其子類中可以重定義或實現這些步驟。同時,在抽象類中實現了一個模板方法(Template Method),用於定義一個演算法的框架,模板方法不僅可以呼叫在抽象類中實現的基本方法,也可以呼叫在抽象類的子類中實現的基本方法,還可以呼叫其他物件中的方法。
ConcreteClass(具體子類):它 是抽象類的子類,用於 實現在父類中宣告的抽象基本操作以完成子類特定演算法的步驟,也可以覆蓋在父類中已經實現具體基本操作。
1.2 案例
示例程式碼:
#include <iostream> using namespace std; // AbstractClass(抽象類),製作飲料 class MakeDrink { public: //1 煮開水 void boil() { cout << "煮開水" << endl; } //2 沖泡 virtual void brew() = 0; //3 倒入杯中 void putInCup() { cout << "倒入杯中" << endl; } //4 加料 virtual void addThings() = 0; // hook 方法,決定某些演算法步驟是否掛鉤在本演算法中 virtual bool CustomWantAddThings() { return true; } // 模板方法(Template Method),製作飲料 void make() { boil(); brew(); putInCup(); if (CustomWantAddThings() == true) { addThings(); } } }; // ConcreteClass(具體子類),製作咖啡 class MakeCoffee :public MakeDrink { public: MakeCoffee(bool isAdd) { this->isAdd = isAdd; } //2 沖泡 virtual void brew() { cout << "沖泡 咖啡豆" << endl; } //4 加料 virtual void addThings() { cout << "新增 糖 和 牛奶" << endl; } virtual bool CustomWantAddThings() { return isAdd; } private: bool isAdd; }; // ConcreteClass(具體子類),沖泡茶葉 class MakeTea :public MakeDrink { public: MakeTea(bool isAdd) { this->isAdd = isAdd; } //2 沖泡 virtual void brew() { cout << "沖泡 茶葉" << endl; } //4 加一些酌料 virtual void addThings() { cout << "新增 檸檬 或者 菊花" << endl; } virtual bool CustomWantAddThings() { return isAdd; } private: bool isAdd; }; int main(void) { MakeDrink *makeCoffee = new MakeCoffee(true); makeCoffee->make(); delete makeCoffee; cout << " ------ " << endl; MakeDrink *makeTea = new MakeTea(false); makeTea->make(); delete makeTea; return 0; }
執行結果:
1.3 優缺點
**優點: **
(1)在父類中形式化地定義一個演算法,而由它的子類來實現細節的處理,在
子類實現詳細的處理演算法時並不會改變演算法中步驟的執行次序。
(2)模板方法模式是一種程式碼複用技術,它在類庫設計中尤為重要,它提取
了類庫中的公共行為,將公共行為放在父類中,而通過其子類來實現不同的行
為,它鼓勵我們恰當使用繼承來實現程式碼複用。
(3)可實現一種反向控制結構,通過子類覆蓋父類的鉤子方法來決定某一特
定步驟是否需要執行。
(4)在模板方法模式中可以通過子類來覆蓋父類的基本方法,不同的子類可
以提供基本方法的不同實現,更換和增加新的子類很方便,符合單一職責原則
和開閉原則。
缺點:
需要為每一個基本方法的不同實現提供一個子類,如果父類中可變的基本方法太多,將會導致類的個數增加,系統更加龐大,設計也更加抽象。
1.4 適用場景
(1)具有統一的操作步驟或操作過程。
(2)具有不同的操作細節。
(3)存在多個具有同樣操作步驟的應用場景,但某些具體的操作細節卻各
不相同。
2 命令模式
命令模式(Command Pattern)定義:將一個請求封裝為一個物件,並傳給呼叫物件。呼叫物件尋找可以處理該命令的合適的物件,並把該命令傳給相應的物件,該物件執行命令。
命令模式,別名為動作(Action)模式或事務(Transaction)模式。
意義:命令模式可以將請求傳送者和接收者完全解耦,傳送者與接收者之間沒有直接引用關係,傳送請求的物件只需要知道如何傳送請求,而不必知道如何完成請求。
本質:是對請求進行封裝,一個請求對應於一個命令,將發出命令的責任和執行命令的責任分割開。
2.1 命令模式中的角色和職責
Command(抽象命令類):抽象命令類一般是一個抽象類或介面,在其中聲明瞭用於執行請求的 execute()
等方法,通過這些方法可以呼叫請求接收者的相關操作。
ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中宣告的方法,它對應具體的接收者物件,將接收者物件的動作繫結其中。在實現 execute()
方法時,將呼叫接收者物件的相關操作(Action)。
Invoker(呼叫者,即請求傳送者):它通過命令物件來執行請求。一個呼叫者並不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關聯關係。在程式執行時可以將一個具體命令物件注入其中,再呼叫具體命令物件的 execute()
方法,從而實現間接呼叫請求接收者的相關操作。
Receiver(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。
2.2 案例
示例:
聯想路邊擼串燒烤場景,有烤羊肉,燒雞翅命令,有烤串師傅,和服務員。根據命令模式,設計烤串場景。
示例程式碼:
#include <iostream>
#include <list>
using namespace std;
// Receiver(接收者),烤串師傅
class Cooker
{
public:
// 烤串
void makeChuaner()
{
cout << "烤串師傅進行了烤串" << endl;
}
// 烤雞翅
void makeChicken()
{
cout << "烤串師傅進行了烤雞翅" << endl;
}
};
//Command(抽象命令類),烤串的抽象的選單
class Command
{
public:
Command(Cooker *cooker)
{
this->cooker = cooker;
}
~Command()
{
if (this->cooker != NULL)
{
delete this->cooker;
this->cooker = NULL;
}
}
// 呼叫接收者物件的相關操作
virtual void execute() = 0;
protected:
Cooker *cooker;
};
// ConcreteCommand(具體命令類),烤串的選單
class CommandChuaner :public Command
{
public:
CommandChuaner(Cooker *cooker) : Command(cooker)
{
}
virtual void execute()
{
// 命令 最終讓接收者乾的工作
this->cooker->makeChuaner();
}
};
// ConcreteCommand(具體命令類),烤雞翅的選單
class CommandChicken :public Command
{
public:
CommandChicken(Cooker * cooker) : Command(cooker)
{
}
virtual void execute()
{
// 命令 最終讓接收者乾的工作
this->cooker->makeChicken();
}
};
// Invoker(呼叫者,即請求傳送者),服務員
class Waitress
{
public:
// 給服務員 新增選單的方法
void setCmd(Command *cmd)
{
this->cmd_list.push_back(cmd);
}
// 讓服務員 下單
void notify()
{
list<Command *>::iterator it = cmd_list.begin();
for (; it != cmd_list.end(); it++)
{
(*it)->execute();
}
}
private:
list<Command *> cmd_list;
};
int main(void)
{
Waitress *mm = new Waitress;
Command *chuanger = new CommandChuaner(new Cooker);
Command *chicken = new CommandChicken(new Cooker);
// 把訂單都給服務員
mm->setCmd(chuanger);
mm->setCmd(chicken);
// 讓服務員下單,最終讓師傅幹活
mm->notify();
delete mm;
return 0;
}
執行結果:
2.3 優缺點
優點:
(1)降低系統的耦合度。由於請求者與接收者之間不存在直接引用,因此請求者與接收者之間實現完全解耦,相同的請求者可以對應不同的接收者,同樣,相同的接收者也可以供不同的請求者使用,兩者之間具有良好的獨立性。
(2)新的命令可以很容易地加入到系統中。由於增加新的具體命令類不會影響到其他類,因此增加新的具體命令類很容易,無須修改原有系統原始碼,甚至客戶類程式碼,滿足“開閉原則”的要求。
(3)可以比較容易地設計一個命令佇列或巨集命令(組合命令)。
缺點:
使用命令模式可能會導致某些系統有過多的具體命令類。因為針對每一個對請求接收者的呼叫操作都需要設計一個具體命令類,因此在某些系統中可能需要提供大量的具體命令類,這將影響命令模式的使用。
2.4 適用場景
(1)系統需要將請求呼叫者和請求接收者解耦,使得呼叫者和接收者不直接互動。請求呼叫者無須知道接收者的存在,也無須知道接收者是誰,接收者也無須關心何時被呼叫。
(2)系統需要在不同的時間指定請求、將請求排隊和執行請求。一個命令物件和請求的初始呼叫者可以有不同的生命期,換言之,最初的請求發出者可能已經不在了,而命令物件本身仍然是活動的,可以通過該命令物件去呼叫請求接收者,而無須關心請求呼叫者的存在性,可以通過請求日誌檔案等機制來具體實現。
(3)系統需要將一組操作組合在一起形成巨集命令。
3 策略模式
策略模式定義:建立表示各種策略的物件和一個行為隨著策略物件改變而改變的 context 物件。策略物件改變 context 物件的執行演算法。
意圖:定義一系列的演算法,把它們一個個封裝起來, 並且使它們可相互替換。
3.1 策略模式中的角色和職責
Context(環境類):環境類是使用演算法的角色,它在解決某個問題(即實現某個方法)時可以採用多種策略。在環境類中維持一個對抽象策略類的引用例項,用於定義所採用的策略。
Strategy(抽象策略類):它為所支援的演算法聲明瞭抽象方法,是所有策略類的父類,它可以是抽象類或具體類,也可以是介面。環境類通過抽象策略類中宣告的方法在執行時呼叫具體策略類中實現的演算法。
ConcreteStrategy(具體策略類):它實現了在抽象策略類中宣告的演算法,在執行時,具體策略類將覆蓋在環境類中定義的抽象策略類物件,使用一種具體的演算法實現某個業務處理。
3.2 案例
示例:商場促銷有策略A(0.8折) 策略B(消費滿200,返現100),用策略模式模擬場景。
示例程式碼:
#include <iostream>
using namespace std;
// Strategy(抽象策略類),銷售策略
class AbstractStrategy
{
public:
//商品具體的銷售策略計算方式
virtual double getPrice(double price) = 0;
};
// ConcreteStrategy(具體策略類),策略A :商品打八折
class StrategyA :public AbstractStrategy
{
public:
virtual double getPrice(double price)
{
return price*0.8;
}
};
//ConcreteStrategy(具體策略類),策略B:如果商品超過200,減100
class StrategyB :public AbstractStrategy
{
public:
virtual double getPrice(double price)
{
if (price > 200)
{
price = price - 100;
}
return price;
}
};
// Context(環境類),商品
class Item
{
public:
Item(string name, double price)
{
this->name = name;
this->price = price;
}
//提供一個可以更換策略的方法
void setStrategy(AbstractStrategy *strategy)
{
this->strategy = strategy;
}
//最終獲得商品的價格的方法
double SellPrice()
{
return this->strategy->getPrice(this->price);
}
private:
string name;
double price;
//銷售的策略
AbstractStrategy *strategy;
};
int main(void)
{
Item it("nike鞋", 201);
AbstractStrategy *sA = new StrategyA;
AbstractStrategy *sB = new StrategyB;
cout << "上午 商場執行 銷售策略A,全場 八折" << endl;
it.setStrategy(sA);
cout << "nike鞋 應該賣" << it.SellPrice() << endl;
cout << "-----" << endl;
cout << "下午 商場執行 銷售策略B,全場 超 200 減 100" << endl;
it.setStrategy(sB);
cout << "nike鞋 應該賣" << it.SellPrice() << endl;
return 0;
}
執行結果:
3.3 優缺點
優點:
(1)策略模式提供了對“開閉原則”的完美支援,**使用者可以在不修改原有系統的基礎上選擇演算法或行為,也可以靈活地增加新的演算法或行為。 **
(2)使用策略模式可以避免多重條件選擇語句。多重條件選擇語句不易維護,它把採取哪一種演算法或行為的邏輯與演算法或行為本身的實現邏輯混合在一起,將它們全部硬編碼(Hard Coding)在一個龐大的多重條件選擇語句中,比直接繼承環境類的辦法還要原始和落後。
(3)策略模式提供了一種演算法的複用機制。由於將演算法單獨提取出來封裝在策略類中,因此不同的環境類可以方便地複用這些策略類。
缺點:
(1)客戶端必須知道所有的策略類,並自行決定使用哪一個策略類。這就意味著客戶端必須理解這些演算法的區別,以便適時選擇恰當的演算法。換言之,策略模式只適用於客戶端知道所有的演算法或行為的情況。
(2)策略模式將造成系統產生很多具體策略類,任何細小的變化都將導致系統要增加一個新的具體策略類。
3.4 適用場景
準備一組演算法,並將每一個演算法封裝起來,使得它們可以互換。
4 觀察者模式
觀察者模式是用於建立一種物件與物件之間的依賴關係,一個物件發生改變時將自動通知其他物件,其他物件將相應作出反應。
在觀察者模式中,發生改變的物件稱為觀察目標,而被通知的物件稱為觀察者,一個觀察目標可以對應多個觀察者,而且這些觀察者之間可以沒有任何相互聯絡,可以根據需要增加和刪除觀察者,使得系統更易於擴充套件。
4.1 命令模式中的角色和職責
Subject(抽象被觀察者):被觀察的物件。當需要被觀察的狀態發生變化時,需要通知佇列中所有觀察者物件。Subject需要維持(新增,刪除,通知)一個觀察者物件的佇列列表。
ConcreteSubject(具體被觀察者):被觀察者的具體實現。包含一些基本的屬性狀態及其他操作。
Observer(抽象觀察者):介面或抽象類。當 Subject 的狀態發生變化時,Observer 物件將通過一個 callback 函式得到通知。
ConcreteObserver(具體觀察者):觀察者的具體實現。得到通知後將完成一些具體的業務邏輯處理。
4.2 案例
示例程式碼:
#include <iostream>
#include <string>
#include <list>
using namespace std;
// Observer(抽象觀察者)
class Listenner
{
public:
// 老師來了,應該做的動作
virtual void onTeacherComming() = 0;
virtual void doBadThing() = 0;
virtual void stopBadThing() = 0;
};
// 抽象被觀察者
class Notifier
{
public:
// 新增觀察者的方法
virtual void addListenner(Listenner *listenner) = 0;
// 刪除觀察者的方法
virtual void delListenner(Listenner *listenner) = 0;
// 通知所有觀察者的方法
virtual void notify() = 0;
};
// ConcreteObserver(具體觀察者),學生
class Student : public Listenner
{
public:
Student(string name, string badthing)
{
this->name = name;
this->badthing = badthing;
}
// 老師來了學生該怎麼辦
virtual void onTeacherComming()
{
stopBadThing();
}
virtual void doBadThing()
{
cout << "學生 " << name << "目前正在 " << badthing << endl;
}
virtual void stopBadThing()
{
cout << "學生 " << name << "發現班長給我使眼神了,停止 " << badthing << endl;
}
private:
string name;
string badthing;
};
// ConcreteSubject(具體被觀察者),班長
class Monitor : public Notifier
{
public:
// 新增觀察者的方法
virtual void addListenner(Listenner *listenner)
{
this->l_list.push_back(listenner);
}
// 刪除觀察者的方法
virtual void delListenner(Listenner *listenner)
{
this->l_list.remove(listenner);
}
// 通知所有觀察者的方法
// 班長使眼神的方法
virtual void notify()
{
// 廣播資訊,讓每一個學生都執行各自的重寫的 onTeacherComming 方法
for (list<Listenner *>::iterator it = l_list.begin(); it != l_list.end(); it++)
{
(*it)->onTeacherComming();
}
}
private:
list<Listenner *> l_list; //班長手中所有的學生(觀察者)
};
int main(void)
{
Student zhangsan("張三", "抄作業");
Student lisi("李四", "打lol");
Monitor monitor;
// 將所有的學生列表告知具體被觀察者,好讓具體被觀察者進行通知
monitor.addListenner(&zhangsan);
monitor.addListenner(&lisi);
cout << "教室一片和諧,老師沒有來 " << endl;
zhangsan.doBadThing();
lisi.doBadThing();
cout << "班長突然發現老師來了,給學生們使了一個眼神" << endl;
monitor.notify();
return 0;
}
執行結果:
4.3 優缺點
優點:
(1)觀察者模式可以實現表示層和資料邏輯層的分離,定義了穩定的訊息更新傳遞機制,並抽象了更新介面,使得可以有各種各樣不同的表示層充當具體觀察者角色。
(2)觀察者模式在觀察目標和觀察者之間建立一個抽象的耦合。觀察目標只需要維持一個抽象觀察者的集合,無須瞭解其具體觀察者。由於觀察目標和觀察者沒有緊密地耦合在一起,因此它們可以屬於不同的抽象化層次。
(3)觀察者模式支援廣播通訊,觀察目標會向所有已註冊的觀察者物件傳送通知,簡化了一對多系統設計的難度。
(4)觀察者模式滿足“開閉原則”的要求,增加新的具體觀察者無須修改原有系統程式碼,在具體觀察者與觀察目標之間不存在關聯關係的情況下,增加新的觀察目標也很方便。
缺點:
(1)如果一個觀察目標物件有很多直接和間接觀察者,將所有的觀察者都通知到會花費很多時間。
(2)**如果在觀察者和觀察目標之間存在迴圈依賴,觀察目標會觸發它們之間進行迴圈呼叫,可能導致系統崩潰。 **
(3)觀察者模式沒有相應的機制讓觀察者知道所觀察的目標物件是怎麼發生變化的,而僅僅只是知道觀察目標發生了變化。
4.4 適用場景
(1)一個抽象模型有兩個方面,其中一個方面依賴於另一個方面,將這兩個方面封裝在獨立的物件中使它們可以各自獨立地改變和複用。
(2)一個物件的改變將導致一個或多個其他物件也發生改變,而並不知道具體有多少物件將發生改變,也不知道這些物件是誰。
(3)需要在系統中建立一個觸發鏈,A物件的行為將影響B物件,B物件的行為將影響C物件……,可以使用觀察者模式建立一種鏈式觸發機制。