13_状态模式

1 初识状态模式

  • 关键:区分事物内部的状态

事物内部的状态改变往往会带来行为改变

有一个电灯,电灯上面只有一个开关

  • 当电灯开着的时候,此时按下开关,电灯会切换到关闭状态
  • 再按一次开关,电灯又将被打开

同一个开关按钮,在不同的状态下,表现出来的行为是不一样的

1.1 电灯程序

1. 定义Light类
class Light {}
2. 首先给出不同状态模式的电灯程序实现
constructor() {
  this.state = 'off'; // 记录电灯当前状态
  this.button = null; // 表示具体开关按钮
}
3. 定义init方法:负责在页面中创建一个真实的button节点

假设这个button就是电灯的开关按钮,onclick事件触发时就是电灯开关被按下时

init() {
  let btn = document.createElement('button');
  btn.innerHTML = '开关';
  this.button = document.body.appendChild(btn);
  this.button.onclick = () => {
    this.buttonWasPressed()
  }
}
4. buttonWasPressed方法实现

开关按下之后的行为都被封装在了 buttonWasPressed 方法中

  • 状态 -- 打开 | 关闭
buttonWasPressed() {
  if(this.state === 'off') {
    console.log('开灯');
    this.state = 'on';
  } else if(this.state === 'on') {
    console.log('关灯');
    this.state = 'off';
  }
}
  • 状态 -- 弱光 | 强光 | 关闭
buttonWasPressed() {
  if(this.state === 'off') {
    console.log('弱光');
    this.state = 'weakLight';
  } else if(this.state === 'weakLight') {
    console.log('强光');
    this.state = 'strongLight';
  } else if(this.state === 'strongLight') {
    console.log('关灯');
    this.state = 'off';
  }
}
5. 缺点

image.png

1.2 状态模式改进电灯程序

谈到封装,一般都会优先封装对象的行为,而不是对象的状态

  • 关键:把事物的每种状态都封装成单独的类,跟此状态相关的行为都被封装在类的内部

button被按下时只需在上下文中将这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为

  • 把状态的切换规律事先分布在状态类中 -- 消除原本存在的大量条件分支语句
1. 定义状态类

3个状态类都有buttonPressed方法,代表在各自状态下,按钮被按下时将发生的行为

class OffLightState {
  constructor(light) {
    this.light = light
  }
  buttonWasPressed() {
    console.log('弱光'); // offLightState 对应的行为
    this.light.setState(this.light.weakLightState)
  }
}
class WeakLightState {
  constructor(light) {
    this.light = light
  }
  buttonWasPressed() {
    console.log('强光'); // WeakLightState 对应的行为
    this.light.setState(this.light.strongLightState)
  }
}
class StrongLightState {
  constructor(light) {
    this.light = light
  }
  buttonWasPressed() {
    console.log('关灯'); // StrongLightState 对应的行为
    this.light.setState(this.light.offLightState)
  }
}
2. 改写Light类

不再使用一个字符来记录当前状态,而是更加立体化的状态对象

  • 在Light类的构造函数里为每个状态类都创建了一个状态对象 --> 可以明显地看到电灯有几种状态
constructor() {
  this.offLightState = new OffLightState(this);
  this.weakLightState = new WeakLightState(this);
  this.strongLightState = new StrongLightState(this);
  this.currState = this.offLightState;
  this.button = null;
}

按钮被按下时,context不再直接进行任何实质性的操作,而是通过 self.currState.buttonPressed() 将请求委托给当前持有的状态对象去执行

init() {
  let btn = document.createElement('button');
  btn.innerHTML = '开关';
  this.button = document.body.appendChild(btn);
  // 设置当前状态
  this.currState = this.offLightState;

  this.button.onclick = () => {
    this.currState.buttonWasPressed()
  }
}

状态对象可以通过 setState() 方法来切换light对象的状态

  • 状态的切换规律事先被定义在各个状态类中 --> 在context中不会出现任何一个跟状态切换相关的条件分支语句
setState(newState) {
  this.currState = newState;
}
3. 新增状态类superStrongLightSatate类
class SuperStrongLightSatate {
  constructor(light) {
    this.light = light
  }
  buttonWasPressed() {
    console.log('关灯'); // SuperStrongLightSatate 对应的行为
    this.light.setState(this.light.offLightState) // 切换状态到 offLightState
  }
}

在Light构造函数里新增一个superStrongLightSatate对象

constructor() {
  this.offLightState = new OffLightState(this);
  this.weakLightState = new WeakLightState(this);
  this.strongLightState = new StrongLightState(this);
  this.superStrongLightSatate = new SuperStrongLightSatate(this);
  this.currState = this.offLightState;
  this.button = null;
}

