浅谈如何从状态机的角度来思考业务代码

本文通过实际业务例子举例,如何将状态机思想迁移并运用到实际业务开发场景中,从而帮助我们在开发中,简化复杂的多状态流转场景。

1. 实际问题

先描述一下需求背景,项目中要实现一个搜索页,搜索页的主要逻辑大致为:

  • 当用户未输入时,展示快捷输入面板

image

  • 当用户在搜索框输入后,展示联想输入的列表

image

  • 当确定搜索的关键词时,包含从快捷面板或联想面板选择、搜索框输入后回车确定,展示搜索结果列表

image

在对需求进行分析之后,可以总结出一个规律:根据用户的不同操作,需要呈现相应的功能

以上场景,恰好符合有限状态机的三个特征

  • 状态机(State)的个数是有限的
  • 在任一时刻,只处于一种状态
  • 当满足某种触发条件(Event),就会从某种状态转移(Transition)到另一种状态,并同时执行某个动作(Action)。

那么,采用状态机的思想,将对用户不同状态流转进行描述,是不是会更加清晰呢?

2. 概念

我们先来看一下,有限状态机四个核心概念的定义:

  • 状态(State):包括初始状态和事件触发后的状态,同时必须要有一个最终状态。
  • 事件(Event):触发状态机从一种状态切换到另一种状态。
  • 转移(Transition):表示从一个状态切换到另一个状态。引起状态迁移的事件被称为触发事件(triggering event)。
  • 动作(Action):表示在进行状态转移后要执行的具体行为。

3. 在实际的场景中,如何用状态机的思想抽象?

S1:抽离状态 —— 划分用户的操作阶段,枚举存在的状态

  • 输入前(初态)
  • 正在输入的状态
  • 输入后有结果(终态)

S2:定义动作 —— 找出转移到每种状态后对应的行为

  • 展示快捷输入面板
  • 展示联想输入的列表
  • 展示搜索结果列表

S3:明确事件 —— 找出触发状态发生转化的条件

通过一个状态转化图,描述状态间的转化关系:

image.png

4. 如何实现?

S1 :按划分的状态,定义变量的值

searchStatus用来表示搜索页的搜索状态,即before-输入前,doing-正在输入,after-确认输入结果(包括:输入关键词后回车确认、点击联想列表、以及热门和历史规则的情况)

data() {
  return {
    searchStatus: 'before', // 默认首次进入该页,为输入前的状态
    searchResult: '' // 记录搜索结果
  }
}
复制代码

S2 :按状态以及每种状态对应的动作,进行组件拆分,并与每个状态值关联

将整个搜索页拆成了两部分:

image.png

第一部分是:在三个状态下,都共有的搜索框

<div class="keyword-search">
  <search-input
    ref="searchInput"
    placeholder="搜索"
    @debounce-input="valueChange"
    @keyup="keyup"
    @cancel="cancelSearch"
  ></search-input>
</div>
复制代码

第二部分,则通过切换不同的状态,展示在三种状态下相应的组件。包括:

  • 热搜、历史搜索面板组件

在输入的初始态,应展示热搜、历史搜索面板。这里,用 this.searchStatus === 'before' 控制是否展示。

<search-option-panel
  v-if="searchStatus === 'before'"
  title="热搜规则"
  @click="selectHotResult"
></search-option-panel>
<search-option-panel
  v-if="searchStatus === 'before'"
  title="搜索历史"
  @click="selectKeywordOption"
  @clear="clearSearchHistory"
></search-option-panel>
复制代码
  • 联想输入的列表

在用户在输入框中,输入文字时,即输入中,应展示联想输入的列表,其中,包括用户输入无相关的联想词时,提示暂无内容相关的相关逻辑。这里,用 this.searchStatus === 'doing' 控制是否展示。

 <div class="related-keyword-list-wrapper" v-if="searchStatus === 'doing'">
  <div class="related-keyword-list" v-if="relatedKeywordList.length">
    <div class="keyword-option border-bottom-1px"
         v-for="(value, index) in relatedKeywordList"
         :key="index"
         @click="selectKeywordOption(value)">
      {{value}}
    </div>
  </div>
  <div v-else class="no-keyword-list-wrapper">
    <div class="no-data-icon"></div>
    <div class="no-data-tip">抱歉,未找到相关结果。</div>
    <div class="no-data-suggest">可以尝试别的关键词</div>
  </div>
</div>
复制代码
  • 搜索结果列表

在获取用户输入内容之后,应展示搜索结果列表。这里,使用 this.searchStatus === 'after' 控制结果列表的展示。当用户手动输入(通过回车确认)、或从搜索联想列表、以及热门和历史规则中选择后,设置 searchStatusafter,并将选择的搜索结果用 searchResult 存储,展示搜索结果列表。该部分的逻辑还包括用户输入无相关结果时,提示暂无内容相关的相关逻辑。

<list ref="list" v-if="searchStatus === 'after' && searchResult"></list>
复制代码

S3:根据事件触发,切换不同的状态值

(以下代码为了便于理解,只保留控制状态切换的关键逻辑)

methods: {
  // 选择热搜面板中关键词
  selectHotResult(val) {
    this.searchStatus = 'after'
    this.searchResult = val
  },
  // 选择历史搜索面板中关键词、选择搜索联想词
  selectKeywordOption(val) {
    this.searchStatus = 'after'
    this.searchResult = val
  },
  // 输入搜索词后,回车确认
  keyup($event) {
    if ($event.keyCode === 13) {
      this.searchStatus = 'after'
      this.searchResult = this.searchValue
    }
  },
  // 点击搜索框的取消按钮
  cancelSearch() {
    if (this.searchStatus === 'before') {
        this.$router.go(-1)
    } else {
      this.searchStatus = 'before'
      this.searchResult = ''
      this.$refs.searchInput.clearValue()
    }
  },
  // 在输入框输入关键字
  valueChange() {
    this.searchStatus = 'doing'
    this.searchValue = v
    this.searchResult = ''
    ...
  }
}
watch: {
  searchResult: {
    handler(value) {
      if (!value) {
        return
      }
      this.searchStatus = 'after'
      ...
    }
  }
}
复制代码

总结

状态机的思想,适用于需要根据多个状态控制不同功能的业务场景。开发者只需要聚焦于在对状态的控制上,以及关心渲染每种状态相对应的行为即可,每组的状态、行为以及结果,互相独立,即使每组之间若在后期发生变化,都是互不影响的。

在实际的开发场景中,当我们遇到状态多、分支复杂的情况,可以考虑用状态机的思想来梳理逻辑,从而帮助我们更清晰、更快速地梳理业务逻辑,避免使用庞大、繁琐的条件语句。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享