STRATEGY模式———趙子龍單騎救主
[junguo](#)
STRATEGY在中文中被譯成了策略,我感覺這個意思并不妥切,但翻英文詞典能得到的翻譯也只有這個,我的詞典比較簡單,不知道是否還有其它意思?如果沒有,那么我想可能和中國研制的CPU在研發階段被定名為“狗剩”一樣,它只是一個名字而已,并不能確切的代表真實的意義。經典著作《設計模式》中將策略模式定義為:定義一系列的算法,把它們一個個的封裝起來,并且使它們可以相互轉換。這個定義還是比較抽象,下面我將通過一個例子來具體的講解策略模式。感覺這樣更容易幫助新手理解模式,這也是《Head First Design Patterns》中講解模式的方法。先來描述一下用到的例子的背景資料:
話說三國時期,劉備失去徐州之后,四處奔逃,寄人籬下。先投袁紹,后附荊州劉表,屯兵新野。好不容易有些轉折,三顧茅廬請出了諸葛亮。但此后不久,曹操率領大軍猝然而至,意欲掃平江南。劉備不及防范,更加之區區數千軍馬根本不是五十萬大軍的對手。無奈之下,率同新野軍民逃奔襄陽。不意趕上劉表身亡,劉表之妻蔡夫人及蔡夫人之兄蔡瑁決議投降曹操,不給劉備開門。再度無奈,只好繼續向南奔逃,但劉備帶領數萬百姓,行動遲緩,被曹軍追上。劉備安排張飛斷后,讓趙云保護家小,自己繼續向當陽逃竄。終還是被曹軍包圍了起來。趙云苦戰之中走失了劉備家小,遂于亂軍之中左奔右突尋找劉備妻小。不期碰到曹操隨身背劍之將夏侯恩,子龍搶挑夏侯恩,奪了青釭寶劍(曹操有兩把寶劍,另一把名為倚天劍)。子龍繼續四處尋找幼主。終在一堵斷墻邊找到劉備之妻糜夫人和劉備幼子劉禪,糜夫人為了不拖累趙云護劉禪突圍,跳井而死。趙云將劉禪護于胸中,縱馬向外奔突。槍挑曹軍數將,而后曹軍一擁而上。短兵相接,子龍拔出了青釭寶劍,左揮右砍,勇不可擋,殺退曹軍眾將。由于曹操愛惜趙云人才,不許放冷箭,趙云幸而沖出了曹軍包圍圈。抱阿斗去見劉備。而后劉備摔了阿斗,有了我們都知道的諺語:劉備摔孩子——收買人心。
后人有詩贊曰:血染征袍套甲紅,當陽誰敢與爭鋒!古來沖陣扶危主,只有常山趙子龍。聲明一下,該詩完全是抄寫羅貫中的。剛剛看到一件樂事:高曉松要起訴韓寒,原因是韓寒多年前寫的小說《三重門》中引用了高的歌詞,也真能想的出來。如今互聯網上的趣事真多。
劉備五虎將中,趙云排名最后,確最受歡迎。雄姿英發,英勇強悍,確又心細如絲,體貼入微,實為千古男人之典范。以至千載之下猶有眾多美女粉絲。如果設計一個以子龍為原型的游戲,定會吸引眾多玩家,說不定還會擁有眾多的女性玩家。玩過以前大型游戲機上的三國志,趙云挺帥,但太過簡單,沒有情節,感覺不過癮。加上不是國產的,失去了不少感情。不知道現今的三國志一類的游戲是否有單騎救主這樣的情節?由于本人游戲IQ太差,不是太關心這些。不管這些了,先來設計一下我們的這段情節,簡單實現之(呵呵,說清楚了,該程序只是一個簡單的文字輸出,圖形版的我還沒這水平,這里只是教你如何使用策略模式)。
我們來看看要實現的功能,趙云手握長槍與曹軍眾將武斗,由于他奪來了青釭寶劍,所以他的兵器可以隨時更換,但每次都只能使用一種兵器(不要和我抬杠,說他可以左手持劍,右手握槍;絞盡腦汁才想起這么一個例子,容易嗎,我?)。而每種兵器的殺傷力并不相同。我們要實現的就是這么一個簡單的功能。
首先我們幫趙云提煉出一個他所屬的類——武將類,該類擁有武將名字,所使兵器等信息;還包括前進,沖鋒等方法。先來看看類圖:

這個類擁有兩個成員變量m_strName和m_strWeapon,分別表示武將名和武將使用的兵器。另有幾個成員函數:Advance表示前進;Assault表示攻擊,由于使用的武器不同,武器的使用及殺傷力并不相同,所以在我們現在設計的類中,Assault需要根據m_strWeapon的類型來實現不同的操作;SetWeapon用來設置武器的類型。
我們首先來想象一下Assault的實現,我們需要根據武器類型的不同來實現不同的操作,如果武器有數十種呢?那么最簡單的方式就是,在Assault中加入switch … case,然后根據不同的case來實現功能。如果我們可以將各個case條件下的操作提煉成一個個函數,這樣也許程序也不會太龐雜。不過我見過笨蛋寫的程序,一個函數中有數十個case條件,每個case條件下都有數十上百行代碼,整個函數搞到上千行;還好意思拿這樣的函數向人炫耀,真是無知者無畏。再接著想,我們的兵器庫不斷的變更,每當增加新的兵器類型的時候,我們是不是都需要改Assault呢?那么原本已經測試好的東西,經過變動,又需要經歷一次測試的洗禮,我們可以確保不給以前的程序帶來問題嗎?你改沒改過這樣的程序?我改過,整個過程就一個字:累。有沒有方法幫我們避免這樣的問題發生呢?有,當然有了!
解決這樣的麻煩,我們應該牢記面向對象的一個原則:一個模塊對擴展應該是開放的,而對修改應該是關閉的。那我們該如何做到在為我們的模塊添加新型武器的時候,做到不需要修改原有的類呢?最簡單的方法就是通過繼承來實現,先看類圖:

將類General做了修改,并為它添加了兩個子類GeneralWithLance(帶長矛的將軍)和GeneralWithSword(帶劍的將軍)。由于使用的兵器不同,Assault的實現不同,所以我們在子類中重載了Assault。這樣當我們的程序中需要添加新的兵器類型的時候,我們只需要重新派生新的子類就可以解決問題了。這個世界是不是變得美好了一些,不需要去修改原有的代碼,也就意味著我們可以少碰一些別人的代碼。有過經驗的人都知道,修改別人的代碼,是件痛苦至極的事情。但不要高興的太早,問題馬上又來了。這時候有人提出應該考慮將軍的坐騎,如水軍統領的行動工具是船,而輕騎將軍的行動工具應該是馬,而且行動工具不同,將軍的殺傷力也不同。我靠,整個世界又向黑暗傾斜了。想想我們當前的方法,再按繼承的方式作,就需要再擴展類:騎馬的帶劍將軍,乘船的持矛將軍….而且每次添加一種兵器就需要相應得組合不同的行動方式(如下圖所示)。可怕的現象出現了,隨著兵器和行動方式的增多,類都可能成倍的增加。類越來越多,越來越難控制,這就是傳說中的類爆炸了。這樣的程序,你還如何去維護?不過到目前為止,我還沒見過這樣的程序。那些用C++寫了十多年程序還只會select…case,而不知道用類的笨蛋,我不知道他們是只懂過程化設計?還是看到類膨脹而不敢使用類?不過類膨脹比結構化的程序更為可怕,面向對象也是把雙刃劍,達到什么樣的效果,就看應用人的水平了。

我們想使用面向對象的特性,而且不想看到類膨脹,該如何辦呢?那就應該記住另一條面向對象的原則:優先使用聚合,而不是繼承。先來簡單看看聚合的概念。
~~~
class IDCart {}; //身份證
class Person
{
public:
…..
private:
string name;
int age;
IDCart idcart;
};
~~~
(看到這個定義,基本可以確定該套系統是為中國公民做的。身份證對于身處外地打工的人來說是重要的。前段時間一哥們把身份證弄丟了,由于跳槽的緣故,他離開了原來所在的城市。人民警察要他把戶口遷移出去,大城市的戶口一般工作單位都不給辦。遷哪兒去?想想諾大的中國那里是我們的容身之所?派出所百般刁難,不給補辦。好不容易弄了一個臨時身份證,拿著去銀行注銷銀行卡,銀行居然也不給辦。哥們郁悶至極,比錢包丟的時候都郁悶。說是要戶籍改革,不知道會改些什么?想想一年前,我被小偷順走了錢包,那時候還是一代身份證,感覺辦起來比現在方便了很多。戶籍是要改革了嗎?會改成什么樣呢?)
看我們的定義,一個人擁有名字,年齡,身份證等屬性。由于身份證有一些相關的操作:發放,掛失,補辦等操作,我們把它提煉成一個單獨的類。此處我們使用聚合的方式來完成對于身份證的處理,所有對于身份證的操作,都通過idcart來實現。如:發放身份證的操作,在聚合條件下就變成了:
~~~
class IDCart { Public: void PutOut(){} }; class Person { Public: Void PutOutIDCart() { idcart->PutOut(); } Void SetIDCart(IDCart cart) { idcart = cart; } Private: string name; int age; IDCart idcart; };
~~~
聚合說白了就是在一個類中定義一個另一個類的對象,然后通過該被聚合的對象來實現相應本需要聚合類實現的功能。
使用聚合的優點是:可以幫助我們更好的封裝對象,使每個類集中在單一的功能上,使類的繼承層次也不會無限制的增加,避免出現類數量的膨脹。而且使用聚合還有一個優點就是可以動態的改變對象(下面會討論到)。不過聚合相對于繼承來說,沒有繼承直觀,理解比較困難。
在確定使用繼承還是聚合的時候,有一個原則:繼承體現的類之間“是一個”的關系。例如我們需要對學生,工人進行單獨的處理。那么我們的例子應該是這樣:
~~~
Class student : public person
{
};
Class worker : public person
{
};
~~~
也就是說學生是一個人,而工人也是一個人。學生和人之間體現的是“是一個”的關系。而工人也一樣。
而身份證對于人來說,是人的一個屬性。那么我們就可以提煉出來成為一個單一的類,通過聚合來實現。
接著還是回到我們策略模式的例子,同樣在我們的例子程序中,可以把武器提煉成一個單獨的類,類圖如下:

我們提煉出一個Weapon類,將在General中使用。噫!怎么又有一個m_strWeapon?你可能要開罵了:誰他媽是傻子呢?這樣做不又回到了過程化設計的鬼樣了?別急,提供這個錯誤的方法,只是為了給你提供另一個面向對象的設計原則:盡量針對接口編程,而不要針對實現編程。
C++中沒有象C#或者Java等面向對象語言那樣,提供對Interface的語言支持。但接口也不過是一個概念,我們使用純虛函數類,等同于接口。我們提供一個不被實例化的基類事實上也可以當作接口來用。針對接口編程的意義是:可以不用知道對象的具體類型和實例,這樣可以減少實現上的依賴性。可以幫助我們提高程序的靈活性。好了,我們再重新來設計類圖:

新的類圖中Weapon被抽象成了一個接口,擁有一個虛函數Assault,擁有兩個子類Lance和Sword。而在General類中,我們擁有了一個新的成員:m_pWeapon。而攻擊的函數變成了performAssault,它是通過調用m_pWeapon->Assault()來實現攻擊的。這樣一來武器就可以隨時變更了。我們來看看簡單的代碼實現:
~~~
//武器類
class Weapon
{
public:
virtual void Assault() = 0; //純虛函數
};
//長槍類
class Lance : public Weapon
{
public:
virtual void Assault()
{
cout << " I kill enemy with the Lance and can kill 10 every time!" << endl;
}
};
//寶劍類
class Sword : public Weapon
{
public:
virtual void Assault()
{
cout << " I kill enemy with the sword and can kill 20 every time!" << endl;
}
};
//武將類
class General
{
private:
string m_strName;
Weapon *m_pWeapon;
public:
//構造函數,初始化m_pWeapon為Lance類型
General(string strName):m_strName(strName),m_pWeapon(new Lance())
{
}
//指針是需要刪除的
~General()
{
if ( m_pWeapon != NULL ) delete m_pWeapon;
}
//設置武器類型
void SetWeapon(Weapon *pWeapon)
{
if ( m_pWeapon != NULL ) delete m_pWeapon;
m_pWeapon = pWeapon;
}
void performAssault()
{
m_pWeapon->Assault();
}
void Advance()
{
cout << "Go,Go,Go!!!" << endl;
}
};
int main(int argc, char* argv[])
{
//生成趙云對象
General zy("Zhao Yun");
//前進
zy.Advance();
//攻擊
zy.performAssault();
//更換武器
zy.SetWeapon(new Sword());
zy.Advance();
zy.performAssault();
return 0;
}
~~~
其實程序的實現相當簡單,就是一個簡單的聚合加應用針對接口編程的例子。這就是我們要講的第一個模式:策略模式。重新看一下它的定義:定義一系列的算法,把它們一個個的封裝起來,并且使它們可以相互轉換。這里所說的一系列的算法封裝就是通過繼承把各自的實現過程封裝到子類中去(我們的例子中是指Lance和Sword的實現),而所說的相互轉換就是我們通過設置基類指針而只向不同的子類(我們的例子上是通過SetWeapon來實現的)。
??? 是不是很簡單呢?如果你懂虛函數的話,千萬別告訴我沒看懂,這是對我無情的打擊,也許導致我直接懷疑你的智商。如果你不懂虛函數,那回頭找本C++的書看看吧,推薦的是《C++ primer》,第四版出了。
參考書目:
1, 設計模式——可復用面向對象軟件的基礎(Design Patterns ——Elements of Reusable Object-Oriented Software) Erich Gamma 等著 李英軍等譯 機械工業出版社
2, Head First Design Patterns(影印版)Freeman等著 東南大學出版社
3, 道法自然——面向對象實踐指南 王詠武 王詠剛著 電子工業出版社
4, 三國演義 網上找到的電子檔