最后改变状态类之间的切换规则,从 StrongLightState->OffLightState 变成 StrongLightState->SuperStrongLightSatate->OffLightState

2 状态模式的定义

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

解析

  1. 将状态封装成独立的类,并且将请求委托给当前对象,当对象的内部状态改变时会带来不同的行为变化
  2. 使用的对象在不同的状态下具有不同的行为

3 状态模式的通用结构

在电灯例子中,我们完成了一个状态模式程序的编写

  1. 首先定义了Light类,即上下文Context
  2. 随后在Light的构造函数中创建每个状态类的实例对象 -- Context持有对这些状态对象的引用,以便将请求委托给状态对象
  3. 用户的请求,即点击button的动作也是在Context中实现

4 提供状态父类避免状态类缺乏方法定义

class SuperLightState {
  constructor(light) {
    this.light = light
  }
  buttonWasPressed() {
    throw new Error('父类的buttonWasPressed方法必须被重写')
  }
}

5 例子--文件上传

文件上传程序中会有扫描、正在上传、暂停、上传成功、上传失败这几种状态,在上传中和暂停状态下的行为表现是不一样的,样式也不同

暂停按钮: 上传中‖ 暂停中▶

5.1 更复杂的切换条件

2个按钮
  • 暂停|继续上传
  • 删除文件
不同状态下,按钮行为不同
  • 扫描中,不能进行任何操作,既不能暂停也不能删除
  • 扫描完成之后
    • 文件已存在于服务器--上传完成
    • 文件损坏--上传失败
    • 上传中, 暂停|继续上传
  • 扫描和上传中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后才能删除文件

5.2 准备工作

上传是一个异步过程,所以控件会不停地调用js提供的一个全局函数 window.external.upload 来通知js目前的上传进度,控件会把当前文件状态作为参数 state塞进 window.external.upload

1. 模拟
  1. setTimeout 模拟文件上传进度
  2. window.external.upload 只负责打印log
window.external.upload = function(state) {
  console.log(state); // sign | uploading | done | error
}
2. 定义用于上传的插件对象
let plugin = (function() {
  let plugin = document.createElement('embed');
  plugin.style.display = 'none';
  plugin.type = 'application/txftn-webkit';
  plugin.sign = function() {
    console.log('开始文件扫描');
  }
  plugin.pause = function() {
    console.log('暂停文件上传');
  }
  plugin.uploading = function() {
    console.log('开始文件上传');
  }
  plugin.del = function() {
    console.log('删除文件上传');
  }
  plugin.done = function() {
    console.log('文件上传完成');
  }
  document.body.appendChild(plugin);
  return plugin;
})()

5.3 定义Upload类

1. 构造函数

控制上传过程的对象将从 Upload 类中创建而来

class Upload {
  constructor(fileName) {
    this.plugin = plugin;
    this.fileName = fileName;
    this.button1 = null;
    this.button2 = null;
    this.state = 'sign'; // 设置初始状态为 waiting
  }
}
2. init方法

进行初始化工作:包括创建节点,起主要作用的是用于控制上传流程的2个按钮

init() {
  // 创建节点:用于控制上传流程的按钮
  this.dom = document.createElement('div');
  this.dom.innerHTML = `
    <span>文件名称:${this.fileName}</span>
    <button data-action="button1">扫描中</button>
    <button data-action="button2">删除</button>
  `
  document.body.appendChild(this.dom)
  this.button1 = this.dom.querySelector('[data-action="button1"]')
  this.button2 = this.dom.querySelector('[data-action="button2"]')
  this.bindEvent()
}
3. bindEvent方法

给2个按钮绑定点击事件

bindEvent() {
  this.button1.onclick = () => {
    switch(this.state) {
      case 'sign': // 扫描状态下,任何操作无效
        console.log('扫描中,点击无效...');
        break;
      case 'uploading': // 上传中,点击切换到暂停
        this.changeState('pause');
        break;
      case 'pause': // 暂停中,点击切换到上传中
        this.changeState('uploading');
        break;
      case 'done':
        console.log('文件已完成上传, 点击无效'); 
        break;
      case 'error':
        console.log('文件上传失败, 点击无效');
        break;
    }
  }
  this.button2.onclick = () => {
    switch(this.state) {
      case 'done':
      case 'error':
      case 'pause':
        // 上传完成、上传失败和暂停状态下可以删除
        this.changeState('del');
        break;
      case 'sign':
        console.log('文件正在扫描中,不能删除'); 
        break;
      case 'uploading':
        console.log('文件正在上传中,不能删除');
        break;
    }
  }
}
4. changeState方法

