Post

The Gap Between Description and Execution

Explores why describing a plan feels like progress but often substitutes for the harder work of actually doing it.

写表单校验的时候,我一直觉得有什么地方不对劲,但说不清楚。

字段多了之后,validateForm 就开始无限膨胀。name 要判断,age 要判断,hobby 要判断——每加一个字段,就往里面多塞一个 if

1
2
3
4
5
6
7
8
9
10
11
12
const validateForm = (formData) => {
  if (!formData.name) {
    return "请输入姓名";
  }
  if (!formData.age) {
    return "请输入年龄";
  }
  if (!formData.hobby) {
    return "请选择爱好";
  }
  return true;
};

功能上没毛病。但哪里不对劲呢?我后来想明白了——这段代码把「规则是什么」和「怎么执行校验」混在一起写了age 是必填这件事,是一条规则;但去对象里取值、判断是否为空,是执行动作。两件事搅在一起,规则就没法复用,也没法扩展。

比如我想给 age 再加一条「必须是数字、不能超过三位」——只能继续往 if 块里堆。这就是症结。


想清楚这一点之后,我突然觉得这个问题和函数本身的逻辑很像。函数定义是描述,函数调用是执行。 描述说的是”做什么”,执行才是真正在做。两件事要分开,才有复用的可能。

element-plus 的表单校验就是这个思路——先声明 rules 对象,这是描述;组件内部的校验器去跑这些规则,这是执行。两件事分得很清楚。

那能不能自己也这么搞?


先把规则描述出来

最简单的版本,写一个函数,接受字段名,返回一个描述对象:

1
2
3
4
5
6
const getValidationRules = (field) => {
  return {
    field: field,
    type: "notEmpty",
  };
};

调用三次,就得到三条规则描述:

1
2
3
4
5
6
7
8
9
10
11
12
[
  getValidationRules("name"),
  getValidationRules("age"),
  getValidationRules("hobby"),
]

// 得到:
// [
//   { field: "name", type: "notEmpty" },
//   { field: "age",  type: "notEmpty" },
//   { field: "hobby", type: "notEmpty" },
// ]

这些对象什么都不做,就是描述。后面交给真正的校验器去跑。描述和执行,干净地分开了。


但一个字段只能有一条规则,还是不够用

age 可能要同时满足:必填、是数字、不超过三位。一个 type 字段放不下。

这时候就需要让描述支持「链式追加规则」。用闭包把规则列表收起来,每个方法往里 push 一条,最后 exec() 一次性吐出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const field = (name) => {
  const rules = []

  const api = {
    notEmpty() {
      rules.push({ type: 'notEmpty' })
      return api
    },

    minLength(len) {
      rules.push({ type: 'minLength', len })
      return api
    },

    exec() {
      return { field: name, rules }
    }
  }

  return api
}

关键是每个方法都 return api,这样才能一直链下去。exec() 不再链,它是真正的出口——把攒好的描述吐出来,交给执行层。

用起来就是这个感觉:

1
2
3
const rules = field("name").notEmpty().minLength(3).exec();

// { field: 'name', rules: [{ type: 'notEmpty' }, { type: 'minLength', len: 3 }] }

整个表单的规则集中声明,清晰很多:

1
2
3
4
5
6
7
8
9
10
11
const rules = [
  field("name").notEmpty().minLength(6).exec(),
  field("age").notEmpty().minLength(3).exec(),
  field("hobby").notEmpty().exec(),
]

// [
//   { field: 'name', rules: [{ type: 'notEmpty' }, { type: 'minLength', len: 6 }] },
//   { field: 'age',  rules: [{ type: 'notEmpty' }, { type: 'minLength', len: 3 }] },
//   { field: 'hobby', rules: [{ type: 'notEmpty' }] }
// ]

这一坨描述本身不会报错,不会弹提示,什么都不做——它只是数据。后面传给校验器,才真正跑起来。


「描述」和「执行」分开,其实就是把”规则是什么”从”怎么跑规则”里剥离出来。前者可以随意组合、复用、扩展,后者只负责按规则办事。一旦想清楚这个边界,很多写起来越堆越乱的逻辑,其实都可以用这个思路重新拆一遍。

This post is licensed under CC BY 4.0 by the author.