前言

这是关于设计模式的系列文章,在每篇文章中将对常见设计模式进行讲解,因为针对前端方向,而且前端常用语言 JavaScript 本身是弱类型,面向对象(模拟面向对象)编程的实现相较于其他强类型语言实现更为繁琐,所以代码主要以 JavaScript 表现。

系列文章链接:

状态模式简介

有些情况下一个对象的行为取决于一个或者多个动态变化的属性,这样的属性叫做状态,这个对象叫做有状态的对象,这种情况下通常有很多的判断来处理状态不同时代码的执行逻辑,执行逻辑可能会非常复杂,让代码变得难以维护,“状态模式” 就是将这些逻辑委托给外面的对象或类来单独维护,来减少状态对象的逻辑,增强代码的维护性。


状态模式 UML 图
状态模式 UML 图


状态模式的实现

下面是一个类,功能为根据电池不同状态打印当前不同的颜色,下面是正常的实现方式。

class Battery {
  constructor() {
    this.amount = 'high'; // 电量高
  }
  show() {
    if (this.amount === 'high') {
      console.log('显示绿色');
      this.amount = 'middle'; // 电量中等
    } else if (this.amount === 'middle') {
      console.log('显示黄色');
      this.amount = 'low'; // 电量低
    } else if (this.amount === 'low') {
      console.log('显示红色');
    }
  }
}

const battery = new Battery();

battery.show(); // 显示绿色
battery.show(); // 显示黄色
battery.show(); // 显示红色

上面的代码虽然实现了我们想要的功能,但是代码中 show 方法违反了开放封闭原则和单一职责原则,状态切换逻辑不明显,判断条件太多,维护性和扩展性差,下面使用状态模式进行优化。

class Battery {
  constructor() {
    this.amount = 'high'; // 电量高
    this.state = new SuccessState(); // 绿色状态
  }
  show() {
    // 把显示逻辑委托给状态对象
    this.state.show();
    if (this.amount === 'high') {
      this.amount = 'middle'; // 电量中等
      this.state = new WarningState(); // 黄色状态
    } else if (this.amount === 'middle') {
      this.amount = 'low'; // 电量低
      this.state = new ErrorState(); // 红色状态
    }
  }
}

class SuccessState {
  show() {
    console.log('显示绿色');
  }
}

class WarningState {
  show() {
    console.log('显示黄色');
  }
}

class ErrorState {
  show() {
    console.log('显示红色');
  }
}

const battery = new Battery();

battery.show(); // 显示绿色
battery.show(); // 显示黄色
battery.show(); // 显示红色

经过 “状态模式” 的优化,我们将状态拆分成三个类,无论关于状态操作的多复杂的逻辑都在拆分出的类中实现,而不再需要在状态对象 Battery 中。

状态模式的应用

点赞

点赞是我们在项目开发中经常见到的功能,点赞后也可以取消点赞,这就出现了按钮关于点赞状态的切换和按钮文案的切换,下面是使用 “状态模式” 来实现的点赞功能。

<div id="root"></div>
<script>
  // 维护点赞渲染逻辑的对象
  const likeState = {
    render(element) {
      element.innerHTML = '赞';
    }
  };

  // 维护取消点赞渲染逻辑的对象
  const likedState = {
    render(element) {
      element.innerHTML = '取消'
    }
  }

  class Button {
    constructor(container) {
      this.liked = false; // 默认为未点赞状态
      this.state = likeState; // 设置当前的状态为未点赞

      this.element = document.createElement('button');
      container.appendChild(this.element);
      this.render(); // 初始化渲染
    }
    setState(state) {
      this.state = state; // 修改渲染状态
      button.liked = !button.liked; // 修改状态属性
      this.render(); // 重新渲染
    }
    render() {
      this.state.render(this.element);
    }
  }

  // 获取按钮对象并添加点击事件
  const button = new Button(document.getElementById('root'));
  button.element.addEventListener('click', () => {
    button.setState(button.liked ? likeState : likedState);
  });
</script>

上面代码使用 “状态模式” 统一封装了按钮的类 Button,传入的参数为渲染按钮的容器元素,按钮类的内部创建按钮并添加到容器元素中,统一管理了点赞状态,点赞渲染对象,如果想要切换状态只需要执行 button 对象提供的 setState 方法通过传入的不同状态对象进行状态切换和页面渲染。

