【设计模式学习笔记】 之 状态模式
简介:
每种事物都有不同的状态,不同的状态会有不同的表现,通过更改状态从而改变表现的设计模式称为状态模式(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 } 31 32 }
测试类:
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折,这个时候我们既可以说工作日、周末是不同的状态,也可以说不同的时间使用不同的策略