共享 Schema 与 SchemaFactory

为什么改了一个 max,另一个表单也受影响?ArkTS 校验规则的共享实例问题

注册页要求用户名最多 8 个字符,资料编辑页允许 32 个字符。两个页面的规则很接近,把公共部分提取出来,看起来只需要几行代码。

但"共用规则定义"和"共用同一个规则实例"之间,有一个容易忽略的区别。

这篇用我维护的鸿蒙 ArkTS 校验库 @hmkit/validator,说明共享引用会怎样影响规则,以及 1.4.0 的 SchemaFactory 如何组织独立构造与派生。

示例使用 1.4.0:

bash 复制代码
ohpm install @hmkit/validator@1.4.0

1. 两个变量,可能仍是同一个 Schema

先看这个例子:

typescript 复制代码
import { liteV } from '@hmkit/validator/lite';

const base = liteV.string().required();
const shortName = base.max(8);
const longName = base.max(32);

console.info(`${shortName === longName}`); // true
console.info(`${shortName.validate('123456789').valid}`); // false
console.info(`${longName.validate('123456789').valid}`);  // false

9 个字符本来符合"最多 32 个字符"的要求,为什么 longName 也没有通过?

原因有两层:

  • 当前基础链式方法会修改自身,并返回当前实例。这里的三个变量引用同一个对象。
  • .max() 会追加一条规则。继续调用 .max(32),不会自动删除前面的 .max(8)。

因此,这个实例同时包含两个最大长度约束,输入需要同时通过它们。最多 8 个字符的限制仍然生效。

这是当前可变 API 的行为约定。需要分别维护两种业务规则时,应让它们拥有独立实例。

2. 先用构造函数建立独立实例

最直接的解决方式,是复用创建规则的函数:

typescript 复制代码
import { liteV, StringSchema } from '@hmkit/validator/lite';

function createNameRule(maxLength: number): StringSchema {
  return liteV.string().required().max(maxLength);
}

const registerName = createNameRule(8);
const profileName = createNameRule(32);

console.info(`${registerName === profileName}`); // false
console.info(`${registerName.validate('123456789').valid}`); // false
console.info(`${profileName.validate('123456789').valid}`);  // true

这里复用的是"如何创建规则",每次调用都创建新的 StringSchema。

如果业务只有两三个简单变体,普通函数已经足够清楚。随着字段组合、派生配置增加,也可以把创建与派生组织成统一接口。

3. 新建外层对象,也要看内部引用

对象派生还有一处需要分清。

partial() 返回新的对象 Schema,并独立维护哪些字段允许缺失;内部字段仍可能引用相同的子 Schema:

typescript 复制代码
import { liteV } from '@hmkit/validator/lite';

const sharedName = liteV.string().required();
const original = liteV.object({ 'name': sharedName });
const patch = original.partial();

sharedName.max(8);

const input: Record<string, Object> = {
  'name': '123456789'
};

console.info(`${original === patch}`); // false
console.info(`${patch.validate({}).valid}`); // true
console.info(`${original.validate({}).valid}`); // false
console.info(`${original.validate(input).valid}`); // false
console.info(`${patch.validate(input).valid}`);    // false

patch 允许 name 缺失,并没有把 original 的字段也变成可选。但是,name 出现时,两者仍使用 sharedName 进行校验。

因此,"外层 Schema 不同"还不足以证明整个可变规则图已经隔离。对象字段、数组元素、联合分支和递归结构,都需要考虑子规则的创建位置。

4. 用 SchemaFactory 组织构造和派生

1.4.0 新增了可选入口 @hmkit/validator/reuse:

typescript 复制代码
import { liteV, StringSchema } from '@hmkit/validator/lite';
import { SchemaFactory } from '@hmkit/validator/reuse';

const names = new SchemaFactory<StringSchema>(
  (): StringSchema => liteV.string().required()
);

const shortNames = names.derive(
  (schema: StringSchema): StringSchema => schema.max(8)
);

const longNames = names.derive(
  (schema: StringSchema): StringSchema => schema.max(32)
);

