状态计算,权限计算,多条件值计算。这一次全部搞定!

1,243 阅读7分钟

首先我们先来看一个简单的需求,假设我们要计算一个按钮是否可用。规则如下⬇️

普通用户Vip用户管理员
工作日
非工作日

一、逻辑判断

首先我们很自然可以想到使用if else 的逻辑判断来模拟需求的每一个分支

const IsWorkDay // 是否是工作日
const UserType  // 用户类型

// 模拟所有逻辑分支
if (IsWorkDay)  {
  if (UserType === '普通用户') return true
  if (UserType === 'Vip用户') return true
  if (UserType === '管理员') return false
} else {
  if (UserType === '普通用户') return false
  if (UserType === 'Vip用户') return false
  if (UserType === '管理员') return true
}

这是一个很典型的面向过程的写法。我们按照需求依次输出逻辑分支判断。最终得到结果

当然上面的代码还可以进行逻辑分支合并,减少代码量

// 合并过后
if (UserType !== '管理员' && IsWorkDay) return true
if (UserType === '管理员' || !IsWorkDay) return true
return false

实际上上述代码的还是非常易于阅读,如果我们的需求真这么简单,这就是最好的写法:)

二、穷举所有状态,存表查询

如果我们换一个视角来看这个需求,我们的目的是获取多个 不同的维度值 下的结果。

如果我们把所有不同维度值的组合全部获取,并用某种算法对不同的组合生成 对应的唯一 的值,该指向最终的结果

这样一来,我们要做的事情就变成了 穷举所有状态实现生成唯一值的算法

再把生成的唯一值和结果关联起来,可以使用任意 数据结构,也可以使用 数据库表

这里用数据结构的方式示意:

// 关联表对象
const map = {}
const UserType = ['普通用户', 'Vip用户' , '管理员']
const IsWorkDay = ['工作日', '非工作日]

// 生存map表的唯一键
function genMapKey(...dims: string[]) {
    return dims.sort().join('-')
}

// 生成按钮状态
function genButtonStatus(dims: string[], state: boolean) {
    map[getMapKey(...dims)] = state
}

// 查询按钮状态
function queryButtonStatus(...dims: string[]) {
    return map[genMapKey(...dims)]
}
// 穷举所有的状态
// todo:此处还能优化吗?
genButtonStatus(['普通用户', '工作日'], true)
genButtonStatus(['Vip用户', '工作日'], true)
genButtonStatus(['管理员', '工作日'], false)
genButtonStatus(['普通用户', '非工作日'], false)
genButtonStatus(['Vip用户', '非工作日'], false)
genButtonStatus(['管理员', '非工作日'], true)

// 查询的时候
return queryButtonStatus('普通用户', '工作日')

对于上述代码,很明显我们维护的重点从逻辑分支的书写变成了 穷举状态算法

一般来说,算法不会经常变化,那么剩下的工作就是穷举状态而已。那么这样写又有什么问题呢?

  1. 增加了代码阅读的成本
  2. 穷举状态的代码很难维护:一旦出现了bug,难以定位是那条分支定义错了

三、自动穷举所有状态

从上面的代码可以看出,让我们自行穷举所有状态,是非常反人类的。并且这部分代码读起来让人头昏脑胀,四肢发麻...

还记得最开始的那个需求表吗?我们是否可以直接读取表的内容然后自动生成状态呢?

// 我们以md格式为例
假设需求存在rules.md文档,内容如下:
|  | 普通用户 | Vip用户 | 管理员 |
| --- | --- | --- | --- |
| 工作日 | ✅ |  ✅  | ❌ |
| 非工作日 | ❌ | ❌ | ✅ |

我们直接读取这个文件
// 省略实现,直接用nodejs即可
const content = readRuleMd()

const rows = content.split('\n').filter(Boolean).reduce((rows, row) => {
    rows.push(row.split('|').filter(Boolean))
    return rows
}, [])

rows[0].forEach((item, idx) => {
  if (idx === 0) return;
  rows.slice(1).forEach((row, index) => {
    // 注册状态和值
    genButtonStatus(item, row[0]) = row[idx] === '✅'
  });
});


很明显,自动将需求表中的内容穷举并存表。维护起来非常方便,甚至这部分文档的内容可以转交给产品同学来定义,一旦出问题了,产品背锅:)

题外话,如果这个需求规则表的内容是后端接口返回,这是不是就是一个动态的呢

下面我们将讨论其他几种值的情况,并将以一个真实需求为例

真实需求

在线地址

status.png 这是一个值计算,其中白色块就是对应的结果,返回一个数组

