设计模式在前端开发中的实际应用(一)——状态模式

状态模式

前言

本系列是一套关于设计模式在实际开发中的总结,我在学习设计模式的过程中,主要参考了曾探老师的《JavaScript设计模式》和程杰老师的《大话设计模式》,结合我7年实际开发经验遇到的一些业务场景,向大家展示一些设计模式改善业务代码设计的范例,希望能够通过此系列文章帮助各位读者提高编码能力。

为了不让大家觉得本文是一个标题党文章,就没有由简单到复杂的阐述顺序,(按理说,最简单的单例模式应该首先阐述的,哈哈哈),请各位读者谅解。

1、基本概念

状态模式:当一个对象内在的状态改变时允许改变其行为,这个对象看起来像是改变了其类。

状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到了表示不同状态的一系列类当中,可以把复杂的逻辑简单化。

本系列的文章假设你已经掌握了基本的UML图,如果还不会的话,可以查阅相应的资料进行补齐。

以下是状态模式的UML图:

2、标准代码代码示例

定义一个抽象类State表示状态。

ts 复制代码
abstract class State {
  /**
   * 当前状态的名称
   */
  abstract stateName: string;
  /**
   * 在当前状态上处理的业务逻辑,将Context传递给当前业务状态,使之可以改变Context的状态
   */
  public abstract handler(ctx: Context): void;
}

定义一些具象化的业务类,继承State,负责各自的业务逻辑。

ts 复制代码
/**
 * 起床业务类
 */
class GetUpState extends State {
  stateName: string = "起床状态";

  public handler(ctx: Context): void {
    console.log("起床啦~~~~~~~~~~~");
    ctx.setState(new EatState());
  }
}

/**
 * 吃饭类
 */
class EatState extends State {
  stateName: string = "吃饭状态";

  public handler(ctx: Context): void {
    console.log("吃饭啦~~~~~~~~~~~");
    ctx.setState(new WorkState());
  }
}

/**
 * 工作类
 */
class WorkState extends State {
  stateName: string = "工作状态";

  public handler(ctx: Context): void {
    console.log("工作啦~~~~~~~~~~~~~");
    ctx.setState(new SleepState());
  }
}
/**
 * 睡觉类
 */
class SleepState extends State {
  stateName: string = "睡觉状态";

  public handler(ctx: Context): void {
    console.log("睡觉啦~~~~~~~~~~~~~~");
  }
}

定义一个上下文Context类,其类中的行为由当前持有的业务逻辑状态子类决定。

ts 复制代码
/**
 * 上下文类
 */
class Context {
  private _state: State;

  get state(): State {
    return this._state;
  }

  private set state(nextState: State) {
    console.log(
      `当前状态:${this._state.stateName}, 下一个状态:${nextState.stateName}`
    );
    this._state = nextState;
  }

  constructor(initState: State) {
    this._state = initState;
  }

  /**
   * 改变Context的行为
   */
  setState(state: State) {
    this.state = state;
  }
  /**
   * 触发当前上下文状态的业务行为
   */
  request() {
    this.state.handler(this);
  }
}

调用方:

ts 复制代码
function bootstrap() {
  const ctx = new Context(new GetUpState());
  // 起床啦~~~~~~~~~~~
  // 当前状态:起床状态, 下一个状态:吃饭状态
  ctx.request();
  // 吃饭啦~~~~~~~~~~~
  // 当前状态:吃饭状态, 下一个状态:工作状态
  ctx.request();
  // 工作啦~~~~~~~~~~~~~
  // 当前状态:工作状态, 下一个状态:睡觉状态
  ctx.request();
  // 睡觉啦~~~~~~~~~~~~~~
  ctx.request();
  // 睡觉啦~~~~~~~~~~~~~~
  ctx.request();
}

3、前端开发中的实践

有的同学一直都在说,Generator学是学了,似乎不知道有什么实际的应用场景。而通过babel编译async函数的代码可以看的出来,regenerator这个库实现async函数使用的就是状态模式。

以下是我模拟的一段业务代码:

js 复制代码
async function func() {
  const val1 = await 1;
  const val2 = await (2 + val1);
  const val3 = await (3 + val2);
  const val4 = await (4 + val3);
  return val4;
}

这段代码,会被babel编译成以下代码:

js 复制代码
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    // 如果执行出错,提前结束
    reject(error);
    return;
  }
  // 如果Generator已经迭代完成,直接把最终的返回值报告给外部的Promise,作为它的fulfilled值,结束递归
  if (info.done) {
    resolve(value);
  } else {
    // 没有完成,把本轮的值包裹,最为入参传递给下一个next或者throw的调用
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  // fn就是一个Generator,执行它可以得到一个迭代器
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      // 得到一个由Generator执行得到的迭代器
      var gen = fn.apply(self, args);
      // 定义next函数
      function _next(value) {
        // 递归的调用next,以使得Generator执行得到的迭代器可以一直向后迭代
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      // 定义错误处理函数
      function _throw(err) {
        // 递归的调用throw,以使得Generator执行得到的迭代器可以一直向后迭代
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      // 开始迭代,因为第一个next不能有参数,所以就传递了一个undefined
      _next(undefined);
    });
  };
}

