设计模式-状态模式

2022-08-27 15:11:282022-08-30 20:35:40

学习一下「状态模式」

从一个场景切入

💡 需求:按下按钮,根据当前的状态,电灯会切换到新的状态 off->on/on->off

针对于这个小的需求,第一时间可能会写下这段代码

enum State {
  on,
  off,
}

class Light {
  state: State;

  constructor() {
    this.state = State.off;
  }

  press() {
    if (this.state === State.on) {
      this.state = State.off;
    } else if (this.state === State.off) {
      this.state = State.on;
    }
  }
}

测试一下

const light = new Light();
console.assert(light.state === State.off);

light.press();
console.assert(light.state === State.on);

light.press();
console.assert(light.state === State.off);

看起来实现了功能,但其实是存在一些隐患(缺陷)的:一段时间后,需求变了,电灯增加了一种状态--强光:在关闭的情况下,第一次打开是正常光,再按一下会切换到强光,再按一下才是关闭。针对于这个变动,我们上面的代码稍作改动也能处理:

// ...
press() {
  if (this.state === State.off) {
    this.state = State.on;
  } else if (this.state === State.on) {
    // 打开的情况下再次按下会切换到强光
    this.state = State.superLight;
  } else if (this.state === State.superLight) {
    this.state = State.off;
  }
}
// ...

但这仅仅是增加了一种状态,那如果以后再添加一种弱光,强强光。。。那都要去修改 press 方法,一来该函数将会变得极其臃肿;再者,该设计也违反了开闭原则:每次新增或修改 light 的状态,都需要修改 press 方法,这使得该函数非常不稳定,难以维护

使用状态模式改进 💡 程序

状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部

class State {
  light: Light;

  constructor(light: Light) {
    this.light = light;
  }

  press() {
    throw new Error("press方法必须被重写");
  }
}

class OffLightState extends State {
  light: Light;

  press() {
    console.log("on");
    this.light.setState(this.light.onLightState);
  }
}

class OnLightState extends State {
  light: Light;

  press() {
    console.log("superLight");
    this.light.setState(this.light.superLightState);
  }
}

class SuperLightState extends State {
  light: Light;

  press() {
    console.log("off");
    this.light.setState(this.light.offLightState);
  }
}

class Light {
  state: State;

  offLightState: OffLightState;

  superLightState: SuperLightState;

  onLightState: OnLightState;

  constructor() {
    this.onLightState = new OnLightState(this);
    this.offLightState = new OffLightState(this);
    this.superLightState = new SuperLightState(this);
    this.state = this.offLightState;
  }

  setState(state: State) {
    this.state = state;
  }
}

const light = new Light();
console.assert(light.state instanceof OffLightState);

light.state.press();
console.assert(light.state instanceof OnLightState);

light.state.press();
console.assert(light.state instanceof SuperLightState);

light.state.press();
console.assert(light.state instanceof OffLightState);

在这里,我们创建了几个不同的 State ,这个状态都有一个 press 方法,代表开关被按下时执行的动作:切换下一个状态

使用状态模式后,代码量明显增加,但这样的好处非常明显:它可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类中,便于管理;此外,由于状态之间的切换都被分布在状态类内部,因此我们无需编写过多的 if-else 来控制状态之间的转换

当我们需要为 light 新增一种状态时,只需要增加一个新的 State 然后改变相关联的状态之前的切换规则即可

使用状态机重构

借助于 xstate 重构 💡 需求

  1. 创建状态机
import { createMachine } from "xstate";

const lightMachine = createMachine({
  id: "light",
  initial: "off",
  states: {
    off: {
      on: {
        press: "weak",
      },
    },
    weak: {
      on: {
        press: "normal",
      },
    },
    normal: {
      on: {
        press: "super",
      },
    },
    super: {
      on: {
        press: "off",
      },
    },
  },
});

export { lightMachine };
  1. 使用
import { interpret } from "xstate";
import { lightMachine } from "./light.machine";

const lightService = interpret(lightMachine).onTransition(() => {
  console.log("lightService.state: ", lightService.state.value);
});

lightService.start();

console.assert(lightService.state.value === "off");

lightService.send("press");
console.assert(lightService.state.value === "weak");

lightService.send("press");
console.assert(lightService.state.value === "normal");

lightService.send("press");
console.assert(lightService.state.value === "super");

lightService.send("press");
console.assert(lightService.state.value === "off");

总结

状态模式定义了状态与行为之间的关系,较为细致的区分了事物内部的状态,并且将跟此种状态相关的行为都封装在这个状态内部(比如切换到新的状态),避免了 Context 无限膨胀,方便增加新的状态和转换。但由于每个状态都需要一个类,较为繁琐,并且由于逻辑分散在各个状态类中,无形中增加了对整体的状态机逻辑的理解难度