负责切换状态后的具体行为:改变按钮的innerHTML + 调用插件开始‘真正’的操作

changeState(state) {
  switch(state) {
    case 'sign':
      this.plugin.sign();
      this.button1.innerHTML = '扫描中,任何操作无效';
      break;
    case 'uploading':
      this.plugin.uploading();
      this.button1.innerHTML = '正在上传,点击暂停';
      break;
    case 'pause':
      this.plugin.pause();
      this.button1.innerHTML = '已暂停,点击继续上传';
      break;
    case 'done':
      this.plugin.done();
      this.button1.innerHTML = '上传完成';
      break;
    case 'error':
      this.button1.innerHTML = '上传失败';
      break;
    case 'del':
      this.plugin.del();
      this.dom.parentNode.removeChild(this.dom);
      console.log('删除完成');
      break;
  }
  this.state = state;
}
5. 测试
let uploadObj = new Upload('js设计模式及开发实践')
uploadObj.init()

window.external.upload = function(state) {
  uploadObj.changeState(state);
}
window.external.upload('sign') // 文件开始扫描

setTimeout(() => {
  window.external.upload('uploading'); // 1 秒后开始上传
}, 1000);

setTimeout(() => {
  window.external.upload('done'); // 5 秒后上传完成
}, 5000);

这也是一个反例

6. 缺点
  1. 程序中充斥着if else条件分支
  2. 状态和行为都被耦合在一个巨大的方法里
  3. 状态之间的联系非常复杂,更加难以维护

5.4 状态模式重构文件上传程序

1. 提供 window.external.upload 函数 + 定义plugin对象
2. 改造Upload类

在构造函数中为每种状态创建一个实例对象

class Upload {
  constructor(fileName) {
    this.plugin = plugin;
    this.fileName = fileName;
    this.button1 = null;
    this.button2 = null;
    // 设置初始状态为waiting
    this.signState = new SignState(this);
    this.uploadingState = new UploadingState(this);
    this.pauseState = new PauseState(this);
    this.doneState = new DoneState(this);
    this.errorState = new ErrorState(this);
    this.currState = this.signState; // 设置当前状态
  }
}
3. init 方法无需改变

创建节点+事件绑定

init() {
  // 创建节点:用于控制上传流程的按钮
  this.dom = document.createElement('div');
  this.dom.innerHTML = `
    <span>文件名称:${this.fileName}</span>
    <button data-action="button1">扫描中</button>
    <button data-action="button2">删除</button>
  `
  document.body.appendChild(this.dom)
  this.button1 
    = this.dom.querySelector('[data-action="button1"]')
  this.button2
    = this.dom.querySelector('[data-action="button2"]')
  // 绑定点击事件
  this.bindEvent()
}
4. 具体的按钮事件实现

Context并不做任何具体的操作,而是将请求委托给当前状态类执行

bindEvent() {
  this.button1.onclick = () => {
    this.currState.clickHandle1();
  }
  this.button2.onclick = () => {
    this.currState.clickHandle2();
  }
}

把状态对应的逻辑行为放在Upload类中

sign() {
  this.plugin.sign();
  this.button1.innerHTML = '扫描中,任何操作无效';
  this.currState = this.signState;
}
uploading() {
  this.plugin.uploading();
  this.button1.innerHTML = '正在上传,点击暂停';
  this.currState = this.uploadingState;
}
pause() {
  this.plugin.pause();
  this.button1.innerHTML = '已暂停,点击继续上传';
  this.currState = this.pauseState;
}
done() {
  this.plugin.done();
  this.button1.innerHTML = '上传完成';
  this.currState = this.uploadingState;
}
error() {
  this.button1.innerHTML = '上传失败';
  this.currState = this.errorState;
}
del() {
  this.plugin.del();
  this.dom.parentNode.removeChild(this.dom);
}
5. 定义各状态类

定义了状态父类,避免状态类缺少方法定义

