介绍
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用:代码中包含大量与对象状态有关的条件语句。
如何解决:将各种具体的状态类抽象出来。
关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。
应用实例: 1、打篮球的时候运动员可以有正常状态、不正常状态和超常状态。 2、曾侯乙编钟中,'钟是抽象接口','钟A'等是具体状态,'曾侯乙编钟'是具体环境(Context)。
优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。
注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
下面我们通过网上的一个案例来细看一下这个设计模式
1 public abstract class State { 2 /** 3 * 抽象状态(接口)角色,封装了和环境类(Person类)的对象的状态(闹表时间的变化)相关的行为 4 */ 5 public abstract void doSth(); 6 } 7 8 public class GetUp extends State { 9 /** 10 * 各个具体的状态角色,实现状态类, 11 */ 12 @Override 13 public void doSth() { 14 System.out.println("起床啦!"); 15 } 16 } 17 18 public class HaveDinner extends State { 19 @Override 20 public void doSth() { 21 System.out.println("吃晚饭了!"); 22 } 23 } 24 25 public class HaveLunch extends State { 26 @Override 27 public void doSth() { 28 System.out.println("吃中午饭了!"); 29 } 30 } 31 32 public class Sleep extends State { 33 @Override 34 public void doSth() { 35 System.out.println("睡觉咯!"); 36 } 37 } 38 39 public class Study extends State { 40 @Override 41 public void doSth() { 42 System.out.println("学习呢!"); 43 } 44 } 45 46 public class Person { 47 /** 48 * 这个人有一个闹表,靠它的时间变化(状态修改)来决定何时做什么(改变行为) 49 */ 50 private int hour; 51 52 private State state; 53 54 public int getHour() { 55 return hour; 56 } 57 58 public void setHour(int hour) { 59 this.hour = hour; 60 } 61 62 /** 63 * 人(环境类)的个行为 64 * 65 * 状态模式允许通过改变一个对象的内部状态,来改变对象的行为,就像修改了对象的类一样! 66 */ 67 public void doSth() { 68 if (this.hour == 7) { 69 state = new GetUp(); 70 state.doSth(); 71 } else if (this.hour == 11) { 72 state = new HaveLunch(); 73 state.doSth(); 74 } else if (this.hour == 19) { 75 state = new HaveDinner(); 76 state.doSth(); 77 } else if (this.hour == 22) { 78 state = new Sleep(); 79 state.doSth(); 80 } else { 81 state = new Study(); 82 state.doSth(); 83 } 84 } 85 } 86 87 public class MainStateA { 88 public static void main(String[] args) { 89 Person person = new Person(); 90 91 person.setHour(7); 92 person.doSth();// 起床啦! 93 94 person.setHour(11); 95 person.doSth();// 吃中午饭了! 96 97 person.setHour(19); 98 person.doSth();// 吃晚饭了! 99 100 person.setHour(22); 101 person.doSth();// 睡觉咯! 102 103 person.setHour(10); 104 person.doSth();// 学习呢! 105 } 106 }
这个例子,确实就是状态模式描述的场景,有一个Person类,代表(个)人,它有一个时间对象——闹表,通过闹表的时间的变化(修改对象的内部状态)来改变对象的行为(人的一些睡觉,学习的行为),这个对象表现的就好比修改了它的类一样。但是,这个例子并没有使用所谓的状态设计模式来实现,Person类设计的很low!大量的if-else不易维护……那么这个场景下,应该使用本文提到的状态模式。尝试实现:
1 public abstract class State { 2 /** 3 * 抽象状态(接口)角色,封装了和环境类(Person类)的对象的状态(闹表时间的变化)相关的行为 4 */ 5 public abstract void doSth(); 6 } 7 8 public class GetUp extends State { 9 /** 10 * 各个具体的状态角色,实现状态类, 11 */ 12 @Override 13 public void doSth() { 14 System.out.println("起床啦!"); 15 } 16 } 17 18 public class HaveDinner extends State { 19 @Override 20 public void doSth() { 21 System.out.println("吃晚饭了!"); 22 } 23 } 24 25 public class HaveLunch extends State { 26 @Override 27 public void doSth() { 28 System.out.println("吃中午饭了!"); 29 } 30 } 31 32 public class Sleep extends State { 33 @Override 34 public void doSth() { 35 System.out.println("睡觉咯!"); 36 } 37 } 38 39 public class Study extends State { 40 @Override 41 public void doSth() { 42 System.out.println("学习呢!"); 43 } 44 } 45 46 public class Person { 47 /** 48 * 这个人有一个闹表,靠它的时间变化(状态修改)来决定何时做什么(改变行为) 49 */ 50 private int hour; 51 52 private State state; 53 54 public int getHour() { 55 return hour; 56 } 57 58 public void setHour(int hour) { 59 this.hour = hour; 60 } 61 62 /** 63 * 人(环境类)的个行为 64 * 65 * 状态模式允许通过改变一个对象的内部状态,来改变对象的行为,就像修改了对象的类一样! 66 */ 67 public void doSth() { 68 if (this.hour == 7) { 69 state = new GetUp(); 70 state.doSth(); 71 } else if (this.hour == 11) { 72 state = new HaveLunch(); 73 state.doSth(); 74 } else if (this.hour == 19) { 75 state = new HaveDinner(); 76 state.doSth(); 77 } else if (this.hour == 22) { 78 state = new Sleep(); 79 state.doSth(); 80 } else { 81 state = new Study(); 82 state.doSth(); 83 } 84 } 85 } 86 87 public class MainStateA { 88 public static void main(String[] args) { 89 Person person = new Person(); 90 91 person.setHour(7); 92 person.doSth();// 起床啦! 93 94 person.setHour(11); 95 person.doSth();// 吃中午饭了! 96 97 person.setHour(19); 98 person.doSth();// 吃晚饭了! 99 100 person.setHour(22); 101 person.doSth();// 睡觉咯! 102 103 person.setHour(10); 104 person.doSth();// 学习呢! 105 } 106 }
确实有了变化,把之前的Person类对象的内部状态的改变对应的Person的行为的变化做了封装,变成了具体实现抽象的类来表示,但是并没有什么实质上的改变!在这里就是典型的策略模式,还没有真正体现状态模式的意义。
Person类依然有大量不易维护的if-else语句,而状态模式的使用目的就是控制一个对象状态转换的条件表达式过于复杂时的情况——把状态的判断逻辑转译到表现不同状态的一系列类当中,可以把复杂的判断逻辑简化。上一版本没有把对应状态的判断逻辑同时转移,还是留在了环境类(Person类)里……继续优化:
1 public abstract class State { 2 /** 3 * 抽象状态(接口)角色,封装了和环境类(Person类)的对象的状态(闹表时间的变化)相关的行为 4 */ 5 public abstract void doSth(PersonB personB); 6 } 7 8 public class GetUp extends State { 9 /** 10 * 各个具体的状态角色,实现状态类, 11 */ 12 @Override 13 public void doSth(PersonB personB) { 14 if (personB.getHour() == 7) { 15 System.out.println("起床啦!"); 16 } else { 17 // 转移状态 18 personB.setState(new HaveLunch()); 19 // 必须要调用行为 20 personB.doSth(); 21 } 22 } 23 } 24 25 public class HaveDinner extends State { 26 @Override 27 public void doSth(PersonB personB) { 28 if (personB.getHour() == 19) { 29 System.out.println("吃晚饭了!"); 30 } else { 31 personB.setState(new Sleep()); 32 personB.doSth(); 33 } 34 } 35 } 36 37 public class HaveLunch extends State { 38 @Override 39 public void doSth(PersonB personB) { 40 if (personB.getHour() == 11) { 41 System.out.println("吃中午饭了!"); 42 } else { 43 personB.setState(new HaveDinner()); 44 personB.doSth(); 45 } 46 } 47 } 48 49 public class Sleep extends State { 50 @Override 51 public void doSth(PersonB personB) { 52 if (personB.getHour() == 22) { 53 System.out.println("睡觉咯!"); 54 } else { 55 personB.setState(new Study()); 56 personB.doSth(); 57 } 58 } 59 } 60 61 public class Study extends State { 62 @Override 63 public void doSth(PersonB personB) { 64 // 如此,再也不需要向下传递状态了! 65 System.out.println(personB.getHour() + "点,正学习呢!"); 66 } 67 }
把之前放到环境类里的对当前对象状态的逻辑判断(条件表达式……),随着不同的状态放到了对应的状态类里!!!且同时让状态动态的迁移——这里又有责任链模式的影子了。而且继承的抽象状态类的行为方法里加上了环境类的对象作为参数。以起床状态为例:
1 public void doSth(PersonB personB) { 2 if (personB.getHour() == 7) { 3 System.out.println("起床啦!"); 4 } else { 5 // 转移状态 6 personB.setState(new HaveLunch()); 7 // 必须要调用行为 8 personB.doSth(); 9 } 10 }
当getup状态类的if判断不满足时,必须记得转移状态到下一个,set一个新状态去覆盖旧状态……同时记得调用下一个状态的行为(执行方法)。
PS:这里非常像责任链(职责链)模式的思想。最后一个状态学习类,没有转移的其他状态了,那么就不需要转移了呗,直接设置为终结状态(在责任链模式里是依靠判断get到的链接对象是否为null来判断职责链条的终点的)。如下:
1 public class Study extends State { 2 @Override 3 public void doSth(PersonB personB) { 4 // 如此,最后一个状态(或者说代表其他的状态)再也不需要向下传递状态了! 5 System.out.println(personB.getHour() + "点,正学习呢!"); 6 } 7 }
再看环境类,和客户端(客户端代码不需要变化)
1 public class PersonB { 2 /** 3 * 这个人有一个闹表,靠它的时间变化(状态修改)来决定何时做什么(改变行为) 4 */ 5 private int hour; 6 7 private State state; 8 9 public State getState() { 10 return state; 11 } 12 13 public void setState(State state) { 14 this.state = state; 15 } 16 17 public int getHour() { 18 return hour; 19 } 20 21 public void setHour(int hour) { 22 this.hour = hour; 23 } 24 25 public PersonB() { 26 // 在构造器里初始化状态,从早晨起床开始 27 this.state = new GetUp(); 28 } 29 30 /** 31 * 人(环境类)的个行为 32 * 33 * 状态模式允许通过改变一个对象的内部状态,来改变对象的行为,就像修改了对象的类一样! 34 */ 35 public void doSth() { 36 // 传入的是PersonB的对象 37 state.doSth(this); 38 } 39 } 40 41 public class MainStateB { 42 public static void main(String[] args) { 43 PersonB personB = new PersonB(); 44 45 personB.setHour(7); 46 personB.doSth(); 47 48 personB.setHour(11); 49 personB.doSth(); 50 51 personB.setHour(19); 52 personB.doSth(); 53 54 personB.setHour(22); 55 personB.doSth(); 56 57 personB.setHour(10); 58 personB.doSth(); 59 } 60 }
打印:
起床啦!
吃中午饭了!
吃晚饭了!
睡觉咯!
10点,正学习呢!
到这里我们先总结一下:当person初始化的时候,这个时候的状态是state就是初始化的状态GetUp 的类,当执行完起床的类之后,在这个类的最后会把状态修改成吃中午饭的类,这个时候当开始执行11点的方法之后,自然而然会调用吃中午饭的类,同理在执行吃中午饭的最后会把状态改成吃晚饭的类,依次类推。最后可以设置一个当所有的状态都执行完的类状态,无论如何最后都执行这个方法里面的东西。
其实这里面还有个问题,我么你来看一下,怎么让状态复位
public class MainStateB { public static void main(String[] args) { PersonB personB = new PersonB(); personB.setHour(7); personB.doSth(); personB.setHour(11); personB.doSth(); personB.setHour(19); personB.doSth(); personB.setHour(22); personB.doSth(); personB.setHour(10); personB.doSth(); personB.setHour(7); personB.doSth();// 有问题 } }
客户端顺序增了一个7点的状态,发现打印如下:
起床啦!
吃中午饭了!
吃晚饭了!
睡觉咯!
10点,正学习呢!
7点,正学习呢!
不对啊!7点应该是起床啦!说明我之前的状态模式的实现代码还是不完美!问题出在环境类(Person类)的初始化上,客户端new了一个人,则person的构造器自动初始化状态为getup起床,当把对象的内部状态修改,那么会去寻找对应的状态类,找不到就迁移到下一个状态,它的状态迁移是单向不可逆的……如图:
继续优化如下,只需要修改环境类Person,把环境的对象的内部状态初始化工作放到行为里;
1 public class PersonB { 2 /** 3 * 这个人有一个闹表,靠它的时间变化(状态修改)来决定何时做什么(改变行为) 4 */ 5 private int hour; 6 7 private State state; 8 9 public State getState() { 10 return state; 11 } 12 13 public void setState(State state) { 14 this.state = state; 15 } 16 17 public int getHour() { 18 return hour; 19 } 20 21 public void setHour(int hour) { 22 this.hour = hour; 23 } 24 25 public PersonB() { 26 // 在构造器里初始化状态,从早晨起床开始 27 this.state = new GetUp(); 28 } 29 30 /** 31 * 人(环境类)的个行为 32 * 33 * 状态模式允许通过改变一个对象的内部状态,来改变对象的行为,就像修改了对象的类一样! 34 */ 35 public void doSth() { 36 // 每次都从头开始搜索状态类 37 this.state = new GetUp(); 38 // 传入的是PersonB的对象 39 state.doSth(this); 40 } 41 }
运行之后,又发现了一个问题,这里我把这个问题记录了,真实too young!naive啊!这样搞发生了循环调用问题。再次修改如下,核心思想其实是每次对象内部状态改变之后,都把状态迁移复位一下。记住是之后复位。
public class PersonB { /** * 这个人有一个闹表,靠它的时间变化(状态修改)来决定何时做什么(改变行为) */ private int hour; private State state; public State getState() { return state; } public void setState(State state) { this.state = state; } public int getHour() { return hour; } public void setHour(int hour) { this.hour = hour; } public PersonB() { // 在构造器里初始化状态,从早晨起床开始 this.state = new GetUp(); } /** * 人(环境类)的个行为 * * 状态模式允许通过改变一个对象的内部状态,来改变对象的行为,就像修改了对象的类一样! */ public void doSth() { // 传入的是PersonB的对象 state.doSth(this); // 每次都从头开始搜索状态类 this.state = new GetUp(); } }
其实我们在这里会有个疑问,假如每次都回归成起床的状态的话那假如7点起床,执行完起床的行为后状态又回归成起床的状态了,那岂不是一直执行不下去了。
我们可以来看一下代码的执行。
当执行完起床的状态打印完System.out.println("起床啦!");这句话之后,又执行了下面的代码
this.state = new GetUp();
显而易见,现在状态又回到了起床的状态,但是别忘了,我们还有时间的判断,当设置参数
personB.setHour(11);
我们首先会从起床的类看一下,发现起床类的判断是
if (personB.getHour() == 7) {
System.out.println("起床啦!");
}
所以我们只能执行else的内容
else {
// 转移状态
personB.setState(new HaveLunch());
// 必须要调用行为
personB.doSth();
}
这个时候状态类就变成了HaveLunch到了havenlunch之后
我们发现里面有
public void doSth(PersonB personB) {
if (personB.getHour() == 11) {
System.out.println("吃中午饭了!");
}
这个时候我们发现我们传入的就是11点
所以就打印了吃中午饭的这句代码
最后我们再把
public void doSth() {
// 传入的是PersonB的对象
state.doSth(this);
// 每次都从头开始搜索状态类
this.state = new GetUp();
}
状态改成起床的状态。
同理当我们设置19点的时候我们同样会从起床的状态一个状态一个状态的往下面去找只到找到19点的判断执行完吃晚饭的操作后再把状态设置成起床的状态。
通过上面的解释应该能理解了,其实这个也是本篇博文最难理解的地方。
下面我们来分析一下策略模式和状态模式的区别:
从上面的案例中我们可以看到,我们用大的字体标出来的以上的代码就有策略模式的意思,同样也可以看一下之前写的策略模式的博文,就不具体解释什么是策略模式了。
在这里,这是说两者的区别:
自己的理解
策略模式,是定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。当时一些列的算法中间没有任何的联系
而状态模式则是上一个算法(状态)和下一个算法(状态)有联系的,只有通过上一个算法(状态)才能接着往下走找下一个算法(状态),否则要抛异常的。