把书读薄 | 《设计模式之美》设计模式与范式(行为型-状态模式)

1,163 阅读8分钟

这是我参与8月更文挑战的第4天,活动详情查看: 8月更文挑战

0x0、引言

😀 周一搬砖,元气满满,继续啃设计模式之美,本文对应设计模式与范式:行为型(64),状态模式 (State Pattern),描述了对象 状态变化 及如何在每种状态下表现出不同的 行为~

Tips:二手知识加工难免有所纰漏,感兴趣有时间的可自行查阅原文,谢谢。


0x1、定义

原始定义

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了自己的类一样。

简单点说

让一个对象通过一系列状态的变化来控制行为的变化。

状态模式策略模式 极其相似,可通过内在差别进行区分:

  • 策略模式将具体策略类暴露出去,调用者需了解每种策略的不同之处以便正确使用,封装的是不同算法,算法间没有交互,以达到算法可以自由切换的目的。
  • 状态模式状态的改变是由其内部条件来改变的,与外界无关,封装的是不同状态,以达到状态切换行为随之切换的目的。

0x2、写个简单例子

有home键的Android机为例,按下home键,处于不同状态有不同的行为:

  • 关机状态 → 没有反应;
  • 开机后首次启动 → 密码解锁;
  • 非首次启动 → 密码解锁或指纹解锁;
  • 启动后 → 返回主界面

不使用状态模式实现一波:

public class StateTest {
    private static int state = 0;
    private final static int CLOSE = 0; // 关机状态
    private final static int FIRST_BOOT = 1;   // 首次启动
    private final static int NOT_FIRST_BOOT = 2;    // 非首次启动
    private final static int AFTER_BOOT = 3;    // 启动后

    private static void clickHome() {
        if(state == CLOSE) {
            System.out.println("处于关机状态,没有反应");
        } else if(state == FIRST_BOOT) {
            System.out.println("首次启动。可以进行密码解锁");
        } else if(state == NOT_FIRST_BOOT) {
            System.out.println("非首次启动,可以进行密码或指纹解锁");
        } else if(state == AFTER_BOOT) {
            System.out.println("启动状态,返回主界面");
        }
    }

    public static void main(String[] args) {
        state = CLOSE;
        clickHome();
        state = FIRST_BOOT;
        clickHome();
        state = NOT_FIRST_BOOT;
        clickHome();
        state = AFTER_BOOT;
        clickHome();
    }
}

代码运行结果如下

如果需要增加一种状态,如处于fastboot模式,状态定义要写一个,然后if-else加一个判断;还有,不止处理Home键,还有音量键、电源键,又得定义几个函数,然后复制一波这个if-else,试试用状态模式实现一波。

// 抽象状态
public abstract class State {
    protected StateContext context;

    public void setContext(StateContext context) { this.context = context; }

    abstract void onHomeClick();
    abstract void onPowerClick();
    abstract void onVolumeAscClick();
    abstract void onVolumeDescClick();
}


// 具体状态 → 关机状态
public class CloseState extends State {
    @Override public void onHomeClick() { System.out.println("处于关机状态,按Home键没有反应"); }

    @Override void onPowerClick() {
        System.out.println("手机开机");
        context.setState(FirstBootState.class);
        context.setScreenOn(true);
        context.getState().onHomeClick();
    }

    @Override void onVolumeAscClick() { System.out.println("处于关机状态,按音量+没反应"); }

    @Override void onVolumeDescClick() {  System.out.println("处于关机状态,按音量-没反应"); }
}

// 具体状态 → 第一次启动状态
public class FirstBootState extends State {
    @Override public void onHomeClick() {
        System.out.println("首次启动,可以进行密码解锁");
        System.out.println("解锁完毕,进入主界面");
        context.setState(AfterBootState.class);
        context.setScreenOn(true);
    }

    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
        } else {
            System.out.println("亮屏,等待密码解锁");
        }
        context.setScreenOn(!context.isScreenOn());
    }

    @Override void onVolumeAscClick() { System.out.println("音量+"); }

    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}

