【設計模式學習筆記】 之 狀態模式
簡介:
每種事物都有不同的狀態,不同的狀態會有不同的表現,通過更改狀態從而改變表現的設計模式稱為狀態模式(state pattern)
下邊會通過多個例子進行講述,會有一些代碼重用的類,請註意包名!
舉例1:
人有多種心情,不同的心情會有不同的表現,這裏先使用分支判斷寫個小例子
創建一個Person類,它持有一個表示心情的字符串,通過設置這個字符串並對這個字符串進行判斷來決定產生不同的行為
1 package com.mi.state.state1; 2 3 /** 4 * 人類,擁有一個狀態屬性 5 * 這裏使用分支判斷演示 6 * @author hellxz 7 */8 public class Person { 9 10 private String mood = "happy";//心情 default happy 11 12 //表現方法,在不同的狀態下需要不同的表現 13 public void perform() { 14 if(mood.equals("happy")) { 15 System.out.println("今天很開心,大家都到我家喝酒去吧"); 16 }else if(mood.equals("sad")) { 17 System.out.println("今天很傷心,走,一起去喝酒");18 }else if(mood.equals("boring")) { 19 System.out.println("今天很無聊,敲會代碼吧"); 20 } 21 } 22 23 //getters & setters 24 public String getMood() { 25 return mood; 26 } 27 28 public void setMood(String mood) { 29 this.mood = mood; 30 } 3132 }
測試類:
1 package com.mi.state.state1; 2 3 public class Test { 4 5 public static void main(String[] args) { 6 Person person = new Person(); 7 person.setMood("boring"); 8 person.perform(); 9 } 10 11 }
輸出:
今天很無聊,敲會代碼吧
這個例子的確實現了修改不同的狀態而改變行為,但是代碼寫的不夠靈活,如果添加了新的狀態的時候,還需要修改大量的代碼,改進版如下:
舉例2:
定義Mood接口,所有心情都需要實現這個接口,Person中持有這個接口,通過set這個接口的實現類對象,來多態調用實際的perform方法
1 package com.mi.state.state2; 2 3 /** 4 * 心情接口(使用狀態模式) 5 * @author hellxz 6 */ 7 public interface Mood { 8 9 void perform(); //表現方法 10 }
實現Mood接口的類,每個實現類的表現方法實現都不同
1 package com.mi.state.state2; 2 3 /** 4 * 開心的心情 5 * @author hellxz 6 */ 7 public class HappyMood implements Mood { 8 9 @Override 10 public void perform() { 11 System.out.println("心情不錯出去釣魚"); 12 } 13 14 }
1 package com.mi.state.state2; 2 3 /** 4 * 無聊 5 * @author hellxz 6 */ 7 public class BoringMood implements Mood { 8 9 @Override 10 public void perform() { 11 System.out.println("好無聊啊,還是代碼有意思!"); 12 } 13 14 }
1 package com.mi.state.state2; 2 3 /** 4 * 心情不好 5 * @author hellxz 6 */ 7 public class BadMood implements Mood { 8 9 @Override 10 public void perform() { 11 System.out.println("心情不好,說好的幸福呢"); 12 } 13 14 }
改進後的Person類
1 package com.mi.state.state2; 2 3 public class Person { 4 5 private Mood mood; //持有心情接口引用 6 7 //表現方法 8 public void perform() { 9 //接口引用執行接口方法,多態調用實現類方法 10 mood.perform(); 11 } 12 13 //setters & getters 14 public Mood getMood() { 15 return mood; 16 } 17 18 public void setMood(Mood mood) { 19 this.mood = mood; 20 } 21 22 }
測試類
1 package com.mi.state.state2; 2 3 public class Test { 4 5 public static void main(String[] args) { 6 Person person = new Person(); 7 Mood mood = new BoringMood(); 8 // Mood mood = new HappyMood(); 9 // Mood mood = new SadMood(); 10 person.setMood(mood); 11 person.perform(); 12 } 13 }
輸出
好無聊啊,還是代碼有意思!
嘗試其余註釋行執行的不同效果,可以發現這樣實現非常靈活,只需要實現Mood接口,在測試類中傳入就可以改變執行效果!細心的你可能會發現,這種結構不是策略模式的結構麽?這個文章結尾會寫
舉例3:
人應該擁有喜悅、哀傷、無聊等狀態,這些狀態會在特定的時候展示出來,所以Person這個類中應該持有一個狀態的集合,通過不同的傳入參數來決定它的表現效果。舉例2中設置Mood實現類的時候需要手動去修改Test類的代碼,如果在生產環境中,我們需要改完代碼、重新編譯、重新部署,很麻煩,這個例子使用了配置文件
1 package com.mi.state.state3; 2 3 import java.io.IOException; 4 import java.util.Collection; 5 import java.util.HashMap; 6 import java.util.Map; 7 import java.util.Properties; 8 9 import com.mi.state.state2.HappyMood; 10 import com.mi.state.state2.Mood; 11 12 /** 13 * 狀態模式中,通過配置文件獲取狀態,使狀態更改更加靈活 本示例中引用state2包中的大部分代碼,除了Person類和測試類 14 * 15 * @author hellxz 16 */ 17 public class Person { 18 19 // 人對象內部持有心情屬性,在特定場景觸發,所以應持有所有心情屬性集合 20 // 方便的情況下,可以使用其他數據結構 21 private Map<String, Mood> moods = new HashMap<>(); 22 23 // 構造方法 24 public Person() { 25 // 添加默認心情屬性,防止配置文件出錯而導致的空指針問題 26 moods.put("default", new HappyMood()); 27 } 28 29 // 添加心情屬性 30 public void addMood(String moodName, Mood mood) { 31 moods.put(moodName, mood); 32 } 33 34 // 人的表現方法 35 public void perform() throws IOException { 36 // 心情map中獲取配置文件中的key所對應的對象,在方法中聲明變量,避免多線程安全問題 37 Mood currentMood = moods.get(getMoodName()); 38 // 防止出現空指針,設置默認心情 39 if (currentMood == null) { 40 currentMood = moods.get("default"); 41 } 42 // 當前心情表現 43 currentMood.perform(); 44 } 45 46 // 私有方法,配置文件中讀取moodName 47 private String getMoodName() throws IOException { 48 // 讀取配置文件 49 Properties props = new Properties(); 50 props.load(Person.class.getResourceAsStream("state.properties")); 51 return props.getProperty("mood"); 52 } 53 54 // 這個方法是為了批量添加心情集合,為什麽我們不用Map作為傳入的參數呢? 55 // 我們最好不要暴露內部的結構,更加專業,而且因為客戶端程序員不知道內部數據結構, 56 // 這時可以接收其他集合對象 57 @SuppressWarnings("unchecked") 58 public void addAllMoods(Collection<Mood> moods) { 59 ((Collection<Mood>) this.moods).addAll(moods); 60 } 61 }
測試類:
1 package com.mi.state.state3; 2 3 import java.io.IOException; 4 5 import com.mi.state.state2.BadMood; 6 import com.mi.state.state2.BoringMood; 7 import com.mi.state.state2.HappyMood; 8 9 /** 10 * 測試類 11 * @author hellxz 12 */ 13 public class Test { 14 15 public static void main(String[] args) throws IOException { 16 Person person = new Person(); 17 person.addMood("happy", new HappyMood()); 18 person.addMood("bad", new BadMood()); 19 person.addMood("boring", new BoringMood()); 20 21 person.perform(); 22 } 23 24 }
配置文件state.properties:
mood=bad
輸出:
心情不好,說好的幸福呢
配置文件中填入不同value,測試效果,如果該值未在Mood實現類中定義,那麽會使用默認的happy狀態。
舉例4:
人有青少年、壯年、老年三個狀態,過完一個狀態之後會自動進入下一個狀態,我們不能在人這個類中進行分支判斷,那樣代碼實在是很糟糕,不靈活。這時我們需要抽取狀態接口,狀態有一個表現方法,還有一個獲取下一狀態的方法,這樣我們只需要在實現類中指定下一狀態為什麽即可解決分支判斷的窘境。
創建一個State接口
1 package com.mi.state.state4; 2 3 /** 4 * 狀態接口 5 * @author hellxz 6 */ 7 public interface State { 8 9 //表現 10 public void perform(); 11 //下一狀態 12 public State nextState(); 13 }
實現State接口的青少年、年輕人、老年
1 package com.mi.state.state4; 2 3 /** 4 * 青少年狀態 5 * @author hellxz 6 */ 7 public class TeenState implements State{ 8 9 @Override 10 public void perform() { 11 System.out.println("我是小孩子,好好學習天天向上"); 12 } 13 14 @Override 15 public State nextState() { 16 //下一狀態為年輕人狀態 17 return new YouthState(); 18 } 19 20 }
1 package com.mi.state.state4; 2 3 /** 4 * 年輕人狀態 5 * @author hellxz 6 */ 7 public class YouthState implements State{ 8 9 @Override 10 public void perform() { 11 System.out.println("我是年輕人,精力充沛,世界等著我去創造"); 12 } 13 14 @Override 15 public State nextState() { 16 //年輕人下一狀態為老年人狀態 17 return new EldState(); 18 } 19 20 }
1 package com.mi.state.state4; 2 3 /** 4 * 老年狀態 5 * @author hellxz 6 */ 7 public class EldState implements State { 8 9 @Override 10 public void perform() { 11 System.out.println("我是老年人,一起來跳老年迪斯科"); 12 } 13 14 @Override 15 public State nextState() { 16 //老年之後沒有了,這裏組成一個環形 17 return new TeenState(); 18 } 19 20 }
新建Person類
1 package com.mi.state.state4; 2 3 /** 4 * 人類,狀態分別為 青少年,年輕人,老年 5 * 6 * @author hellxz 7 */ 8 public class Person { 9 10 //狀態接口引用 11 private State state; 12 13 public void setState(State state) { 14 this.state = state; 15 } 16 17 //表現 18 public void perform() { 19 state.perform(); 20 //更改狀態為下一狀態 21 state = state.nextState(); 22 } 23 }
測試類,多次執行perform方法
1 package com.mi.state.state4; 2 3 public class Test { 4 5 public static void main(String[] args) { 6 7 Person person = new Person(); 8 person.setState(new TeenState()); 9 person.perform(); 10 person.perform(); 11 person.perform(); 12 person.perform(); 13 } 14 15 }
輸出:
我是小孩子,好好學習天天向上
我是年輕人,精力充沛,世界等著我去創造
我是老年人,一起來跳廣場舞
我是小孩子,好好學習天天向上
這樣的確實現了自動轉換下一狀態的功能,但是看過編程思想我們知道,java的垃圾回收器不是很勤奮的,我們每次執行nextState方法就會new一個對象,這樣可能會造成內存負載過高,這樣就有了柳大講的使用枚舉方法實現的狀態模式,也就是這個例子的升級版
舉例5:
使用枚舉方式實現人的青少年、年輕人、老年狀態的自動切換,解決執行舉例3中的nextState方法new過多的對象問題
這裏復制舉例4的Person類到新包中,為什麽不引用呢?因為之前的例子中已經指向了同包中的State接口,如果引用過來,我們setState會報錯
1 package com.mi.state.state5; 2 3 /** 4 * 人類,狀態分別為 青少年,年輕人,老年 5 * 6 * @author hellxz 7 */ 8 public class Person { 9 10 //狀態接口引用 11 private State state; 12 13 public void setState(State state) { 14 this.state = state; 15 } 16 17 //表現 18 public void perform() { 19 state.perform(); 20 //更改狀態為下一狀態 21 state = state.nextState(); 22 } 23 }
枚舉實現的State
1 package com.mi.state.state5; 2 3 /** 4 * 枚舉實現狀態模式,內部的變量均為匿名內部類實現 5 * 6 * @author hellxz 7 */ 8 public enum State { 9 10 TEEN { 11 @Override 12 public void perform() { 13 System.out.println("我是小孩子,好好學習天天向上"); 14 } 15 16 @Override 17 public State nextState() { 18 return YOUTH; 19 } 20 }, 21 YOUTH { 22 @Override 23 public void perform() { 24 System.out.println("我是年輕人,精力充沛,世界等著我去創造"); 25 } 26 27 @Override 28 public State nextState() { 29 return ELD; 30 } 31 }, 32 ELD { 33 @Override 34 public void perform() { 35 System.out.println("我是老年人,一起來跳老年迪斯科"); 36 } 37 38 @Override 39 public State nextState() { 40 return TEEN; 41 } 42 }; 43 44 public abstract void perform(); //表現 45 46 public abstract State nextState(); //下一狀態 47 }
測試類,因為是枚舉,所以類名.元素 表示實現了State接口的對象
1 package com.mi.state.state5; 2 3 public class Test { 4 5 public static void main(String[] args) { 6 7 Person person = new Person(); 8 person.setState(State.TEEN); 9 person.perform(); 10 person.perform(); 11 person.perform(); 12 person.perform(); 13 } 14 15 }
輸出:
1 我是小孩子,好好學習天天向上 2 我是年輕人,精力充沛,世界等著我去創造 3 我是老年人,一起來跳廣場舞 4 我是小孩子,好好學習天天向上
總結:
- 表示每種事物中根據不同狀態擁有的不同表現
- 擁有和策略模式相同的結構,同樣是持有一個接口引用,使用這個接口引用來執行方法。
- 根據set方式設置具體實現類
狀態模式和策略模式的區別:
- 相同點: 相同的代碼結構
- 不同點:語義不同。
狀態模式中的狀態接口必須是這種事物緊密相關的,即只有有這種事物才會有這種狀態,互相依存;而策略模式中的被執行體的接口是可以單獨存在的,與代碼執行的這個類之間耦合不大。比如策略模式中的cd舉例,cd是一個接口,它可以單獨存在,也可以和其他的播放設備發生關聯。而這篇博客中的Person和State之間的關系就是互相依存的關系,狀態不能單獨存在。
有些時候策略模式和狀態模式是含糊不清的,比如京東打折,工作日9折,周末8折,這個時候我們既可以說工作日、周末是不同的狀態,也可以說不同的時間使用不同的策略
【設計模式學習筆記】 之 狀態模式