如果我们使用if else 的写法,相信我,写出来的代码没人想维护

那么如果使用存表查询呢?

首先我们还是可以采用前面的算法函数。至于状态规则定义,实际上也是大同小异。无非是新增了一些维度信息


const dim1 = ['条件1', '条件2' , '条件3']
const dim2 = ['根实体', '子实体']
const dim3 = ['树形实体', '非树形实体']
const dim4 = ['子实体一对多']
const dim5 = ['详情页', '列表页' , '其他页面']


genButtonStatus(['条件1', '详情页', '根实体', '树形实体'],  [2])
genButtonStatus(['条件1', '详情页', '根实体', '非树形实体'],  [1])
// 等等...

同样,我们也可以写一个自动注册的函数来穷举所有的状态定义。此处省略(我懒...

但是你会发现,维度的增加并没有给整体代码添加复杂度,多的只是状态表里面的几行数据而已

状态聚合

在前端开发中,有一种场景非常的常见,就是表单。 对于表单来说,状态是必不可少的一环:

  • 是否必填:提示用户的文案内容是什么
  • 是否禁用:原因是什么
  • 是否可见:和权限相关
  • 是否能修改:流程相关
  • 值:当前值、服务器上的最新值、修改前的值等等
  • 等等。。。

表单的每一个节点都是上述的状态的集合。如果我们有一种机制,可以把这些状态(值)的计算单独定义,并聚合到一个数据结构中,那么整个表单的状态管理将变的异常轻松。同时状态的抽离会让UI和逻辑解耦,那么跨端复用也将有方向可行。

而对于外部使用者来说,也可以很轻松的获取到整个表单的任意节点的任意状态

毋庸置疑,树是一个非常适合该场景的数据结构。表单对象本身就是一颗树,里面的每一个属性都是一个节点。

/**
 * 以下代码仅供参考,并没有真实调试过
 * 项目中真实代码远比这个复杂
 */
export class FormStatusTreeManager<T extends object> {

  value: T
  form: FormStatusTreeItem<T>


  getFormItem(path: string) {
    // 查找value中是否存在
    const item = ...
 
    if (item) return item

    return new FormStatusTreeItem(path)
  }
}

/**
 * 节点的状态
 */
export class FormStatusTreeItem <T>{
  // 状态表
  private statusMap = {}

  private value?: any
  private serverValue?: any

  // 子节点
  private children?; FormStatusTreeItem[]

  constructor(path: string) {
    this.path = path

    // 建立父子级节点关系
  }

  public get disabled(ctx: any) {
    const key = this.genDisabledStatusMapKey(ctx)
    return this.statusMap[key]
  }

  public genDisabledStatus(...args: any) {}
  public genDisabledStatusMapKey(...args: any) {}

  // 其他属性类似
  
}


const form = new FormStatusTreeManager()

// 自动创建状态树

form.getItem('name')
form.getItem('code')
form.getItem('user.id')
...

// 应用对应的规则
// 请不要纠结这些维度值,我们完全可以维护一个上下文来处理
form.getItem('name').genDisabledStatus(['后台管理员'], false)
form.getItem('name').genDisabledStatus(['id的值为空'], true)


// 正常使用
const item = form.getItem('name')


 <Input
  value={item.value}
  disaebld={item.value}   
  readonly={item.readonly} 
  error={item.error}
 />

 function onSubmit() {
  // 直接从form上获取对应的信息
  // 包括form的校验等
 }

如果大家使用过现在的各个UI库的form组件,就会发现他们也是在做状态聚合。

每当我们主动使用<Form.Item>的时候,每个<Form.Item> 就是创建了一个表单节点,所有的状态都可以绑定到对应的节点上,内部自然就可以构建成一个完整的树。

但是这种通过标签的声明式方式,会导致很难去做抽象,你必须要在他的基础上。再去做一套 FormCreator,然后通过一大堆的配置去生成标签节点树。同时你也无法主动获取当前某个节点的状态,除非UI库本身提供了这样的功能(实际上没有提供

所以我更推荐的一种做法是 FormStatus 这一层自己做,把所有的状态计算,逻辑等内聚到类里面,把UI库当成一个纯UI Component 来使用,放弃他提供的Form组件能力。

当积累到一定程度的时候,FormStatus的各个rule最后都可以可视化掉(你看看这个像什么...

正经总结

  1. 状态(值)计算简单的时候,直接面向过程写
  2. 复杂的、动态的状态(值)计算,可以参考存表查询的方式来做
  3. 多个不同维度状态描述同一个节点,我们可以往上层抽象。最后聚合出来一个解决方案

有问题欢迎和我讨论