如果你的简历里出现了"设计模式"的字样,那么作为面试官的我几乎都会问到一个问题: "状态模式与策略模式有哪些区别"。很多人一脸懵,我就知道这次愉快的技术交流无疾而终了。可能对于很多人来说,策略模式比较熟悉,可什么是状态模式,好多人还是比较迷糊的。此篇专题,我们就来聊聊状态模式与策略模式。
第一部分 状态模式
考虑这样的一个场景:一个电梯,有四种操作:运行
、停止
、开门
、关门
。每一种操作成功后,都对应着状态的切换。每一种状态,又可以随着操作,向另一种状态切换。但是状态与状态之间又不是随意切换的。如下表所示:
-
运行状态
- 可以向停止状态切换
- 不能再次切换到运行状态
- 不能在电梯的运行过程中开门
- 不能在电梯的运行过程中关门 - 因为运行的过程中,电梯的门必然是关的
-
停止状态
- 可以向运行状态切换
- 可以向开门状态切换
- 可以向关门状态切换
- 不能再次切换到停止状态
-
开门状态
- 可以向关门状态切换
- 不能再次切换到开门状态
- 不能在开门的状态下运行
- 不能在开门的状态下停止 - 因为开门状态下,电梯的状态必然是停止的
-
关门状态
- 可以向运行状态切换
- 可以向开门状态切换
- 不能向停止状态切换 - 因为在关门状态下,电梯必然是停止的
- 不能再次切换到关门状态
说得比较复杂,看一个状态机图
Lift-State.png有箭头的,就是允许;没有箭头的,就不允许
1 错误示范
if
- else
真是个害人精,它让我们在实现功能的时候,不必过多地思考,很多没有研习过状态模式的同学也是可以轻松实现的——只不过没那么优美罢了。
- 新建一个枚举,列出四种状态
- 电梯有4个方法
- 电梯有1种状态
-
LiftState.java
package com.futureweaver.enums; // 电梯状态 public enum LiftState { // 开门状态 Opening, // 关门状态 Closed, // 运行状态 Running, // 停止状态 Stoped }
-
Lift.java
package com.futureweaver.domain; import com.futureweaver.enums.LiftState; // 电梯 public class Lift { private LiftState state = LiftState.Closed; // 开门 public void open() { if (state == LiftState.Opening) { System.out.println("failure: 无法重复开门"); } else if (state == LiftState.Closed) { state = LiftState.Opening; System.out.println("success: 开门"); } else if (state == LiftState.Running) { System.out.println("failure: 运行状态下不能开门"); } else/* if (state == LiftState.Stoped)*/ { state = LiftState.Opening; System.out.println("success: 开门"); } } // 关门 public void close() { if (state == LiftState.Opening) { state = LiftState.Closed; System.out.println("success: 关门"); } else if (state == LiftState.Closed) { System.out.println("failure: 无法重复关门"); } else if (state == LiftState.Running) { System.out.println("failure: 运行状态下,就一定是关门状态了"); } else/* if (state == LiftState.Stoped)*/ { System.out.println("failure: 停止后就是关门状态了"); } } // 运行 public void run() { if (state == LiftState.Opening) { System.out.println("failure: 电梯门没关,不能运行"); } else if (state == LiftState.Closed) { state = LiftState.Running; System.out.println("success: 运行"); } else if (state == LiftState.Running) { System.out.println("failure: 无法重复运行"); } else/* if (state == LiftState.Stoped)*/ { state = LiftState.Running; System.out.println("success: 运行"); } } // 停止 public void stop() { if (state == LiftState.Opening) { System.out.println("failure: 开门状态下不会运行,自然也不需要停止"); } else if (state == LiftState.Closed) { System.out.println("failure: 关门状态下不会运行,自然也不需要停止"); } else if (state == LiftState.Running) { state = LiftState.Stoped; System.out.println("success: 停止"); } else/* if (state == LiftState.Stoped)*/ { System.out.println("failure: 无法重复停止"); } } }
-
LiftTest.java
package com.futureweaver.domain; import org.junit.Test; public class LiftTest { @Test public void testLift() { Lift lift = new Lift(); lift.close(); lift.close(); lift.open(); lift.run(); lift.open(); lift.stop(); } }
-
输出
failure: 无法重复关门 failure: 无法重复关门 success: 开门 failure: 电梯门没关,不能运行 failure: 无法重复开门 failure: 开门状态下不会运行,自然也不需要停止
结论
从测试的结果可以看出,需求实现了,也没什么问题。但这是我们编写的简单代码,回过头再来审视一下Lift.java
,我们做了大量的条件判断。同一个类当中的代码量又太多——如果状态不止4种,怎么办?如果状态与状态之间的切换,业务比较复杂,不能一两条代码就搞得定,又怎么办?
2 正确示范
在现实领域中,电梯状态,自然而然就是电梯的一个属性。然而在面向对象语言中,所谓万物皆对象,状态,自然也可以作为一个对象。既然状态可以作为对象,那么就可以利用多态来解决了。
- 新建一个
电梯状态
的抽象类,定义4个操作:打开
、关闭
、停止
、运行
- 新建四个
电梯状态
的子类 - 每个实际状态,自己判断能否向目标状态切换。如果能切换的话,创建目标状态对象,并向电梯发送修改状态的请求
-
Lift.java
package com.futureweaver.domain; // 电梯 public class Lift { private LiftState state = new ClosedState(); public LiftState getState() { return state; } public void setState(LiftState state) { this.state = state; } // 开门 public void open() { // 由状态对象自己处理切换行为 state.open(this); } // 关门 public void close() { // 由状态对象自己处理切换行为 state.close(this); } // 运行 public void run() { // 由状态对象自己处理切换行为 state.run(this); } // 停止 public void stop() { // 由状态对象自己处理切换行为 state.stop(this); } }
-
LiftState.java
package com.futureweaver.domain; // 电梯状态 public abstract class LiftState { // 电梯状态的共同父类,无论向哪一个状态切换都可以,子类自己覆盖要阻止的操作 public void open(Lift lift) { // 如果成功的话,直接修改电梯的"状态"属性 lift.setState(new OpeningState()); System.out.println("success: 开门"); } public void close(Lift lift) { // 如果成功的话,直接修改电梯的"状态"属性 lift.setState(new ClosedState()); System.out.println("success: 关门"); } public void stop(Lift lift) { // 如果成功的话,直接修改电梯的"状态"属性 lift.setState(new StopedState()); System.out.println("success: 停止"); } public void run(Lift lift) { // 如果成功的话,直接修改电梯的"状态"属性 lift.setState(new RunningState()); System.out.println("success: 运行"); } }
-
OpeningState.java
package com.futureweaver.domain; public class OpeningState extends LiftState { @Override public void open(Lift lift) { System.out.println("failure: 无法重复开门"); } @Override public void stop(Lift lift) { System.out.println("failure: 开门状态下不会运行,自然也不需要停止"); } @Override public void run(Lift lift) { System.out.println("failure: 电梯门没关,不能运行"); } }
-
ClosedState.java
package com.futureweaver.domain; public class ClosedState extends LiftState { @Override public void close(Lift lift) { System.out.println("failure: 无法重复关门"); } @Override public void stop(Lift lift) { System.out.println("failure: 关门状态下不会运行,自然也不需要停止"); } }
-
StopedState.java
package com.futureweaver.domain; public class StopedState extends LiftState { @Override public void close(Lift lift) { System.out.println("failure: 停止后就是关门状态了"); } @Override public void stop(Lift lift) { System.out.println("failure: 无法重复停止"); } }
-
RunningState.java
package com.futureweaver.domain; public class RunningState extends LiftState { @Override public void open(Lift lift) { System.out.println("failure: 运行状态下不能开门"); } @Override public void close(Lift lift) { System.out.println("failure: 运行状态下,就一定是关门状态了"); } @Override public void run(Lift lift) { System.out.println("failure: 无法重复运行"); } }
-
LiftTest.java
package com.futureweaver.domain; import org.junit.Test; public class LiftTest { @Test public void testLift() { Lift lift = new Lift(); lift.close(); lift.close(); lift.open(); lift.run(); lift.open(); lift.stop(); } }
-
输出
failure: 无法重复关闭 failure: 无法重复关闭 success: 开门 failure: 电梯门没关,不能运行 failure: 无法重复开门 failure: 开门状态下不会运行,自然也不需要停止
结论
这种实现方式,电梯与电梯状态产生了双向依赖,属于一种紧耦合;电梯状态抽象父类,与电梯状态具体类又产生了双向依赖,属于一种紧耦合。理解起来有点绕,一条一条地说。
- 电梯与电梯状态的通信
电梯具备一个电梯状态对象,当接收到操作请求时,电梯对象本身不做任何的判断和处理,而是交由状态对象处理。
当电梯状态接收到转换请求时,如果可以转换,那么我们想要得到的结果是:电梯的状态发生了变化。所以电梯状态需要向电梯对象发送"改变状态"的消息,那么电梯状态就需要知道,到底是哪一个电梯对象。所以电梯状态的转换操作,需要接收一个电梯对象的参数。
- 抽象状态与具体状态的通信
具体状态需要继承自抽象状态。
抽象状态定义了默认方法,其中的默认方法就是: 所有的操作,都是合法的。既然是合法的,就要向目标状态切换。因为使用了状态模式,状态是一个对象了,所以需要创建具体状态的对象,再把创建好的状态对象,发送给电梯。
-
说得比较复杂,结合看一下类图和序列图
- 类图
- 序列图
3 状态模式总结
State-Pattern.png-
状态模式
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
通过内聚,提升了灵活性、可维护性和可扩展性。
第二部分 状态模式与策略模式
在上一篇文章《设计模式在RESTful当中的应用》当中,已经聊过策略模式,乍一看它们的类图,是很相似的:
- 状态模式
- 策略模式
策略模式与状态模式都把实际的行为,延迟到了子类,以此完成多态。同时,上下文(Context)面向的都是一个抽象类。(一些编程语言明确地区分了接口与抽象类,比如Java;而一些编程语言并没有明确地区分,比如C++。OOD本身与语言的关联是比较弱的,所以在OOP的时候,到底是面向接口还是抽象类,是需要酌情考虑的。)
状态模式与策略模式的区别
类结构相似,想要找出状态模式与策略模式的区别,就需要从它们的行为入手了
-
策略模式
在程序运行的过程中,策略与策略之间,是相互独立的,从而耦合度是比较松的。因为本身策略中封装的是一系列可以相互替换的算法,每一个策略是可以独立完成自己所要完成的工作的。
上下文(Context)依赖于策略,而策略不依赖于上下文。因为策略在工作时,并不关心这个信息是谁发送过来的。
-
状态模式: 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
在程序运行的过程中,状态与状态之间。比如从A状态,过渡到B状态,A状态是需要获取一个B状态(至于到底怎么获取,创建也好,使用注册表也好,使用享元也好,这个就要看具体业务了)的状态对象的。所以状态与状态之间是互相依赖的,耦合度是比较紧的。
上下文(Context)依赖于状态,而状态又依赖于上下文。比如从A状态,过渡到B状态,A状态先获取一个B状态,之后要找到上下文,把上下文的状态给修改掉。所以上下文下状态之间是互相依赖的,耦合度也是比较紧的。
第三部分 总结
综上所述,策略模式实现起来比较简单,是真正利用了面向对象的多态技术,完成了算法的互换使用,并且既遵循了高内聚
,又遵循了松耦合
的设计原则。
而状态模式实现起来比较复杂,其亦是利用了面向对象的多态技术,完成状态与状态之间的过渡。虽然状态模式遵循了高内聚
的设计原则,但却破坏了松耦合
原则。
两者都是通过内聚
,提升了灵活性
,可维护性
和可扩展性
。但归根结底,两者的区别就在于:策略模式是松耦合
、状态模式是紧耦合
。
打完收工
网友评论