function func() {
  return _func.apply(this, arguments);
}

function _func() {
  _func = _asyncToGenerator(
    // 得到一个Generator,这个Generator执行就可以得到一个迭代器
    /*#__PURE__*/ _regeneratorRuntime().mark(function _callee() {
      var val1, val2, val3, val4;
      return _regeneratorRuntime().wrap(function _callee$(_context) {
        while (1) {
          switch ((_context.prev = _context.next)) {
            case 0:
              _context.next = 2;
              return 1;

            case 2:
              val1 = _context.sent;
              _context.next = 5;
              return 2 + val1;

            case 5:
              val2 = _context.sent;
              _context.next = 8;
              return 3 + val2;

            case 8:
              val3 = _context.sent;
              _context.next = 11;
              return 4 + val3;

            case 11:
              val4 = _context.sent;
              return _context.abrupt("return", val4);

            case 13:
            case "end":
              return _context.stop();
          }
        }
      }, _callee);
    })
  );
  // 向外界返回一个Promise
  return _func.apply(this, arguments);
}

如果你对Generator函数还不太清楚的话,请先移步Generator函数的介绍,否则后文的阐述,您将无法理解。

在这一节,我们把注意力放在_regeneratorRuntime().mark(...)这个位置,此刻的回调函数,其中的_context参数正是我们状态模式的这个Context类。

首先Generator函数执行得到一个迭代器,我们每次调用迭代器的next,这个回调函数都知道去修改Context的状态,只不过上面的业务实现类被其写成了switch-case(不要怪babel编译的代码违背了什么开闭原则,babel不是在写业务,它是不知道你的业务逻辑的,它只能根据Generator函数yield语句得到这样的分支流程,不可能为你去生成那一系列的业务实现类),因此,我们每次调用迭代器,其行为就不一样(体现在你执行的不同的异步逻辑)

以下是一个使用Generator函数和不使用Generator函数实现状态模式的例子:

不使用Generator

js 复制代码
class GreenLight {
  next = new YellowLight();

  turnon(ctx) {
    console.log("绿灯亮起");
    ctx.state = this.next;
  }
}

class YellowLight {
  next = new RedLight();

  turnon(ctx) {
    console.log("黄灯闪烁,红灯即将亮起");
    ctx.state = this.next;
  }
}

class RedLight {
  next = new GreenLight();
  turnon(ctx) {
    console.log("红灯亮起");
    ctx.state = this.next;
  }
}

class SignalLight {
  state = new GreenLight();

  loop() {
    this.state.turnon(this);
  }
}

const light = new SignalLight();

function start(immediate) {
  setTimeout(() => {
    light.loop();
    start();
  }, 1000);
  immediate && light.loop();
}

使用Generator

js 复制代码
function* func() {
  while (1) {
    yield console.log("红灯亮起");
    yield console.log("绿灯亮起");
    yield console.log("黄灯闪烁,红灯即将亮起");
  }
}

const light = func();

function start(immediate) {
  setTimeout(() => {
    light.next();
    start();
  }, 1000);
  immediate && light.next();
}

因此,可以利用Generator的这个语法在实际开发中代替手写原生的状态模式的代码。

总结

状态模式主要用于管理具有多个状态的对象,特别是当这些状态下的行为差异较大,且状态转换逻辑复杂时。

通过将每个状态的行为封装在独立的类中,状态模式提高了代码的可维护性和可扩展性,同时使得状态转换逻辑更加清晰。

在以下业务场景中就可以考虑使用状态模式:

  • 游戏角色的不同状态(如站立、跑动、跳跃、攻击等,比如角色要站立之后可以根据用户的按键决定他是否可以进入奔跑状态)
  • 订单处理系统中订单的不同状态(如新建、已支付、已发货、已完成、已取消等,比如当用户支付订单之后,订单流转到已发货的处理逻辑)。
  • 电梯的不同运行状态(如静止、上升、下降、维修状态等,当电梯到达了目的楼层,就可以是否有用户请求电梯,决定是停止还是运行至用户的目标楼层)
  • 网络连接的不同状态(如连接中、已连接、断开、重连等,如果我们配置了断线自动重连,当网络状态变化的时候,自动切换至连接中的状态并处理对应的逻辑)。
相关推荐
mldong7 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排8 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60618 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角9 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!10 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰10 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi11 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen12 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马13 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu13 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss