表单或者领域模型多层嵌套时的验证如何写更优雅的思考(长文⚠️)

发布于:2024-04-25 ⋅ 阅读:(14) ⋅ 点赞:(0)

背景

其实,做表单验证,对前端来说,再正常不过了。

但场景复杂起来的时候,如果缺少一定的逻辑性,必然会带来可维护性以及性能的问题。

溯源

最开始的前端验证,一般步骤更为繁琐,一方面要获取值,一方面要做值的验证,而获取值的前提是对应的控件或者组件存在。在jq的时代,两者没有直接的绑定关系。

目前无论是vue 还是react都支持直接给组件设置属性加回调,或者直接提供model属性。这样的好处是,简化了组件的获取以及天然的耦合了组件和对应组件值。当我们需要知道某个控件的值对不对的时候,直接判断对应的关联数据是初始值还是某个设置值就好。当然,少数情况,组件也会把值重置,这时候就需要一个额外的属性与组件开关相关的,让我们可以知道这个属性是默认没有导致的还是用户主动操作设置的,这两者意义是不同的。

这个举个例子,比如用户新建一篇笔记,默认内容是空文本,但是用户操作过程中有反复修改的过程,如果修改过程的中间阶段,内容也是空文本,那我们的编辑态是认为用户新建的还是某个编辑态呢?这个会影响页面的一些交互功能。所以这时候,会有一个默认文本属性是空文本,然后会有一个文本控件内部的文本属性,这绝对要设置为两个,才能保证交互的独立性。

表单验证的当前模式

目前的大多表单,都会给一个表单设置一个form对象,里面的字段分配给各个控件,各个控件为了保证能够正确的设置和读取,又会设置一个具备唯一性的标识。这样就通过这种模式,轻松的能够根据form的设置,获取,验证三个方法快速的实现我们需要的基本要点。

而验证逻辑,是分散在每个控件里分配的validator方法里,这个方法会返回这个控件的一些基本需要的属性。

反思这种模式适合解决什么问题

适合解决常规的表单字段格式检验,比如邮箱密码的格式检验,这种不但相对固定,而且可以通过工具化的方式批量配置化。

那也一定有不适合解决的问题,比如下面这样的:

1 耦合了表单上下文环境的

2 验证逻辑比较复杂,不适合直接只用一个验证方法来实现的

3 验证依赖于其他控件的交互

4 不同控件验证顺序,逻辑流转,会随着业务变化很不一样

5 表单嵌套表单,表单嵌套组件,层层嵌套的,没有默认的设计模式支持你递归验证或者组合验证

4 有些非表单类的领域数据也需要复合到验证中,比如业务状态值

我遇到的具体问题

一个收银系统里的退单流程,支持部分退单,但是一个单可能有多个套餐,套餐类型里,可能有多个品,每个人可能有多个数量,后端给前端的领域单位是单品的订单行级别,订单行里给了这个品属于哪个套餐,套餐下可退的总数量。其中,用户针对其他情况是可退一个的,但套餐内不允许。

几种解决思路,

1 改变输入框的交互逻辑,每当发现数量的输入框是套餐的单品时,默认给它这个品的值设置为套餐可退数量,同时自动设置该套餐全部品全部退。相反,如果把某个品减少到0或者改为低于可退全部数量,就自动的把该套餐全部品数量改为0。这种交互模型下,用户提交给的每个套餐都是符合需求的,不需要额外验证。这时候,会发现很复杂的事情在前端交互上做限制很简单,这是因为用户的操作有过程性,只要过程的每一步能被前端监听到,那么过程就是对的,那么最终结果也是对的。但如果用户给前端的是非过程性的,那就只能用第三种结果性判断了,这种就可能是用户是直接粘贴或者导入表格拿到的一个结果,而非界面一步步输入。

2 收敛套餐品,把所有的套餐品聚合到一行,然后让用户只操作退套餐的数量,传给后端接口时,每个商品的退货数量和金额需要再处理一次。

3 核心只处理验证逻辑。步骤也简单,首先看到一个递进逻辑,单品小于套餐,一个单可能有多个套餐组。梳理之后,首先是根据标记套餐的订单商品行是否存在某个品退货数量小于标准数量,如果小于,直接熔断,后面逻辑不用判断。而套餐组和套餐包含关系,我们需要保证所有选了数量的单品的套餐组符合要求,但具体判断又是单套餐符合,所以这里也是一个熔断逻辑,任何一个套餐不满足就是不满足,提前返回。