class State {
  constructor(uploadObj) {
    this.uploadObj = uploadObj;
  }
  clickHandle1() {
    throw new Error('父类的clickHandle1方法必须被重写')
  }
  clickHandle2() {
    throw new Error('父类的clickHandle2方法必须被重写')
  }
}
class SignState extends State {
  constructor(uploadObj) {
    super(uploadObj)
  }
  clickHandle1() {
    console.log('扫描中,点击无效...');
  }
  clickHandle2() {
    console.log('文件正在扫描中,不能删除'); 
  }
}
class UploadingState extends State {
  constructor(uploadObj) {
    super(uploadObj)
  }
  clickHandle1() {
    this.uploadObj.pause();
  }
  clickHandle2() {
    this.uploadObj.del();
  }
}
class PauseState extends State {
  constructor(uploadObj) {
    super(uploadObj)
  }
  clickHandle1() {
    this.uploadObj.uploading();
  }
  clickHandle2() {
    this.uploadObj.del();
  }
}
class DoneState extends State {
  constructor(uploadObj) {
    super(uploadObj)
  }
  clickHandle1() {
    console.log('文件已完成上传, 点击无效'); 
  }
  clickHandle2() {
    this.uploadObj.del();
  }
}
class ErrorState extends State {
  constructor(uploadObj) {
    super(uploadObj)
  }
  clickHandle1() {
    console.log('文件上传失败, 点击无效');
  }
  clickHandle2() {
    this.uploadObj.del();
  }
}
测试
let uploadObj = new Upload('js设计模式与开发实践')
uploadObj.init()

window.external.upload = function(state) {
  uploadObj[state]()
}
window.external.upload('sign') // 文件开始扫描

setTimeout(() => {
  window.external.upload('uploading'); // 1 秒后开始上传
}, 1000);

setTimeout(() => {
  window.external.upload('done'); // 5 秒后上传完成
}, 5000);

6 状态模式的优缺点

image.png

7 状态模式和策略模式的关系

image.png

8 js版本的状态机

使用状态机改写电灯

方法1:通过 Function.prototype.call 把请求委托给某个字面量对象来执行

class Light {
  constructor() {
    // 设置当前状态
    this.currState = FSM.off;
    this.button = null;
  }
  init() {
    let button =  document.createElement('button');
    button.innerHTML = '已关灯';
    this.button = () => {
      // 把请求委托给FSM状态机
      this.currState.buttonWasPressed.call(this)
    }
  }
}
let FSM = {
  off: {
    buttonWasPressed() {
      console.log('关灯');
      this.button.innerHTML = '下一次按我是开灯';
      this.currState = FSM.on;
    }
  },
  on: {
    buttonWasPressed() {
      console.log('开灯');
      this.button.innerHTML = '下一次按我是关灯';
      this.currState = FSM.off;
    }
  }
}

方法2:利用delegate函数实现状态机编写

面向对象设计和闭包互换

1. 闭包把变量封装在闭包形成的环境中
let delegate = function(client, delegation) {
  return {
    buttonWasPressed() {
      // 将客户的操作委托给delegation对象
      return delegation.buttonWasPressed.apply(client, arguments);
    }
  }
}
2. 面向对象设计把变量保存为对象的属性
let FSM = {
  off: {
    buttonWasPressed() {
      console.log('关灯');
      this.button.innerHTML = '下一次按我是开灯';
      this.currState = this.onState;
    }
  },
  on: {
    buttonWasPressed() {
      console.log('开灯');
      this.button.innerHTML = '下一次按我是关灯';
      this.currState = this.offState;
    }
  }
}
3. Light类
class Light {
  constructor() {
    this.offState = delegate(this, FSM.off);
    this.onState = delegate(this, FSM.on);
    this.currState = this.offState;
    this.button = null;
  }
  init() {
    let button =  document.createElement('button');
    button.innerHTML = '已关灯';
    this.button = () => {
      // 把请求委托给FSM状态机
      this.currState.buttonWasPressed()
    }
  }
}

9 表驱动的有限状态机

基于表驱动

let fsm = StateMachine.create({
  initial: 'off',
  events: [
    { name: 'buttonWasPressed', from: 'off', to: 'on' },
    { name: 'buttonWasPressed', from: 'on', to: 'off' },
  ],
  callback: {
    onbuttonWasPressed(event, from, to) {
      console.log(arguments);
    }
  },
  error(eventName, from, to, args, errorCode, errorMessage) {
    console.log(arguments);
  }
})
button.onclick = () => {
  fsm.buttonWasPressed();
}

10 实际项目中的其他状态机

  • 下拉菜单的hover动作:显示、悬浮、隐藏等
  • 一次TCP请求:建立连接、监听、关闭等
  • 格斗游戏:攻击、跳跃、防御、跌倒等
let FSM = {
  walk: {
    attack() {
      console.log('攻击');
    },
    defense() {
      console.log('防御');
    },
    jump() {
      console.log('跳跃');
    },
  },
  attack: {
    walk() {
      console.log('攻击的时候不能行走');
    },
    defense() {
      console.log('攻击的时候不能防御');
    },
    jump() {
      console.log('攻击的时候不能跳跃');
    },
  }
}
posted on 2023-05-09 10:52  pleaseAnswer  阅读(19)  评论(0编辑  收藏  举报