const shortRule = shortNames.create();
const longRule = longNames.create();

console.info(`${shortRule.validate('123456789').valid}`); // false
console.info(`${longRule.validate('123456789').valid}`);  // true

它的执行过程可以分成三步:

  1. 保存基础构造函数。
  2. derive() 保存派生配置,此时不执行构造。
  3. 每次 create() 都重新执行基础构造,再应用相应配置。

这样,各个变体可以共用创建逻辑,实际执行校验时使用各自的规则实例。派生配置也可以返回不同输出类型的 Schema,具体类型由返回的规则决定。

构造与派生回调可能被多次执行,因此适合放规则创建和配置逻辑。请求接口、写入外部状态等副作用应放在相应业务流程中处理。

5. 子规则也要在构造函数里创建

例如,一个资料对象包含姓名和标签数组,可以这样组织:

typescript 复制代码
import { liteV, ObjectSchema } from '@hmkit/validator/lite';
import { SchemaFactory } from '@hmkit/validator/reuse';

function createProfileSchema(): ObjectSchema<Record<string, Object>> {
  return liteV.object({
    'name': liteV.string().required(),
    'tags': liteV.array(liteV.string().required())
  });
}

const profiles = new SchemaFactory<ObjectSchema<Record<string, Object>>>(
  createProfileSchema
);

const firstProfile = profiles.create();
const secondProfile = profiles.create();

这里的对象、姓名规则、数组和数组元素规则,都在每次构造时重新创建。

相反,如果回调只是返回外部已经存在的实例:

typescript 复制代码
const shared = liteV.string().required();
const reused = new SchemaFactory<StringSchema>(
  (): StringSchema => shared
);

多次 create 仍然会得到 shared。SchemaFactory 执行你提供的构造函数,不会自动复制它返回的对象。

同样,可变默认对象、递归图以及自定义回调捕获的外部状态,也需要按照实际使用方式管理。

6. 为什么采用重新构造的方式

Schema 可能包含函数、插件、元数据和递归引用,简单的 JSON 序列化无法保留这些执行语义。

另一方面,原有链式方法采用可变约定,直接把所有方法改成返回新实例,会影响已有调用方式。

1.4.0 采用显式工厂,让需要隔离的业务变体从新的规则图开始,同时保留旧 API 的调用习惯。它提供的是按构造约定实现的隔离方案,任意已有 Schema 的通用深拷贝仍需要单独设计。

在项目中,我建议把规则构造函数放到独立模块,按场景创建实例;需要统一管理变体时,再使用 SchemaFactory。检查复用问题时,除了比较外层对象,也要核对内部可变子规则的来源。

开源地址与说明

欢迎带着可复现的规则组合和输入数据反馈问题,尤其是嵌套规则、递归结构与自定义插件的复用场景。

相关推荐
holly就是我2 小时前
AI 编程助手为什么跑偏?用开源 ccglass 看清 Prompt、工具调用和 Token 消耗
人工智能·后端
屏什么录2 小时前
录屏超过两小时就卡顿崩盘?从参数调优到代码实现,一文彻底搞定长时间录屏
前端·音视频开发
用户788477316342 小时前
小程序后端三选一:云开发、.NET WebAPI、Node,我按客户现状分了三种答案
后端
aiopencode2 小时前
局域网设备抓包怎么做?用 TraceEagle 抓电视、游戏机和 IoT 流量
后端·ios
计算机魔术师2 小时前
950亿怎么拆:Meta给AI基建配的三档弹药
前端
智驭未来掌门人2 小时前
CLion 开发 Qt 项目的优势
后端
碳基生物猛吃膳食纤维2 小时前
结合DEEPSEEK和CHATGPT弄了个小脚本,批量检查商品主图
前端
绝世唐门三哥2 小时前
CSS 换行三剑客:overflow-wrap、word-break 与 white-space 完全指南
前端·css
EnCi Zheng2 小时前
后端怎么把AI接口稳定安全低成本接进业务:超时重试、密钥隔离与成本熔断清单
人工智能·后端·安全·架构·api