// 具体状态 → 非第一次启动状态
public class NotFirstBootState extends State {
    @Override public void onHomeClick() {
        System.out.println("非首次启动,可以通过密码或指纹解锁");
        System.out.println("解锁完毕,进入主界面");
        context.setScreenOn(true);
        context.setState(AfterBootState.class);
    }

    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
        } else {
            System.out.println("亮屏,等待密码或指纹解锁");
            context.setState(NotFirstBootState.class);
        }
        context.setScreenOn(!context.isScreenOn());
    }

    @Override void onVolumeAscClick() { System.out.println("音量+"); }

    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}

// 具体状态 → 启动后
public class AfterBootState extends State {
    @Override void onHomeClick() { System.out.println("返回主界面"); }

    @Override void onPowerClick() {
        if(context.isScreenOn()) {
            System.out.println("熄屏");
            context.setState(NotFirstBootState.class);
        } else {
            System.out.println("亮屏,等待密码或指纹解锁");
            context.getState().onHomeClick();
        }
        context.setScreenOn(!context.isScreenOn());
    }

    @Override void onVolumeAscClick() { System.out.println("音量+"); }

    @Override void onVolumeDescClick() {  System.out.println("音量-"); }
}

// 上下文信息类
public class StateContext {
    private boolean isScreenOn = false;   // 屏幕是否亮着
    public final static Map<Class, State> stateMap = new HashMap<>();
    private State state;    // 手机当前状态

    static {
        stateMap.put(CloseState.class, new CloseState());
        stateMap.put(FirstBootState.class, new FirstBootState());
        stateMap.put(NotFirstBootState.class, new NotFirstBootState());
        stateMap.put(AfterBootState.class, new AfterBootState());
    }

    public void setState(Class stateClass) {
        this.state = stateMap.get(stateClass);
        this.state.setContext(this);
    }

    public State getState() { return state; }

    public boolean isScreenOn() { return isScreenOn; }

    public void setScreenOn(boolean screenOn) {
        isScreenOn = screenOn;
        System.out.println("===> 屏幕处于:" + (isScreenOn ? "亮屏状态": "熄屏状态"));
    }
}

// 测试用例
public class StateTest {
    public static void main(String[] args) {
        StateContext context = new StateContext();
        context.setState(CloseState.class);
        // 处于关机状态点击音量- 和 home键
        context.getState().onVolumeDescClick();
        context.getState().onHomeClick();
        // 处于关机状态点击电源键
        context.getState().onPowerClick();
        context.getState().onPowerClick();
        context.getState().onHomeClick();
        context.getState().onVolumeAscClick();
    }
}

代码运行结果如下

通过状态模式,我们把事件触发的 状态转移和动作执行,拆分到不同的状态类中,避免了分支判断结构

顺带带出UML类图、组成角色、使用场景及优缺点~

  • Context (上下文信息类) → 存储当前状态类,并负责具体状态的切换;
  • State (抽象状态类) → 定义声明状态更新的操作方法,可以是接口或抽象类;
  • ConcreteState (具体状态类) → 实现抽象状态类中定义的方法,根据具体场景指定对应状态改变后的代码逻辑;

使用场景

  • 某个操作含有庞大的分支判断结构,且分支决定于对象的状态时;
  • 对象行为取决于状态,且必须在运行时根据状态改变其行为时;

优点

  • 符合单一职责原则:将与特定状态相关的代码组织到单独的类中;
  • 更好的扩展性:扩展新的状态只需增加实现类,在需要维护的地方设置下新状态即可;
  • 提前定好可能的状态,降低代码实现复杂度,避免写大量的if-else条件语句;

缺点

  • 类增加,每个状态对应一个具体状态类;
  • 不满足开闭原则,状态模式虽然降低了状态之间的耦合,但是新增或修改状态都会涉及前/后一个状态的修改;
  • 逻辑零散,无法在一个地方就看出整个状态机的转换逻辑;

0x3、补充:有限状态机的概念

英文翻译 Finite State Machine,缩写FSM,简称状态机,它有三个组成部分:状态(State)事件(Event)动作(Action)。其中的事件又称为 转移条件,事件触发状态的转移和动作的执行(非必须)。

也可以理解为一种数学模型,该模型中有几个状态(有限的),在不同场景下,不同的状态间发生转移,在状态转移过程中可能伴随着不同的事件发生。

