策略模式的孪生兄弟——对状态模式的深度复习总结
前言
前面有总结——策略模式,之前早就觉得策略和状态设计模式有一些相似……参考:继承、组合和接口用法——策略模式复习总结 ,该模式其实也很常用,我经常把它和策略模式结合着用,来减少大量的 if-else 代码片段。
策略模式是对象的行为模式,其实就是对一系列级别平等的算法的封装,它不关心算法实现,让客户端去动态的依靠 “环境” 类去选择需要的算法,因为他们能互相替换,可以说策略模式使能一系列算法可以平滑的切换。那么状态(State)模式,也是对象的行为设计模式的一种。
状态模式的演进和实现
官方教科书是这样定义的:
状态模式允许通过改变对象的内部状态而改变对象的行为,这个对象表现得就好像修改了它的类一样。
呵呵,记得早之前学设计模式,学到状态模式的概念,这么一看,这是解释的鸡毛啊…… 先直接看个小例子,顺着它的定义来推演:
/** * Person*/ public class Person { /** * 这个人有一个闹表,靠它的时间变化(状态修改)来决定何时做什么(改变行为) */ private int hour; public int getHour() { return hour; } public void setHour(int hour) { this.hour = hour; } /** * 人的一个行为 * * 状态模式允许通过改变一个对象的内部状态,来改变对象的行为,就像修改了对象的类一样! */ public void doSth() { // 那么我就模拟修改类的对象的内部状态 if (this.hour == 7) { System.out.println("起床啦!"); } else if (this.hour == 11) { System.out.println("吃中午饭了!"); } else if (this.hour == 19) { System.out.println("吃晚饭了!"); } else if (this.hour == 22) { System.out.println("睡觉咯!"); } else { System.out.println("学习呢!"); } } } public class MainState { public static void main(String[] args) { Person person = new Person(); person.setHour(7); person.doSth();// 起床啦! person.setHour(11); person.doSth();// 吃中午饭了! person.setHour(19); person.doSth();// 吃晚饭了! person.setHour(22); person.doSth();// 睡觉咯! person.setHour(10); person.doSth();// 学习呢! } }
这个例子,确实就是状态模式描述的场景,有一个Person类,它有一个时间对象——闹表,通过时间的变化(修改对象的内部状态)来改变对象的行为(人的一些睡觉,学习的行为),这个对象表现的就好比修改了它的类一样。
但是,这个例子并没有使用状态模式来实现,Person类设计的很low,因为有大量的 if-else 不易维护……那么这个场景下,应该使用本文提到的状态模式。尝试实现:
public abstract class State { /** * 抽象状态(接口)角色,封装了和环境类(Person类)的对象的状态(闹表时间的变化)相关的行为 */ public abstract void doSth(); } public class GetUp extends State { /** * 各个具体的状态角色,实现状态类, */ @Override public void doSth() { System.out.println("起床啦!"); } } public class HaveDinner extends State { @Override public void doSth() { System.out.println("吃晚饭了!"); } } public class HaveLunch extends State { @Override public void doSth() { System.out.println("吃中午饭了!"); } } public class Sleep extends State { @Override public void doSth() { System.out.println("睡觉咯!"); } } public class Study extends State { @Override public void doSth() { System.out.println("学习呢!"); } } public class Person { /** * 这个人有一个闹表,靠它的时间变化(状态修改)来决定何时做什么(改变行为) */ private int hour; private State state; public int getHour() { return hour; } public void setHour(int hour) { this.hour = hour; } /** * 人(环境类)的个行为 * * 状态模式允许通过改变一个对象的内部状态,来改变对象的行为,就像修改了对象的类一样! */ public void doSth() { if (this.hour == 7) { state = new GetUp(); state.doSth(); } else if (this.hour == 11) { state = new HaveLunch(); state.doSth(); } else if (this.hour == 19) { state = new HaveDinner(); state.doSth(); } else if (this.hour == 22) { state = new Sleep(); state.doSth(); } else { state = new Study(); state.doSth(); } } } public class MainStateA { public static void main(String[] args) { Person person = new Person(); person.setHour(7); person.doSth();// 起床啦! person.setHour(11); person.doSth();// 吃中午饭了! person.setHour(19); person.doSth();// 吃晚饭了! person.setHour(22); person.doSth();// 睡觉咯! person.setHour(10); person.doSth();// 学习呢! } }
确实有了变化,把之前的Person类对象的内部状态的改变对应的Person行为的变化做了封装,变成了类来表示,但是并没有什么实质上的改变。
Person类依然有大量不易维护的if-else语句,而状态模式的使用目的就是控制一个对象状态转换的条件表达式过于复杂时的情况——把状态的判断逻辑转译到表现不同状态的一系列类当中,可以把复杂的判断逻辑简化。
上一版本没有把对应状态的判断逻辑同时转移,还是留在了环境类(Person类)里……继续优化:
public abstract class State { /** * 抽象状态(接口)角色,封装了和环境类(Person类)的对象的状态(闹表时间的变化)相关的行为 */ public abstract void doSth(PersonB personB); } public class GetUp extends State { /** * 各个具体的状态角色,实现状态类, */ @Override public void doSth(PersonB personB) { if (personB.getHour() == 7) { System.out.println("起床啦!"); } else { // 转移状态 personB.setState(new HaveLunch()); // 必须要调用行为 personB.doSth(); } } } public class HaveDinner extends State { @Override public void doSth(PersonB personB) { if (personB.getHour() == 19) { System.out.println("吃晚饭了!"); } else { personB.setState(new Sleep()); personB.doSth(); } } } public class HaveLunch extends State { @Override public void doSth(PersonB personB) { if (personB.getHour() == 11) { System.out.println("吃中午饭了!"); } else { personB.setState(new HaveDinner()); personB.doSth(); } } } public class Sleep extends State { @Override public void doSth(PersonB personB) { if (personB.getHour() == 22) { System.out.println("睡觉咯!"); } else { personB.setState(new Study()); personB.doSth(); } } } public class Study extends State { @Override public void doSth(PersonB personB) { // 如此,再也不需要向下传递状态了! System.out.println(personB.getHour() + "点,正学习呢!"); } }
把之前放到环境类里的对当前对象状态的逻辑判断(条件表达式……),随着不同的状态放到了对应的状态类里,且同时让状态动态的迁移——这里又有责任链模式的影子。而且继承的抽象状态类的行为方法里加上了环境类的对象作为参数。以起床状态为例:
public void doSth(PersonB personB) { if (personB.getHour() == 7) { System.out.println("起床啦!"); } else { // 转移状态 personB.setState(new HaveLunch()); // 必须要调用行为 personB.doSth(); } }
当getup状态类的if判断不满足时,就转移状态到下一个——set 一个新状态去覆盖旧状态……同时记得调用下一个状态的行为(执行doSth方法)。
PS:这里非常像责任链(职责链)模式。最后一个状态——学习类,没有转移的其他状态了,那么就不需要转移,直接设置为终结状态(在责任链模式里是依靠判断get到的链接对象是否为null来判断职责链条的终点的)。
如下:
public class Study extends State { @Override public void doSth(PersonB personB) { // 如此,最后一个状态(或者说代表其他的状态)再也不需要向下传递状态了! System.out.println(personB.getHour() + "点,正学习呢!"); } }
再看环境类,和客户端(客户端代码不需要变化)
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); } } 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(); } }
打印:
起床啦!
吃中午饭了!
吃晚饭了!
睡觉咯!
10点,正学习呢!
貌似 ok。。。睡觉到第二天,早晨又该起床……给客户端顺序增了一个7点的状态
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();// 有问题 } }
发现打印如下:
起床啦!
吃中午饭了!
吃晚饭了!
睡觉咯!
10点,正学习呢!
7点,正学习呢!
相对完美的状态模式实现
分析前面的问题:7点应该是“起床啦!”。
这说明之前的状态模式的实现代码有问题,问题出在环境类(Person类)的初始化上,客户端 new 了一个人,则person的构造器自动初始化状态为getup,把对象的内部状态修改,会去寻找对应的状态类,找不到就迁移到下一个状态,它的状态迁移是单向不可逆的……如图:
优化如下,只需修改环境类——Person,每次搜索,都要重置状态,即从getup 开始搜索,核心思想是每次对象内部状态改变之后,都把状态迁移复位一下。记住是搜索一次之后复位。
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(); } }
这样就ok了。
小结:状态模式隐含着责任链模式的部分思想,而UML类图的设计上和策略模式非常相似,下面继续分析。
状态模式的实战写法——结合单例模式
前面的例子确实是状态模式,但是每次复位状态的时候,还有搜索状态的时候,都要new 一个状态对象,太浪费内存了,故往往实际工程里,都会结合单例模式,把每个状态类都搞成单例,如果实在搞不成,就要重新思考设计了。
参考:最简单的设计模式——单例模式的演进和推荐写法(Java 版)
状态模式都有哪些角色?画出类图?
Context:用户对象,拥有(聚合)一个State类型的成员,以标识对象的当前状态,就是Person类
State:接口或基类,封装与Context的特定状态相关的行为;
ConcreteState:接口实现类或子类,实现了一个与Context某个状态相关的行为。
是不是和策略模式的类图很像很像:同样的一个抽象类(接口),包含一个行为,和N个具体实现的类,外加一个环境类(聚合了接口引用)……
状态模式和策略模式的比较
两个模式的实现类图虽然一致,但是实现目的不一样。
首先,策略模式是一个接口的应用案例,一个很重要的设计模式,简单易用,一般用于单个算法的替换,客户端事先必须知道所有的可替换策略,由客户端去指定环境类需要哪个策略,注意通常都只有一个最恰当的策略(算法)被选择。其他策略是同级的,可互相动态的在运行中替换原有策略。
而状态模式的每个状态类需要包含环境类(Context)中的所有方法的具体实现——条件语句。通过把行为和行为对应的逻辑包装到状态类里,在环境类里消除大量的逻辑判断,而不同状态的切换由继承(实现)State的状态子类去实现,当发现修改的当前对象的状态不是自己这个状态所对应的参数,则各个状态子类自己给Context类切换状态(有职责链模式思想),且客户端不直接和状态类交互,客户端不需要了解状态。
这点和策略模式不一样,策略模式是直接依赖注入到Context类的参数进行选择策略,不存在切换状态的操作,客户端需要了解策略。
联系;状态模式和策略模式都是为具有多种可能情形设计的模式,把不同的处理情形抽象为一个相同的接口(抽象类),符合对开闭原则,且策略模式更具有一般性,在实践中,可以用策略模式来封装几乎任何类型的规则,只要在分析过程中听到需要在不同实践应用不同的业务规则,就可以考虑使用策略模式处理,在这点上策略模式是包含状态模式的功能的
状态模式的使用场景
状态模式主要解决的是:控制一个对象内部的状态转换的条件表达式过于复杂时的情况,且客户端调用之前不需要了解具体状态。它把状态的判断逻辑转到表现不同状态的一系列类当中,可以把复杂的判断逻辑简化。维持开闭原则,方便维护,还有重要一点下面会总结,状态模式是让各个状态对象自己知道其下一个处理的对象是谁,即在状态子类编译时在代码上就设定好了。
状态模式的优缺点
优点,前面说了很多了……
1、状态模式使得代码中复杂而庸长的逻辑判断语句问题得到了解决,而且状态角色将具体的状态和他对应的行为及其逻辑判断封装了起来,这使得增加一种新的状态显得十分简单。
2、把容易出错的if-else语句在环境类 or 客户端中消除,方便维护
3、每一个状态类都符合“开闭”原则——对状态的修改关闭,对客户端的扩展开放,可以随时增加新的Person的状态,或者删除。
4、State类在只有行为需要抽象时,就用接口,有其他共同功能可以用抽象类,这点和其他一些(策略)模式类似。
缺点,个人认为微不足道
使用状态模式时,每个状态对应一个具体的状态类,使结构分散,类的数量变得很多,使得程序结构变得稍显复杂,阅读代码时相对之前比较困难,不过对于优秀的研发人员来说,应该是微不足道的。
因为想要获取弹性,就必须付出代价,除非程序是一次性的,用完就丢掉……如果不是,那么假设有一个系统,某个功能需要很多状态,如果不使用状态模式优化,那么在环境类(客户端类)里会有大量的整块整块的条件判断语句。这才尼玛是真正的变得不好理解,lz我是实习生的时候,在xx公司(匿名了)就见过有人写这样的代码,一个方法或者一个类,动不动几千行代码……重要的是里面一大块一大块的if-else……还倍感优越。。。看,我写的快不快。。。
状态模式恰恰是看着类多了,其实是让状态变的清晰,让客户端和环境类都彼此干净,更加方便理解和维护。
实际编程中,面对大量的if-else,switch-case逻辑判断,如何优化?
有时业务不是很复杂,参数校验不是很多的时候,当然可以使用if或者if-else逻辑块或者switch-case块来进行编码,但是一旦扩展了程序,增加了业务,或者开始就有很多很多的逻辑判断分支,这并不是一件好事,它首先不满足OCP——开闭原则,一旦需要修改判断方法或者类,那么牵一发动全身,常常整个逻辑块都需要大改,责任没有分解,对象内部状态的改变和对应逻辑都杂糅在了一起,也不符合单一职责原则,恰恰此时,我希望分解整个判断过程,分离职责,把状态的判断逻辑转移到表示不同状态的一系列类当中,把复杂的判断逻辑简化,这就是刚刚说的状态模式。状态模式把当前类对象的内部的各种状态转移逻辑分布到State抽象类的子类中,这样减少了各个逻辑间的依赖,客户端也不需要实现了解各个状态。
不过,综上总结,我发现,状态模式是让各个状态对象自己知道其下一个处理的对象是谁!即在编译时在代码上就设定好了!比如之前例子的状态子类:
public class GetUp extends State { /** * 各个具体的状态角色,实现状态类, */ @Override public void doSth(PersonB personB) { if (personB.getHour() == 7) { System.out.println("起床啦!"); } else { // 转移状态,明确知道 要转移到哪个 已有 的状态! personB.setState(new HaveLunch()); // 必须要调用对应状态的行为 personB.doSth(); } } }
如果有一种复杂逻辑判断,比如公司考勤系统处理员工请假的流程,不同级别,类型,部门等的员工的请假流程是不一样的,我们无法知道员工该状态的下一个状态是什么。。。
比如老王是临时工,请假只需要直接领导批准,老李是正式工,请假需要先让直接领导审批,再交给主管批准,老张是安全部门的员工,请假需要的流程更复杂……或者哪天系统变化升级,请假制度修改了……换句话说就是请假系统里请假相关的各个对象并不指定(也不知道)其下一个处理的对象到底是谁,只有在客户端才设定。这怎么办?这就需要责任链设计模式解决,两者类图不一样,具体解耦责任,转移对象的流程略微的不一样,但是总的目标一致:参考:大量逻辑判断优化的思路——责任链模式复习总结及其和状态模式对比。
状态模式和职责链模式对比
大体上看,责任链模式要比状态模式灵活,虽然职责链模式灵活,但是遵循够用原则,比如前面的状态模式的例子:Person类的闹表记录一天的状态及其对应的行为,各个状态(判断逻辑)明确知道其下一个状态(处理对象)是谁,在内部编码时就确定了,状态模式就ok了,用责任链就显得很呵呵,适合就好。
还有简单情景下,可以使用三元运算符 condition ? : 代替简单的if-else语句,或者数组这种随机存储乃至查询性能很好的数据结构替换switch-case。
但是我想的是设计模式的阴暗面,不要为了用设计模式而用设计模式,对于switch-case语句块,也不要过度优化,数量不是很大时,switch的性能也不差,没必要优化什么,想起来《Java编程思想》作者和《重构》一书作者都说过的:
等到迫不得已必须要这么做的时候,再想优化,不要陷入优化和设计模式的陷阱。
JDK里都有哪些类有状态模式的应用?
常见的就是java.util.Iterator
拓展:什么是有限状态机?在Java中有什么应用?
先看教科书的具体定义:
(Finite-state machine, FSM),又称有限状态自动机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
FSM是一种算法思想,简单而言,有限状态机由一组状态、一个初始状态、输入和根据输入及现有状态转换为下一个状态的转换函数组成。
本文总结的State模式(状态模式)其实本质就是一种面向对象的状态机思想,可以适应非常复杂的状态管理。
它反映从系统开始到现在时刻输入的变化,以及各个状态之间转移的指示变更,而且必须使用能满足状态转移的条件来描述,状态机里的动作是在给定时刻要进行的活动描述,状态机里的状态存储的是有关于过去的信息,它有多种类型的动作:
- 进入动作(entry action):在进入状态时进行;
- 退出动作:在退出状态时进行;
- 输入动作:依赖于当前状态和输入条件进行;
- 转移动作:在特定转移时进行。
说了那么多,它到底能干嘛的呢,其实不论编程还是生活里,状态机无时不在。我知道,编程是对现实的抽象,状态机也是,当业务逻辑里有大量逻辑判断需要各种来回的转换状态时,有限状态机就有用了,本质上其是用查表法把处理逻辑独立到表中:
可以用通用的代码去处理任意复杂的状态转换,扩展开来,任何复杂状态逻辑的处理都可以比如:
- Java的多线程里,线程的状态转移,就可以使用状态机来描述
- 经常需要使用的正则表达式,判断字符串格式和解析字符串内容基本全靠它,正则表达式就是有限状态机。仅仅表达形式不同,正则表达式写好后可以通过程序“编译”成状态转换表,就是大家常见的状态转换图。
- 各种网络协议,记得上计算机网络课时老师讲过——所有的协议定义都有明确的“有限状态机”设计,为此国际电信联盟专门出了规格描述语言SDL(Specification and Description Language)来描述有限状态机。
- 众所周知的自动客服系统(如10086:接通之后大堆话,按1给查……按2查……按0转……按xx返回xx……)
- 编译器设计中,词法分析和语法分析都会用到
- 字符串匹配的 KMP 算法也是自动机算法的一种
- 游戏开发和设计中,比如一个NPC就是一个很典型的状态机,当玩家按下前进键时,它会从正常状态转移到向前走的状态……
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!