Simple Demo 假如我們設計一款RPG游戲,裡面有各種職階的角色可以選擇:劍士、弓箭手、槍兵、騎師等。 該游戲內部設計使用了標準的面向對象技術,設計了一個角色超類,並讓各種職階角色繼承該超類。子類先以劍士、槍兵為例。 現在突然有了一個需求:在玩家有一段時間沒有操作游戲角色後,游戲角色可以在等 ...
Simple Demo
假如我們設計一款RPG游戲,裡面有各種職階的角色可以選擇:劍士、弓箭手、槍兵、騎師等。
該游戲內部設計使用了標準的面向對象技術,設計了一個角色超類,並讓各種職階角色繼承該超類。子類先以劍士、槍兵為例。
現在突然有了一個需求:在玩家有一段時間沒有操作游戲角色後,游戲角色可以在等待時哼唱本游戲的主題曲,可以簡單理解為讓游戲角色唱歌。
1、使用繼承解決
對於一個面向對象的程式,這很簡單。只要在 Character 類上加上 sing()方法,這樣所有職階的角色類都會繼承 sing()。
但是發生了一個問題,(作為理性被奪走的補償全能力強化的)狂戰士,不存在理性和人格,自然也不可能會唱歌,這樣破壞了設定,並不是所有的職階角色都能唱歌。
雖然可以將sing()方法覆蓋掉,在方法體內什麼都不做。
為了“復用”而使用繼承,但這樣以後每當有新職階的角色類出現,就要被迫檢查該角色是否能夠有唱歌這個行為,並可能需要覆蓋sing()。
2、那麼換成用介面又如何?
將 sing()從超類中取出來,放進一個“ Singable 介面”中,只有會唱歌的職階角色才實現此介面。
但這樣每個會唱歌的角色都要實現sing()方法,會造成重覆代碼變多,代碼無法復用,而且角色哼唱的方式可能還有多種變化。
3、使用策略模式
①先找出應用中可能需要變化之處,將會變化的部分取出封裝起來,讓該部分改變不會影響其他部分。
我們知道 Character 類內的 sing()會隨著職階角色的不同而改變,將其從 Character 類中取出來,建立一組新的類來代表這個唱歌行為。
②如何實現唱歌的行為的類呢?針對介面編程。
我們利用介面 SingBehavior 代表這個行為,並有一些具體實現。
類圖:
實際代碼:
public interface SingBehavior {//行為介面 void sing(); } public class SingSoftly implements SingBehavior { // 輕輕唱 @Override public void sing() { System.out.println("hum hum hum ~ ~"); } } public class SingNoWay implements SingBehavior { // 沒法唱 @Override public void sing() { System.out.println("......"); // 沉默 } }
針對介面編程是為了針對超類型(一個抽象類或者是一個介面)編程,將變數聲明為超類型後,其具體實現類的對象都可以指定給這個變數,根據具體實現執行實際行為。
聲明瞭SingBehavior(超類型)變數後,我們甚至不需要知道實際的子類型,只關心它能夠進行正確的sing()的行為就夠了。
這樣將其分離出來後,可以讓唱歌的行為被其他對象復用,這個行為已經和 Character 類無關了。
③將 SingBehavior 和 Character 組合起來(多用組合,少用繼承)。
在 Character 類中加入一個 SingBehavior 實例變數,每個角色對象都會動態的設置這個變數,在運行時就可以引用正確的行為類。
public abstract class Character { protected SingBehavior singBehavior; public Character{ singBehavior = new SingSoftly();//預設能夠輕輕唱 } public void fight() {// 進行戰鬥 System.out.println("I can fight"); } public abstract void capability(); public void performSing() { singBehavior.sing(); } public void setSingBehavior(SingBehavior singBehavior) { //用於動態設定唱歌行為 this.singBehavior = singBehavior; } // 其他方法...... }
這樣想要進行唱歌的動作,只需委托給 singBehavior 去唱歌就可以了,不需要知道 singBehavior 真正引用的對象到底是什麼。
下麵看三個具體實現類:
Character 已經在構造器里初始化了 singBehavior,所以能夠唱歌的 Saber 類和 Lancer 類不需要再初始化該變數。
public class Saber extends Character { @Override public void capability() { System.out.println("I can use the sword");// 使用劍 } } public class Lancer extends Character { @Override public void capability() { System.out.println("I can use the lance");// 使用長矛 } } public class Berserker extends Character { public Berserker() { singBehavior = new SingNoWay(); //預設沉默無聲 } @Override public void capability() { System.out.println("I can destroy everything");// 破壞一切 } }
測試代碼:
public class CharacterSingTest { public static void main(String[] args) { Character saber = new Saber(); System.out.println("It's Saber"); saber.performSing(); Character berserker = new Berserker(); System.out.println("It's Berserker"); berserker.performSing(); // Berserker預設沉默無聲 //一開始Berserker的設定是失去理性的,假設有辦法讓他短暫的恢復理性 berserker.setSingBehavior(new SingBehavior() { //動態的設置 singBehavior @Override public void sing() { // 短暫的恢復了理性,能夠唱歌了 System.out.println("I come to my senses temporarily , hum hum hum ~ ~"); } }); berserker.performSing(); } }
列印結果:
It's Saber hum hum hum ~ ~ It's Berserker ...... I come to my senses temporarily , hum hum hum ~ ~
重新設計後的類圖:
重新設計後的類結構:所有職階的角色類繼承 Character ,具體唱歌行為實現 SingBehavior 介面,後將Character和SingBehavior組合起來,委托SingBehavior執行唱歌行為。
這一組唱歌行為可以想象成一個演算法族,將每一個演算法封裝起來,且SingSoftly 和 SingNoWay 這些具體演算法實現了同一個介面,是可以互換復用的;演算法以後發生的變化獨立於使用客戶(Character)。這就是策略模式。
策略模式類圖:
4:JDK中存在的策略模式
JDK中的比較器Comparator就應用了策略模式。
我們使用靜態方法Collections.sort()方法給集合排序時,有兩個重載方法:
①Collections.sort(List)
要求參數List集合里的元素必須實現Comparable介面(實現compareTo()方法),這樣才能根據元素自定義的排序演算法進行排序,但每次更換排序方式時都需要去修改compareTo()方法。
②Collections.sort(List, Comparator)
不要求List集合里的元素實現了Comparable介面,排序時會根據Comparator的具體實現演算法進行排序。
這裡的Collections.sort(List, Comparator)方法就是Context(環境角色),Comparator是Strategy(抽象的策略),而真正傳入參數需要的是實現了Comparator的具體實現類(具體策略類)。
這樣每次更換排序方式時只需要替換具體的比較器類,而不需要再去修改集合里的元素類。
參考書籍:《Head First 設計模式》