React 组件显示隐藏

React 中,经常会出现通过事件切换组件的显示隐藏,最简单的方式是通过类组件状态来控制,但其实也可以使用 “状态模式” 在组件外编写对状态更改的逻辑,这样可以使组件的逻辑更清晰,代码更精简。

import React from 'react';
import ReactDOM from 'react-dom';

// 状态管理对象
const States = {
  show() {
    console.log('显示 Banner');
    this.setState({ isShow: true });
  },
  hide() {
    console.log('隐藏 Banner');
    this.setState({ isShow: false });
  }
};

class Banner extends React.Component {
  state = { isShow: true }
  toggle = () => {
    const currentState = this.state.isShow ? 'hide' : 'show';
    States[currentState] && States[currentState].apply(this);
  }
  render() {
    return (
      <div>
        {
          isShow && (
            <nav>导航</nav>
          )
        }
        <button>toggle</button>
      </div>
    )
  }
}

ReactDOM.render(<Banner />, document.getElementById('root'));

在上面代码中组件外部的 States 就是管理切换状态逻辑的对象,就是说 “状态模式” 也可以在框架中单独使用。

有限状态机

其实 “状态模式” 来源一个有限状态机的概念,有限状态机是指一个事物拥有多种状态,但是同一时间只会处于一种状态,可以通过动作来改变当前的状态,在 JavaScript 中拥有第三方模块 javascript-state-machine 专门帮我们来做这件事。

javascript-state-machine 使用

javascript-state-machine 提供一个类,创建实例时传入的参数为一个 options 对象,属性 init 用来定义初始状态值,属性 transitions 用来定义属性变化,methods 用来定义属性发生变化时所触发的钩子。

// 有限状态机对象
const StateMachine = require('javascript-state-machine');

const fsm = new StateMachine({
  init: 'solid', // 初始状态(固态)
  transitions: [
    { from: 'solid', to: 'liquid', name: 'melt' },
    { from: 'liquid', to: 'solid', name: 'freeze' },
    { from: 'liquid', to: 'gas', name: 'vaporize' },
    { from: 'gas', to: 'liquid', name: 'condense' }
  ],
  methods: {
    onMelt() {
      console.log('melt');
    },
    onFreeze() {
      console.log('freeze');
    },
    onVaporize() {
      console.log('vaporize');
    },
    onCondense() {
      console.log('condense');
    }
  }
});

console.log(fsm.state); // solid
console.log(fsm.can('gas')); // false
console.log(fsm.cannot('gas')); // true
console.log(fsm.transitions()); // [ 'melt' ]

console.log(fsm.allTransitions());
// [ 'init', 'melt', 'freeze', 'vaporize', 'condense' ]

console.log(fsm.allStates()); // [ 'none', 'solid', 'liquid', 'gas' ]

fsm.melt(); // melt
console.log(fsm.state); // liquid
console.log(fsm.transitions()); // [ 'freeze', 'vaporize' ]
  • fsm.state:当前状态;
  • fsm.can:查看是否可直接转换到某个状态,参数为要转换的状态值;
  • fsm.cannot:查看是否不能直接转换到某个状态,参数为要转换的状态值;
  • fsm.transitions:返回可转换状态的方法列表;
  • fsm.allTransitions:返回所有状态转换方法列表;
  • fsm.allStates:返回定义的所有状态。

javascript-state-machine 原理

根据 javascript-state-machine 的用法我们模拟实现最基本的逻辑,构建一个有限状态机,代码如下:

class StateMachine {
  constructor(options) {
    const {
      init = 'none',
      transitions = [],
      methods = {}
    } = options;

    this.state = init;

    transitions.forEach(transition => {
      const { from, to, name } = transition;
      this[name] = function () {
        if (this.state === from) {
          this.state = to;
          const method = 'on' + name.slice(0, 1).toUpperCase() + name.slice(1);
          methods[method] && methods[method]();
        }
      }
    });
  }
}

总结

“状态模式” 实现的有限状态机可以更大限度的让状态变化与状态对象进行解耦,更减少了大量的判断逻辑,最后附上 案例地址