1 會飛的鴨子
Duck 基類,含成員函數 Swim() 和 Display();派生類 MallardDuck,RedheadDuck 和 RubberDuck,各自重寫 Display()
class Duck
{ public: void Swim(); virtual void Display(); }; class MallardDuck : public Duck
{ public: void Display(); // adding virtual is OK but not necessary }; class RedheadDuck ...
class RubberDuck ...
現在要求,為鴨子增加飛的技能 -- Fly,應該如何設計呢?
1.1 繼承
考慮到並非所有的鴨子都會飛,可在 Duck 中加普通虛函數 Fly(),則“會飛”的繼承 Fly() ,“不會飛”的重寫 Fly()
void Duck::Fly() { std::cout << "I am flying !" << std::endl; } void RubberDuck::Fly() { std::cout << "I cannot fly !" << std::endl; }
1.2 接口
用普通虛函數並非良策,C++11 之 override 關鍵字 “1.2 普通虛函數” 中已經解釋。代替方法是 “純虛函數 + 缺省實現”,即將基類中的 Fly() 聲明為純虛函數,同時寫一個缺省實現
因為是純虛函數,所以只有“接口”會被繼承,而缺省的“實現”卻不會被繼承,是否調用 Fly() 的缺省實現,則取決於重寫的 Fly()
void MallardDuck::Fly() { Duck::Fly(); }
void RedheadDuck::Fly() { Duck::Fly(); }
1.3 設計模式
到目前為止,並沒有設計模式,但問題已經解決了。實際上用不用設計模式,取決於實際需求,也取決於開發者。
<Design Patterns> 中,關於策略模式的適用情景,如下所示:
1) many related classes differ only in their behavior
2) you need different variants of an algorithm
3) an algorithm uses data that clients shouldn't know about
4) a class defines many behaviors, and these appear as multiple conditional statements in its operations
顯然,鴨子的各個派生類屬於 “related classes”。關鍵就在於“飛”這個行為,如果只是將“飛”的行為,簡單划分為“會飛”和“不會飛”,則不用設計模式完全可以。
如果“飛行方式”,隨着派生類的增多,至少會有幾十種;或者視“飛行方式”為一種算法,以后還會不斷改進;再或“飛行方式”作為封裝算法,提供給第三方使用。那么此時,設計模式的價值就體現出來了 -- 易復用,易擴展,易維護。
而第 4) 種適用情景,多見於重構之中,取代一些條件選擇語句 -- "Replace Type Code with State/Strategy"
2 設計原則
在引出策略模式之前,先看面向對象的三個設計原則
1) 隔離變化:identify what varies and separate them from what stays the same
Duck 基類中, “飛行方式“是變化的,於是把 Fly() 擇出來,和剩余不變的分隔開來
2) 編程到接口:program to an interface, not an implementation
分離Fly(),將其封裝為一個接口,里面實現各種不同的“飛行方式” (一系列”算法“),添加或修改算法都在這個接口里進行。
“接口”對應於 C++ 便是抽象基類,故可將“飛行方式”封裝為 FlyBehavior 類,並在類中聲明 Fly() 為純虛函數
class FlyBehavior
{ public: virtual void Fly() = 0; }; class FlyWithWings : public FlyBehavior
{ public: virtual void Fly(); }; class FlyNoWay ...
class FlyWithRocket ...
具體實現各種不同的算法 -- “飛行方式”,如下:
void FlyWithWings::Fly() { std::cout << "I am flying !" << std::endl; } void FlyNoWay::Fly() { std::cout << "I cannot fly !" << std::endl; } void FlyWithRocket::Fly() { std::cout << "I am flying with a rocket !" << std::endl; }
3) 復合 > 繼承:favor composition (has-a) over inheritance (is-a)
公有繼承即是 “is-a”,而 Composition (復合或組合) 的含義是 “has-a”,因此,可在 Duck 基類中,聲明 FlyBehavior 型指針,如此,只需通過指針 _pfB 便可調用相應的”算法“ -- ”飛行方式“
class Duck
{
... private: FlyBehavior* fb_; // 或 std::unique_ptr<FlyBehavior> fb_; };
3 策略模式
3.1 內容
即便不懂設計模式,只要嚴格按照遵守 隔離變化 --> 編程到接口 --> 復合 三個原則,則設計思路也會和策略模式類似:
下面是策略模式的具體內容:
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Context 指向 Strategy (由指針實現);Context 通過 Strategy 接口,調用一系列算法;ConcreteStrategy 實現了一系列具體的算法
3.2 智能指針
上例中,策略模式的“接口” 對應於 FlyBehavior 類,“算法實現”分別對應派生類 FlyWithWings, FlyNoWay, FlyWithRocket,“引用”對應 fb_ 指針
為了簡化內存管理,可將 fb_ 聲明為一個“智能指針”,如此,則不需要手動實現析構函數,采用編譯器默認生成的即可。
Duck::Duck(FlyBehavior *fb)
: fb_(fb)
{}
3.3 分析
直觀上看, Duck 對應於 Context,實際上是其派生類 MallardDuck 等,通過 FlyBehavior 接口來調用各種“飛行方式”。因此,需要在各個派生類的構造函數中,初始化 fb_
MallardDuck::MallardDuck(FlyBehavior *fb)
: Duck(fb)
{}
然后,在 Duck 基類中,通過指針 fb_, 實現對 Fly() 的調用
void Duck::PerformFly() { fb_->Fly(); }
除了在構造函數中初始化 fb_ 外,還可在 Duck 類中,定義一個 SetFlyBehavior 成員函數,動態的設置“飛行方式”
void Duck::SetFlyBehavior(FlyBehavior *fb) { fb_ = fb; }
3.4 main 函數
因為 main 執行結束后,程序也就結束了,所以對於簡單程序,new 了指針后,可以不用 delete
int main () { FlyBehavior *pfWings = new FlyWithWings; FlyBehavior *pfNo = new FlyNoWay; FlyBehavior *pfRocket = new FlyWithRocket; // fly with wings Duck *pDuck = new MallardDuck(pfWings); pDuck->PerformFly(); // fly with a rocket pDuck->SetFlyBehavior(pfRocket); pDuck->PerformFly(); }
代碼鏈接: https://github.com/fengyibei/Strategy
小結
1) 面向對象的三個設計原則:隔離變化,編程到接口,復合 > 繼承
2) 策略模式主要涉及的是“一系列算法“,熟悉其適用的四種情景
參考資料
<大話設計模式> 第二章
<Head First Design Patterns> chapter 1
<Effective C++> item 32, item 38
<Design Patterns> Strategy
<Refactoring> chapter 8
Herb Sutter, GotW #91 Solution: Smart Pointer Parameters