状态机模式在前端表单联动中的应用

业务背景

表单中, 需要动态添加 case, 每个 case 需要填写一个开始事件 start 和结束时间 end

而每一项的添加规则会收到上方开关的影响:

  • 若开启 interval 选项, 则新增 case 的开始时间为上一个 case的开始时间加上 interval

  • 若开启 duration 选项, 改变当前 case 的开始时间会影响当前 case的结束时间

因此会有 2 * 2 = 4 种的情况, 如果不加优化, 那么在处理点击按钮产生的 adddelete 事件时其回调函数将会充斥着 if 语句的判断

名词

里氏替换原则: 任何基类可以出现的地方,子类一定可以出现

多态 (polymorphism): 为不同类型的实体提供单一接口或使用单一符号来表示多种不同类型

两个概念感觉都差不多 = =

设计模式是对面向对象设计中反复出现的问题的解决方案

设计模式的格式:

  • patter name
  • problem
  • solution
  • motivation
  • ...

模式介绍

在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。

在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

意图: 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

主要解决: 对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。

何时使用: 代码中包含大量与对象状态有关的条件语句

优点:

  • 封装了转换规则
  • 状态的转换更加轻松
  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为

缺点:

  • 随着状态数增加, 类的数量也会以 n * n 的数量增加

应用

状态机模式主要由两个接口组成:

  1. IContext 接口

    • state: IState: 由于多态 (polymorphism), 所有实现 (implements) 了 IState 接口的类 (class) 的实例 (instance) 都可以赋值给 state 变量

    • setState(state: IState) 方法, 用于转换状态

    • doAction() 方法, 执行某种行为

  2. IState 接口

    • handleAction(ctx: IContext): 可在内部通过调用 ctx.setState 来转换状态

因此首先可以定义出两个接口:

typescript 复制代码
interface ICaseState {
  handleAdd: CaseStateActionHandler;
  handleDel: CaseStateActionHandler;
}

interface ICaseContext {
  state: ICaseState;
  setState: (state: ICaseState) => void;
  add: CaseCtxActionHandler;
  del: CaseCtxActionHandler;
}

接下来可以定义出 4 种类

  • 不启用 interval 和 duration 的 NormalCaseState
  • 启用 interval 但不启用 duration 的 UseIntervalCaseState
  • 不启用 interval 但启用 duration 的 UseDurationCaseState
  • 同时启用 interval 和 duration 的 UseIntervalAndDurationCaseState

以上 4 个类均实现了 ICaseState 接口

对于 context 接口, 可以这么实现

typescript 复制代码
class CaseCtx implements ICaseContext {
  state: ICaseState = new NormalCaseState();

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

  add: CaseCtxActionHandler = data => {

    return this.state.handleAdd(this, data);
  };

  del: CaseCtxActionHandler = data => {
    return this.state.handleDel(this, data);
  };
}

注意到, state.handleAddstate.handleDel 方法的第一个参数传入了 this, 也就是 context 它自己. 在典型的状态机模式下, 一个 state 的某个 action 会改变 context 的 state, 但在这个业务需求下, 并不存在这种情况, 因此会显得有点多余.

最后, 在组件中, 可以这么进行事件处理

ts 复制代码
export default defineComponent({
    setup () {
        const caseCtx = new CaseCtx();
        
        // 监听 interval 和 duration 开关的状态
        watchEffect(() => {
            // ... 省略部分代码
            let state: ICaseState;
            if (!useInterval && !useDuration) {
                state = new NormalCaseState();
            } else if (useInterval && useDuration) {
                state = new UseIntervalAndDurationCaseState();
            } else if (useInterval) {
                state = new UseIntervalCaseState();
            } else {
                state = new UseDurationCaseState();
            }
            caseCtx.setState(state);
        });
        
        // 点击 + 号的回调
        const handleCaseAdd = (idx: number) => {
            // 不需要关注开启了 interval 与否, 开启了 duration 与否
            const newCases = caseCtx.add(...);
            // DO something with new Cases after add
        };
        const handleCaseDel = (idx: number) => {
            const newCases = caseCtx.del(...);
            // DO something with new Cases after delete
        }
    }
});

结语

  • 面向对象在前端同样重要
  • 设计模式中每个模式都描述了其 motivation, problem 等信息, 优化代码时可以从这些找出灵感
  • 赞美设计模式, 设计模式 nb
相关推荐
憧憬成为web高手4 小时前
ACTF 12307复现
前端·bootstrap·html
wordbaby5 小时前
Axios 上传大文件崩溃:鸿蒙 RNOH 下 XHR 返回空响应头引发的"假失败"
前端·react native
wordbaby5 小时前
React Native 列表分页实战:下拉刷新与上拉加载的工程化方案
前端·react native
wordbaby6 小时前
脱离 Tab 栏的艺术:React Native 全屏子页面的导航架构实践
前端·react native·harmonyos
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
wordbaby6 小时前
React Native 新架构落地鸿蒙:跨三端政务级应用的工程实践与深度复盘
前端·react native·harmonyos
这是谁的博客?7 小时前
微服务架构设计模式深度解析:从拆分策略到容灾机制
微服务·设计模式·云原生·架构·架构设计·后端开发·分布式系统
excel8 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
ZC跨境爬虫8 小时前
模块化烹饪小程序开发日记 Day7:(菜谱详情接口开发与JSON数据读取全流程)
前端·javascript·css·ui·微信小程序·json
এ慕ོ冬℘゜8 小时前
JS 前端基础面试题
开发语言·前端·javascript