概述

状态模式一种行为型模式。在状态模式中,允许对象在内部状态发生改变的同时也改变它的行为。
以电视遥控器为例:

  • 当电视电源关闭时,遥控器检测到当前是「关机状态」,点击遥控器中的按钮时会亮红灯,不发射任何遥控信号。
  • 而当电视电源开启时,遥控器检测到当前是「开机状态」,点击遥控器中的按钮时会亮绿灯,并发射按钮对应的遥控信号。

一看到需求,先风骚地创建一个含有 PowerFlag (电视电源开关标识) 实例变量的 TvController 类来表示遥控器,在接收到按钮事件后通过判断 powerFlag 标识来决定是否发射对应信号,比如:

  1. public class TvController {
  2. // ...
  3. private boolean mPowerFlag;
  4. public void onNextChannel() {
  5. if (mPowerFlag) {
  6. System.out.println("亮绿灯,并切换到下一个频道");
  7. return;
  8. }
  9. System.out.println("仅红灯");
  10. }
  11. }

这样做在逻辑上当然没有问题,因为我们只有一个 PowerFlag 标识呀,只需判断当前电视是否开机来决定是否发射信号就好了。但在实际生活中,遥控器的功能可能没那么简单噢,比如说:

  • 电视除了有电源开关状态外,还有一个是否待机的状态,预期在待机状态下遥控器是不能发射遥控信号的。
  • 另外,遥控器是有电池的吧,当电池快没电时,预期点击按钮发射信号时不再亮绿灯,而是亮黄灯,以提示用户电源不足。

如果我们要实现的遥控器这么复杂,就需要在 onNextChannel() 等事件通知方法中写大量 if - else 判断逻辑,这无疑增加了系统的复杂性。
那么,有没有一种好的设计模式可用来处理这种场景呢?
有,那便是状态模式。下面我们不再通过 if - else 的方式来编写遥控器代码了,大家都是成年人,编码姿势要优雅一点。

优雅的编码姿势

image.png

  • 电视的电源开关状态可以被遥控器检测到。
  • 遥控器内部的有一个抽象出来的行为接口,根据电视电源开关状态有着不一样的具体实现,如:「开机时状态对象」和「关机时状态对象 (按钮点击发射信号)」。
  • 点击遥控器中的按钮时,遥控器会通过当前的状态对象执行定义好的逻辑。

代码如下:

  1. // 抽象出遥控器的行为 (状态)
  2. public interface TvState {
  3. void onNextChannel();
  4. void onPrevChannel();
  5. void onTurnUp();
  6. void onTurnDown();
  7. }
  8. // 当电视电源为开启时的状态实现
  9. public class PowerOnState implements TvState {
  10. @Override
  11. public void onNextChannel() {
  12. System.out.println("* 亮绿灯,切换到下一个频道");
  13. }
  14. @Override
  15. public void onPrevChannel() {
  16. System.out.println("* 亮绿灯,切换到上一个频道");
  17. }
  18. @Override
  19. public void onTurnUp() {
  20. System.out.println("* 亮绿灯,调大声音");
  21. }
  22. @Override
  23. public void onTurnDown() {
  24. System.out.println("* 亮绿灯,调小声音");
  25. }
  26. }
  27. // 当电视电源为关闭时的状态实现
  28. public class PowerOffState implements TvState {
  29. @Override
  30. public void onNextChannel() {
  31. System.out.println("* 仅亮红灯");
  32. }
  33. @Override
  34. public void onPrevChannel() {
  35. System.out.println("* 仅亮红灯");
  36. }
  37. @Override
  38. public void onTurnUp() {
  39. System.out.println("* 仅亮红灯");
  40. }
  41. @Override
  42. public void onTurnDown() {
  43. System.out.println("* 仅亮红灯");
  44. }
  45. }
  1. // 电源开或关的行为 (通知)
  2. public interface PowerController {
  3. void powerOn();
  4. void powerOff();
  5. }
  6. // 遥控器类 (可接收电源开关的通知、可处理按钮的点击事件)
  7. public class TvController implements PowerController, TvState {
  8. private TvState mState;
  9. public TvController() {
  10. mState = new PowerOffState();
  11. }
  12. @Override
  13. public void powerOn() {
  14. System.out.println("- 检测到电视已开机");
  15. mState = new PowerOnState();
  16. }
  17. @Override
  18. public void powerOff() {
  19. System.out.println("- 检测到电视已关机");
  20. mState = new PowerOffState();
  21. }
  22. @Override
  23. public void onNextChannel() {
  24. mState.onNextChannel();
  25. }
  26. @Override
  27. public void onPrevChannel() {
  28. mState.onPrevChannel();
  29. }
  30. @Override
  31. public void onTurnUp() {
  32. mState.onTurnUp();
  33. }
  34. @Override
  35. public void onTurnDown() {
  36. mState.onTurnDown();
  37. }
  38. }

在上述遥控器的相关代码中:

  • 当检测到电视电源状态发生变化时,遥控器会设置对应的具体状态对象。
  • 当接收到按钮点击事件时,直接使用当前最新的状态对象执行对应方法即可,不用再写繁琐的 if - else 判断了。

当然是真的,不信我们来验证一下:

  1. public static void main(String[] args) {
  2. // 创建遥控对象时,发现电视正处于关机状态
  3. TvController controller = new TvController();
  4. // 电视开机之前,点击遥控的控制按钮都没有用
  5. controller.onNextChannel();
  6. controller.onTurnUp();
  7. System.out.println("点击电视上的电源按钮");
  8. controller.powerOn();
  9. controller.onNextChannel();
  10. controller.onPrevChannel();
  11. controller.onTurnUp();
  12. controller.onTurnDown();
  13. System.out.println("再次点击电视上的电源按钮");
  14. controller.powerOff();
  15. // 电视关机之后,点击遥控的控制按钮都没有用
  16. controller.onNextChannel();
  17. controller.onTurnUp();
  18. controller.onTurnDown();
  19. }

日志输出如下:

  1. * 仅亮红灯
  2. * 仅亮红灯
  3. 点击电视上的电源按钮
  4. - 检测到电视已开机
  5. * 亮绿灯,切换到下一个频道
  6. * 亮绿灯,切换到上一个频道
  7. * 亮绿灯,调大声音
  8. * 亮绿灯,调小声音
  9. 再次点击电视上的电源按钮
  10. - 检测到电视已关机
  11. * 仅亮红灯
  12. * 仅亮红灯
  13. * 仅亮红灯

由输出日志可知,改善后的遥控器代码的运行逻辑是完全符合预期的。

总结

当代码中包含大量与对象状态有关的逻辑判断时,可以将这些逻辑相关的行为抽象出来,并作不同的状态实现。当状态发生变化时,只需根据最新状态设置不同的对象即可。

优点:

  • 封装了转换规则。
  • 枚举可能的状态,在枚举状态之前已经确定好了状态种类。
  • 将所有与某个状态有关的行为放到一个类中,可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是写一长串儿条件语句。
  • 可以让多个环境共享一个状态对象,从而减少系统中对象的个数。

缺点:

  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对 “开闭原则” 的支持并不太好,增加新的状态类时需要修改那些负责状态转换的代码逻辑,否则无法切换到新增状态。