很多人都喜歡玩魔獸爭霸,里面的兵種很多,比如有Footman步兵、Knight騎士、Grunt獸人步兵,他們使用不同的武器,Footman步兵使用寶劍攻擊、knight也使用sword寶劍攻擊,grunt使用axe斧頭攻擊。我們如何來設計這幾個兵種角色呢?
1 初步設計
首先我們想可以創建一個character角色類,他們都能夠walk走路,但有不同的攻擊行為,可以將character設計成抽象類,利用繼承來得到具體的兵種。設計出來的類應該如圖所示。
這種設計通過繼承實現各兵種,footman、knight、grunt可以實現攻擊行為。我們現在想增加兵種kedo科多獸,kedo使用mouth嘴吞噬敵人,還可以用drum戰鼓增加我軍的攻擊力,是個輔助兵種,character中沒有kedo對應的攻擊和輔助方法,kedo不能從character繼承得到。如果想讓kedo繼承character,必須修改character的類結構,增加用嘴攻擊的方法和用鼓輔助的方法。
這里就能看到這個設計存在的問題,第一個是可擴展性不好,不能對適應變化的行為。第二個問題是footman和knight都用寶劍攻擊,swordAttack方法在子類需要重復寫代碼,沒有將代碼復用。
這里我們引入軟件設計的一個原則,就是區分不變的和變化的,將變化的封裝起來。具體到character類來說,就是walk方法是不變的,不用動。attack和assist行為是變化的,我們給封裝起來。
2 第一次改進
將attack和assist行為封裝起來,我們考慮使用接口,設計兩個接口Attackable和Assistable,讓footman、knight、grunt、kedo分別實現相應的接口。設計出來的類如下圖所示。
我們看這個設計方法,第一個可擴展的問題,如果還有其他的行為,我們可以再加入接口,這樣可擴展的問題就解決了。第二個問題,代碼重復的問題,由於footman和knight的swordAttack實現Attackable接口的attack方法,還是存在代碼重復的問題。
解決代碼重復的問題,我們考慮將attack和assist兩種行為從character中分離出來。
3 第二次改進
3.1 分離出行為
我們設計兩個行為的接口,AttackBehavior和AssistBehavior,子類來實現對應的attack和assist行為。接口和實現類的設計如圖所示。
此處使用了軟件設計的一個原則,針對接口編程,不要針對實現編程。針對接口編程,可以使用面向對象的多態特性,比如一個兵種有AttackBehavior的attack行為,這個attack行為在實現的時候可以是SwordAttack,也可以AxeAttack或者MouthAttack,增加靈活性。
3.2 整合兵種的行為
在Character類中增加兩個實例變量,分別是attackBehavior和assistBehavior,聲明為接口類型。Character設計如圖所示。
3.3 代碼實現
以下是Character類的代碼實現
public class Character {
AttackBehavior attackBehavior;
AssistBehavior assistBehavior;
public void walk(){
System.out.println("I can walk!");
}
public void performAttack(){
attackBehavior.attack();
}
public void performAssist(){
assistBehavior.assist();
}
}
以下是Kedo科多獸的主要代碼
public class Kedo extends Character {
public Kedo(){
attackBehavior = new MouthAttack();
assistBehavior = new DrumAssist();
}
}
我們在這里將兵種的攻擊或者輔助行為單獨設計為類,與兵種分離開來,這種做法讓兵種和行為都是獨立的個體,他們的結合方式變成了組合,而不是一開始的繼承。這里用到了軟件設計中一個重要的原則,多用組合,少用繼承。使用組合建立的系統具有很大的彈性。
我們的代碼Kedo科多獸類,有用嘴吞噬和用戰鼓輔助的行為,代碼把他的行為固定了,如果可以不把他的行為固定,動態的設定他的行為,則可以增加代碼的靈活性。我們可以進一步改進代碼。
3.4動態設定行為
在Character類中,增加兩個方法:
public void setAttackBehavior(AttackBehavior atb){
attackBehavior = atb;
}
public void setAssistBehavior(AssistBehavior asb){
assistBehavior = asb;
}
這樣我們如果實現新的兵種,比如 Raider狼騎士 ,他是用大刀(暫時用寶劍代替)砍,我們可以不創建raider這個兵種類,直接實現raider具有寶劍攻擊的行為。代碼如下。
Character raider = new Character();
raider.setAttackBehavior(new SwordAttack());
raider.performAttack();
4 策略模式
現在我們回過頭來再看看整個思路,兵種具有不同的攻擊或輔助行為,我們把行為分離出來進行定義,這樣就定義了一組行為(SwordAttack、AxeAttack、MouthAttack、DrumAssist),並把這些行為進行了封裝,都封裝到了類中,這些行為在地位上平等的,可以互相替代,行為獨立於兵種之外,這就是策略模式的設計思路。
我們把行為擴展到更廣義的算法,一組行為我們擴展為算法簇。再把上面的設計思路說一次,就是策略模式,定義了算法簇,分別封裝起來,讓它們之間可以互相替換,此模式讓算法的變化獨立於使用算法的客戶。