現在我們得讓鴨子能飛è 在Duck類中加上fly()方法,然後所有鴨子都會繼承fly()。
但是,可怕的問題發生了……
“橡皮鴨子”在屏幕上飛來飛去,這是你在開玩笑嗎?
把“橡皮鴨子”當成一種“特色”,
覆蓋父類的fly()方法
可是,如果以後我加入誘餌鴨(DecoyDuck) --> 木頭假鴨,不會飛也不會叫……
利用繼承來提供Duck的行爲,會導致如下行爲
代碼在多個子類中重複。
qB. 運行時的行爲不容易改變。
qC. 我們不能讓鴨子跳舞。
qD. 很難知道所有鴨子的全部行爲。
qE. 鴨子不能同時又飛又叫。
qF. 改變會牽一髮動全身,造成其他鴨子不想要的改變。
利用接口如何?
我可以把fly()從超類中取出來,放進一個“Flyable接口”中。這麼一來,只有會飛的鴨子才實現此接口。同樣的方式,也可以用來設計一個“Quackable接口”,因爲不是所有的鴨子都會叫。
你覺得這個設計如何?
這真是一個超笨的主意,你沒發現這麼一來重複的代碼會變多嗎?如果你認爲覆蓋幾個方法就算是差勁,那麼對於48個Duck的子類都要稍微修改一下飛行的行爲,你又怎麼說?!
並非“所有”的子類都具有飛行和呱呱叫的行爲,所以繼承並不是適當的解決方式。
雖然Flyable與Quackable可以解決“一部分”問題(不會再有會飛的橡皮鴨),但是卻造成代碼無法複用,這隻能算是從一個惡夢跳進另一個惡夢。甚至,在會飛的鴨子中,飛行的動作可能還有多種變化……
現在我們知道使用繼承並不能很好地解決問題,因爲鴨子的行爲在子類裏不斷地改變,並且讓所有的子類都有這些行爲是不恰當的。
Flyable與Quackable接口一開始似乎還挺不錯,解決了問題(只有會飛的鴨子才繼承Flyable),但是Java接口不具有實現代碼,所以繼承接口無法達到代碼的複用。
無論何時你需要修改某個行爲,你必須得往下追蹤並在每一個定義此行爲的類中修改它,一不小心,可能會造成新的錯誤!
設計原則
找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。
找到變化並封裝之,以便以後可以輕易地改動或擴充此部分,而不影響不需要變化的其他部分
如果每次新的需求一來,都會使某方面的代碼發生變化,那麼你就可以確定,這部分的代碼需要被抽出來,和其他穩定的代碼有所區分。
這樣的概念很簡單,幾乎是每個設計模式背後的精神所在。所有的模式都提供了一套方法讓“系統中的某部分改變不會影響其他部分”。
分開變化和不會變化的部分:
除了fly()和quack()的問題之外,Duck類還算一切正常,似乎沒有特別需要經常變化或修改的地方。
爲了要分開“變化和不會變化的部分”,我們準備建立兩組類(完全遠離Duck類),一個是“fly”相關的,一個是“quack”相關的,每一組類將實現各自的動作。
比方說,我們可能有一個類實現“呱呱叫”,另一個類實現“吱吱叫”,還有一個類實現“安靜”。
我們知道Duck類內的fly()和quack()會隨着鴨子的不同而改變。
設計鴨子的行爲 :
我們希望一切能有彈性,我們還想能夠“指定”行爲到鴨子的實例。比方說,我們想要產生一個新的綠頭鴨(Mallard)實例,並指定特定“類型”的飛行行爲給它。讓鴨子的行爲可以動態地改變。
換句話說,我們應該在鴨子類中包含設定行爲的方法(SetBehavior),這樣就可以在“運行時”動態地“改變”綠頭鴨的飛行行爲。
針對接口編程,而不是針對實現編程。
利用接口代表每個行爲,比方說,FlyBehavior與Quack-Behavior,而行爲的每個實現都將實現其中的一個接口。
鴨子類不會負責實現Flying與Quacking接口,反而是由我們製造一組其他類專門實現FlyBehavior與QuackBehavior,這就稱爲“行爲”類。
鴨子的子類將使用接口(FlyBehavior與QuackBehavior)所表示的行爲,所以實際的“實現”不會被綁死在鴨子的子類中。
虛線三角形 --> 實現接口( △-------------)
爲 什 麼非要 把FlyBehavior設計成接口。爲何不使用抽象超類,這樣不就可以使用多態了嗎?
“接口 ” 有多個含義,接口是一個“ 概念”,也是一種Java的interface構造。你可以在不涉及Java interface的情況下,“針對接口編程”,關鍵就在多態。利用多態,程序可以針對超類型編程,執行時會根據實際狀況執行到真正的行爲,不會被綁死在超類型的行爲上。“針對超類型編程”這句話,可以更明確地說成“變量的聲明類型應該是超類型,通常是一個抽象類或者是一個接口,如此,只要是具體實現此超類型的類所產生的對象,都可以指定給這個變量。這也意味着,聲明類時不用理會以後執行時的真正對象類型!
(C++ 中 抽象類,接口。。。額。。。。不懂)
看看下面這個簡單的多態例子:假設有一個抽象類Animal,有兩個具體的實現(Dog與Cat)繼承Animal。
做法如下:
“針對實現編程”
Dog d = new Dog( );
d.bark( );
“針對接口/超類型編程”做法會如下:
Animal animal = new Dog( );
animal.makeSound( );
更棒的是,子類實例化的動作不再需要在代碼中硬編碼,例如newDog(),而是“在運行時才指定具體實現的對象”。
a = getAnimal( );
a.makeSound( );
這樣的設計,可以讓飛行和呱呱叫的動作被其他的對象複用,因爲這些行爲已經與鴨子類無關了。
而我們可以新增一些行爲,不會影響到既有的行爲類,也不會影響“使用”到飛行行爲的鴨子類。
通常在你設計系統時,預先考慮到有哪些地方未來可能需要變化,於是提前在代碼中加入這些彈性。你會發現,原則與模式可以應用在軟件開發生命週期的任何階段。
在OO系統中,是的,類代表的東西一般都是既有狀態(實例變量)又有方法。
如果你要加上一個火箭動力的飛行動作到SimUDuck系統中,你該怎麼做?
建立一個FlyRocketPowered 類,實現FlyBehavior 接口。
整合鴨子的行爲:
class Duck{
protected:
FlyBehavior *myFlyBehavoir;
public:
void PerformFly()
{
myFlyBehavoir->Fly();
};
};
想進行飛的動作,只要叫FlyBehavior對象去飛就好了: myFlyBehavior->Fly();在這部分的代碼中,我們不在乎myFlyBehavior接口的對象到底是什麼,我們只關心該對象知道如何進行飛就夠了。
同理: 想進行呱呱叫的動作,Duck對象只要叫quackBehavior對象去呱呱叫就可以了。
綠頭鴨:
class MallardDuck : public Duck
{
public:
MallardDuck(){
myFlyBehavoir = new FlyWithWings();
}
};
上面的FlyWithWings是FlyBehavior的具體實現類;myFlyBehavoir = new FlyWithWings(); MallardDuck就知道怎麼叫怎麼飛了。
通過實例化類似Quack或FlyWithWings的行爲類,並把它指定到行爲引用變量中。這種初始化實例變量的做法不夠彈性。
但是因爲quackBehavior的實例變量是一個接口類型,我們能夠在運行時,通過多態的魔力動態地給它地指定不同的QuickBehavior實現類。
#include <iostream>
using namespace std;
class FlyBehavior
{
public:
virtual void Fly() {};
};
class FlyWithWings :public FlyBehavior
{
void Fly()
{
cout << "fly with wings...\n";
}
};
class FlyNoWay :public FlyBehavior
{
void Fly()
{
cout << "cannot fly...\n";
}
};
class Duck{
protected:
FlyBehavior *myFlyBehavoir;
public:
void PerformFly()
{
myFlyBehavoir->Fly();
};
void setFlyBehavior(FlyBehavior * flyBehavior)
{
myFlyBehavoir = flyBehavior;
}
};
class MallardDuck : public Duck
{
public:
MallardDuck(){
myFlyBehavoir = new FlyWithWings();
}
};
int main()
{
Duck *duck = new MallardDuck();
duck->PerformFly();
duck->setFlyBehavior(new FlyNoWay());
duck->PerformFly();
return 0;
}
鴨子的行爲不是繼承來的,而是和適當的行爲對象“組合”來的。
多用組合,少用繼承。
策略模式:定義了算法族,分別封裝起來,讓他們之間可以相互替換,此模式讓算法的變化獨立於使用算法的客戶。
Motivation
There are common situations when classes differ only in their behavior. For this cases is a good idea to isolate the algorithms in separate classes in order to have the ability to select different algorithms at runtime.
Intent
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Strategy - defines an interface common to all supported algorithms. Context uses this interface to call the algorithm defined by a ConcreteStrategy.
Context包含一個抽象類Stategy,該抽象類有一個抽象方法,指定如何調用算法。每個派生類按需要實現該算法。具體實現的職責由Client對象承擔,並轉讓給Context對象
ConcreteStrategy - each concrete strategy implements an algorithm.