學習軟件設計,向OO高手邁進!
設計模式(Design pattern)是軟件開發人員在軟件開發過程中面臨的一般問題的解決方案。
這些解決方案是衆多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的。
是前輩大神們留下的軟件設計的"招式"或是"套路"。
什麼是模板方法模式
在本文末尾會給出解釋,待耐心看完demo再看定義,相信你會有更深刻的印象
實例講解
背景
假設我們需要爲客戶開發一款沖泡飲料的應用程序,客戶的要求如下:
Version 1.0
寫兩個類,一個 Coffee 類,一個 Tea 類,很簡單!
Coffee 類
class Coffee {
public:
// 準備飲料
void PrepareBeverage(void) {
BoilWater();
BrewCoffee();
PourInCup();
AddSugarAndMilk();
}
// 燒開水
void BoilWater(void) {
printf("Boiling water\n");
}
// 沖泡咖啡
void BrewCoffee(void) {
printf("Dripping coffee through filter\n"); // 滴濾式咖啡
}
// 倒進杯子
void PourInCup(void) {
printf("Pouring into cup\n");
}
// 加糖和牛奶
void AddSugarAndMilk(void) {
printf("Adding sugar and milk\n");
}
};
Tea 類
class Tea {
public:
// 準備飲料
void PrepareBeverage(void) {
BoilWater();
SteepTea();
PourInCup();
AddLemon();
}
// 燒開水
void BoilWater(void) {
printf("Boiling water\n");
}
// 浸泡茶
void SteepTea(void) {
printf("steeping the tea\n");
}
// 倒進杯子
void PourInCup(void) {
printf("Pouring into cup\n");
}
// 加檸檬
void AddLemon(void) {
printf("Adding lemon\n");
}
};
等等,我發現有重複的代碼,這表示我們需要清理一下設計了
把共同的部分抽取出來,放到基類中
Version 1.1
類圖
PrepareBeverage() 方法在每個子類中都不一樣,所以定義成抽象方法,然後每個子類都實現自己的沖泡方法。
抽象類
class CaffeineBeverage {
public:
// 準備飲料, 設置爲純虛函數, 讓子類實現
virtual void PrepareBeverage(void) = 0;
// 燒開水
virtual void BoilWater(void) {
printf("Boiling water\n");
}
// 倒進杯子
virtual void PourInCup(void) {
printf("Pouring into cup\n");
}
};
Coffee 類
class Coffee : public CaffeineBeverage {
public:
// 準備飲料
virtual void PrepareBeverage(void) {
BoilWater();
BrewCoffee();
PourInCup();
AddSugarAndMilk();
}
// 沖泡咖啡
virtual void BrewCoffee(void) {
printf("Dripping coffee through filter\n"); // 滴濾式咖啡
}
// 加糖和牛奶
virtual void AddSugarAndMilk(void) {
printf("Adding sugar and milk\n");
}
};
Tea 類
class Tea : public CaffeineBeverage {
public:
// 準備飲料
virtual void PrepareBeverage(void) {
BoilWater();
SteepTea();
PourInCup();
AddLemon();
}
// 浸泡茶
virtual void SteepTea(void) {
printf("steeping the tea\n");
}
// 加檸檬
virtual void AddLemon(void) {
printf("Adding lemon\n");
}
};
好吧,再看一眼,咖啡和茶是不是還有什麼共同點呢?
BrewCoffee() 和 SteepTea() 可以概括爲用開水泡咖啡和茶,就叫 Brew() 方法吧
AddSugarAndMilk() 和 AddLemon() 可以概括爲在飲料內加入適當的調料,就叫 AddCondiments() 好了
Version 1.2
類圖
抽象類
class CaffeineBeverage {
public:
// 準備飲料, C++ 11 纔有 final 關鍵字
virtual void PrepareBeverage(void) final {
BoilWater();
Brew();
PourInCup();
AddCondiments();
}
// 燒開水
virtual void BoilWater(void) {
printf("Boiling water\n");
}
// 倒進杯子
virtual void PourInCup(void) {
printf("Pouring into cup\n");
}
// 沖泡
virtual void Brew(void) = 0;
// 添加調料
virtual void AddCondiments(void) = 0;
};
Coffee 類
class Coffee : public CaffeineBeverage {
public:
// 沖泡咖啡
virtual void Brew(void) {
printf("Dripping coffee through filter\n"); // 滴濾式咖啡
}
// 加糖和牛奶
virtual void AddCondiments(void) {
printf("Adding sugar and milk\n");
}
};
Tea 類
class Tea : public CaffeineBeverage {
public:
// 浸泡茶
virtual void Brew(void) {
printf("steeping the tea\n");
}
// 加檸檬
virtual void AddCondiments(void) {
printf("Adding lemon\n");
}
};
總結一下我們做了什麼?
我們泛化了 PrepareBeverage(),把它放在基類。其中 Brew() 和 AddCondiments() 這兩個步驟依賴子類進行
CaffeineBeverage 這個類,瞭解和控制沖泡方法的步驟,親自執行 BoilWater() 和 PourInCup() 步驟,其餘步驟由子類完成
模板方法模式定義
沒錯,我們剛剛實現的就是模板方法模式。PrepareBeverage() 就是模板方法,爲什麼?
- 它畢竟是一個方法
- 它用作一個算法的模板(在本例中,算法是用來製作咖啡因飲料的)
模板方法定義了一個算法的步驟,並允許子類爲一個或多個步驟提供實現
CaffeineBeverage 類主導一切,它擁有算法,而且保護這個算法(PrepareBeverage() 加了 final 參數,子類無法修改)
CaffeineBeverage 類專注在算法本身,而由子類提供完整的實現
這個模板方法提供了一個框架,可以讓其它的咖啡因飲料插進來。新的咖啡因飲料只需實現自己的方法就可以了
在面向對象程序設計過程中,程序員常常會遇到這種情況:設計一個系統時知道了算法所需的關鍵步驟,而且確定了這些步驟的執行順序,但某些步驟的具體實現還未知,或者說某些步驟的實現與具體的環境相關
例如,去銀行辦理業務一般要經過4個流程:取號、排隊、辦理具體業務、對銀行工作人員進行評分等,其中取號、排隊和對銀行工作人員進行評分的業務對每個客戶是一樣的,可以在父類中實現,但是辦理具體業務卻因人而異,它可能是存款、取款或者轉賬等,可以延遲到子類中實現
定義
模板方法模式在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結構的情況下,重新定義算法中的某些步驟
類圖
稍等,HookMethod() 又是什麼?
它是定義在基類的具體方法,它可以什麼都不用做,我們稱之爲鉤子。子類可以視情況來決定要不要覆蓋它
且來看一個例子:我們希望喝到一杯純 coffee,不希望加糖和牛奶,那麼將如何實現個性化擴展呢?
Version 1.3
抽象類,添加鉤子方法
class CaffeineBeverage {
public:
// 準備飲料, C++ 11 纔有 final 關鍵字
virtual void PrepareBeverage(void) final {
BoilWater();
Brew();
PourInCup();
if(CustomerWantsCondiments()) {
AddCondiments();
}
}
......
// 鉤子方法
virtual bool CustomerWantsCondiments(void) {
return true;
}
};
在子類中使用鉤子,根據顧客需求來決定要不要加糖和牛奶
Coffee 類
class Coffee : public CaffeineBeverage {
public:
......
// 重寫鉤子方法
virtual bool CustomerWantsCondiments(void) {
string strAnswer = GetUserInput();
string::const_iterator it = strAnswer.begin();
if(*it == 'y') {
return true;
}
return false;
}
virtual string GetUserInput(void) {
string strAnswer;
printf("Would you like milk and sugar with your coffee (y/n)? ");
cin >> strAnswer;
if(strAnswer.empty()) {
return "no";
}
// 轉小寫
transform(strAnswer.begin(), strAnswer.end(), strAnswer.begin(), ::tolower);
return strAnswer;
}
};
Tea 類,不修改
main 函數
int main(int argc, char *argv[])
{
CaffeineBeverage *objCoffee = new Coffee();
CaffeineBeverage *objTea = new Tea();
objCoffee->PrepareBeverage();
printf("\n");
objTea->PrepareBeverage();
return 0;
}
運行結果
Boiling water
Dripping coffee through filter
Pouring into cup
Would you like milk and sugar with your coffee (y/n)? yes
Adding sugar and milk
Boiling water
steeping the tea
Pouring into cup
Adding lemon
確實很酷,鉤子竟然能作爲條件控制,影響抽象類中的算法流程!
當你的子類必須提供算法中某個方法或步驟的實現時,就使用抽象方法
如果算法的這個部分是可選的,就用鉤子。子類可以選擇實現這個鉤子,也可以不實現
模板方法模式的優缺點
無論哪種模式都有其優缺點,當然我們每次在編寫代碼的時候需要考慮下其利弊
模板方法模式的優點:
- 封裝性好,屏蔽細節。它封裝了不變部分,擴展了可變部分,它把認爲是不變部分的算法封裝到父類中實現,而把可變部分的算法由子類繼承實現,便於子類繼續擴展
- 複用性好,便於維護。它在父類中提取了公共的部分代碼,便於代碼複用
- 部分方法是由子類實現的,因此子類可以通過擴展方式增加相應的功能,符合開閉原則
模板方法模式的缺點:
-
對每個不同的實現都需要定義一個子類,這會導致類的個數增加,系統更加龐大
-
父類中的抽象方法由子類實現,子類執行的結果會影響父類的結果,這導致一種反向的控制結構,它提高了代碼閱讀的難度
總結
模版方法模式中的方法
模板方法
模板方法是定義在抽象類中的,把基本操作方法組合在一起形成一個總算法或一個總行爲
一個抽象類可以有任意多個模板方法,而不限於一個。每一個模板方法都可以調用任意多個具體方法
抽象方法
抽象方法由抽象類聲明,由具體子類實現。在 Java 語言裏抽象方法以 abstract 關鍵字標示。它是子類必須實現的方法
具體方法
具體方法由抽象類聲明並實現,而子類一般並不實現或重寫。它是子類可以選擇實現或不實現的方法
鉤子方法
鉤子方法由抽象類聲明並實現,而子類會加以擴展。它是子類可以選擇實現或不實現的方法
應用場景
- 算法的整體步驟很固定,但其中個別部分易變時,這時候可以使用模板方法模式,將容易變的部分抽象出來,供子類實現
- 當多個子類存在公共的行爲時,可以將其提取出來並集中到一個公共父類中以避免代碼重複
- 當需要控制子類的擴展時,模板方法只在特定點調用鉤子操作,這樣就只允許在這些點進行擴展
注意:爲防止惡意操作,一般模板方法都加上 final 關鍵詞,C++ 11 提供了該關鍵字
參考資料
Head+First設計模式(中文版).pdf