特别说明的是,因为一个套餐里可能有多个品,所以针对套餐要做标记是否判断过,判断过的直接跳过,这里简化了便利遍历商品和遍历多次同一套餐。

延伸

从上面的第三点延伸,其实任何验证一旦真的复杂起来,一定会有几种常见的逻辑。

1 优先级顺序,需要优先判断的要有代码上的体现,这种最常见的是熔断逻辑,能决定结果了就直接返回,并且越优先的写在代码的越前面的位置。

比如,这种看下去就是按照顺序执行,并且优先级就是一定会先命中上面的,后面的有可能不被执行

if(opionA){
  if option
  return flag
  else doif(optionB){

}

2 职责链,都需要执行,每个的执行都会有个影响结果。这个在c端更常见,也许你有多个部分都填写的不对,但交互的弹框提示会只提示你最需要调整的,而不是第一个需要调整到的。那么你会说,为什么不开始就直接只判断那个优先级最高的,是因为优先级最高的,

2.1 可能是不符合正常逻辑的

2.2 前置条件需要先处理前面的部分,比如要前面不符合几项才会怎样。或者需要对比前面一项与当前的关系。

这种,常见的逻辑模型有,多场景取结果最优,多场景取运行最快,多场景选最优先命中而不是代码的顺序最先执行

3 递归,一个组里包含了多个单位模型,需要单位内先判断完成,才能知道组里的状态。这种包含两种模型,

一种是组内任何一满足,整组就满足。这种,默认为false,然后任何一个满足的时候改为true然后返回,跳出递归循环。

还有一种是组内都满足,整组满足。这种,默认为true,任何一个不满足改为false然后返回,跳出递归循环。

4 重复命中的缓存结果,必须有key以及结果,缓存之后,直接消费使用。比如一个模型有效的条件是七个符合三个,那么,如果前面已经有两个符合了,就要记录到已经符合的条件里,中间有不符合的跳过,有符合的追加到符合集合中,直到达到临界值跳出。这时候,不用再对之前已经判断过的验证再去检验。

5 状态机流转,相比职责链的完全移交,不可逆,没有主体,状态机形式的验证会通过核心点领域模型的状态值来实现一些效果,包括但不限于:

5.1 统计所有的可能性,并用一个标记位来归纳,而职责链更多的是通过复杂的逻辑判断入口,和增加处理的工作流来实现。职责链的好处是灵活的吸收上一步的结果,书写当前的执行门槛,以及决定下一步的输入条件,还是决定直接结束

5.2 每个状态下都能共享一些全局的数据,并且处理全局的数据,这时候,不能直接拿到上一步骤的结果,除非显性的传递

5.3 每个状态下具体执行的逻辑都可以完全不同

5.4 为了实现一定的状态流转,每个状态下,都需要在符合一定条件时,将当前的状态设置到另外一个状态

5.5 每个状态下,执行的动作,一般结果是幂等的,没有副作用的

6 事务的概念,在验证里,一定也有一些模式是单向的,不可逆的,当成功了就意味着不可逆,同时也意味着可能有一些重置或者回滚到某一个步骤,这个称为事务性。

常见的有,步骤页,调查问卷,登录页,考试答题模式,阅读已阅状态等。

这种,一方面要把批量动作和数据记录到栈内,方便回溯,一方面也要保留最初数据和最终数据,能够实现快速的重置和一键直达结果。

一个延伸结论

验证的所有领域模型是扁平的,没有逻辑关联和数据与库表一致准确性的时候,前后端谁做工作量是一致的,只是语言不同。

如果所有数据项是有优先级顺序,并且受到界面交互控制,是可以通过用户一步步操作演化而来的,前端做验证会简单很多,只要分别控制每一步操作,做自动的验证步骤以及这一步的关联数据即可。

反之,如果得到的数据不仅仅收到用户操作限制,还受到领域模型限制,库表真实数据的查询结果限制,这时候前端做这件事成本和后端几乎是一致的,唯一不同的是,后端还要做最后的结果是否满足入库的条件。这种情况下,后端有一个天然的优势,就是后端本来就有一些领域模型的dto以及属性的查询,设置,判断可以帮助他们节省一些数据的基本验证,而前端如果没有建立这样的数据模型,就要全靠手拼添加属性,判断属性,当然你可以通过引入ts,引入前端自己的类型规则,领域模型来类比后端做的方式,只是突然做这件事的心智被提高了。

个人主页