状态机有三种常见的实现方式:

  • 分支逻辑法 → 缺点是改变业务逻辑,改起来容易出错,代码也不易看懂。适合简单状态机;
  • 查表法 → 适用于状态很多、状态转移比较复杂的状态机,用二维数组表示状态转移图,可极大提高代码的可读性与可维护性;
  • 状态模式 → 适用于状态并不多、状态转移较简单,事件触发动作包含的业务逻辑可能较复杂的状态机。

0x4、加餐:Android源码中是如何使用16进制进行状态管理的?

在Android系统源码中涉及到 多状态 管理总是通过十六进制数字来表示,如ViewGroup中:

static final int FLAG_CLIP_CHILDREN = 0x1;
private static final int FLAG_CLIP_TO_PADDING = 0x2;
static final int FLAG_INVALIDATE_REQUIRED  = 0x4;
private static final int FLAG_RUN_ANIMATION = 0x8;
static final int FLAG_ANIMATION_DONE = 0x10;
private static final int FLAG_PADDING_NOT_NULL = 0x20;
private static final int FLAG_ANIMATION_CACHE = 0x40;
static final int FLAG_OPTIMIZE_INVALIDATE = 0x80;
static final int FLAG_CLEAR_TRANSFORMATION = 0x100;
private static final int FLAG_NOTIFY_ANIMATION_LISTENER = 0x200;

这是为什么呢?先复习下几种二进制运算:

  • 按位与(&)对应位都为1才为1,否则为0,如0x1 & 0x2 → 0001 & 0010 → 0000;
  • 按位或(|)对应位有一个为1即为1,如0x1 | 0x2 → 0001 | 0010 → 0011
  • 取反(~)按位取反,如~0x1 → 0001 → 1110

接着以上面手机状态为例,写个状态管理的例子:

private static int state = 0;
private final static int CLOSE = 0x1; // 关机状态
private final static int FIRST_BOOT = 0x2;   // 首次启动
private final static int NOT_FIRST_BOOT = 0x4;    // 非首次启动
private final static int AFTER_BOOT = 0x8;    // 启动后

状态增加 → 或运算

state | CLOSE → (0000 | 0001) → 0001 → 此时状态:CLOSE
state | FIRST_BOOT → (0001 | 0010) → 0011 → 此时状态:CLOSE + FIRST_BOOT
state | NOT_FIRST_BOOT → (0011 | 0100) → 0111 → 此时状态:CLOSE + FIRST_BOOT + NOT_FIRST_BOOT

状态移除 → 对应的位数从1改为0,先取反,再与运算

state &= ~NOT_FIRST_BOOT → (0111 & 1011) → 0011 → 此时状态:CLOSE + FIRST_BOOT
state &= ~CLOSE → (0011 & 1110) → 0010 → 此时状态:FIRST_BOOT

状态判断 → 与运算判断结果是否为0

// 假设此时状态为:CLOSE + FIRST_BOOT + NOT_FIRST_BOOT
state & FIRST_BOOT → 0111 & 0010 = 00100010 → 结果不为0,包含此状态;

// 假设此时状态为:CLOSE + FIRST_BOOT
state & NOT_FIRST_BOOT → 0011 & 0100 → 结果为0,不包含此状态;

疑惑:用来标识状态的十六进制并不是连续的,如跳过了0x3:

如果把上面的NOT_FIRST_BOOT从0x4改为0x3,而CLOSE + FIRST_BOOT 结果为0011,同为0x3,此时进行状态判断结果不为0,难道说增加了NOT_FIRST_BOOT状态吗?所以这里的取值是有固定规则的,即 左移一位

private final static int CLOSE = 1 << 0; 
private final static int FIRST_BOOT = 1 << 1;   
private final static int NOT_FIRST_BOOT = 1 << 2;    
private final static int AFTER_BOOT = 1 << 3;

选择十六进制的原因而不用其他进制的原因(如十进制):

计算机中,一个字节有八位,最大值为1111111,对应十进制255,十六进制FF,半个字节用十六进制 通过一个字母 就能表示,而转换成十进制则是一个无规律的数字,相比起十进制,十六进制转二进制 更直观一些。


参考文献