mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
6.7 KiB
6.7 KiB
结构化值等价性
这份规范解决“两个结构化值是否表示同一事实”的判断。它不规定所有领域共享一个
isEqual工具;每个业务判断必须先说明比较的是完整快照、身份、集合还是有序序列。
先看正反例
// 同一完整快照:对象键的插入顺序不是业务语义。
const fromTransport = {
id: "record-1",
payload: { kind: "text", text: "hello", version: 1 },
};
const fromStore = {
id: "record-1",
payload: { version: 1, kind: "text", text: "hello" },
};
isDeepStrictEqual(fromTransport, fromStore); // true
// 身份相同不代表完整事实相同:值变化必须保留为不一致。
const conflictingStoreValue = {
id: "record-1",
payload: { version: 1, kind: "text", text: "different" },
};
isDeepStrictEqual(fromTransport, conflictingStoreValue); // false
// 反例:JSON 文本相同才算相等,把编码细节误当成业务语义。
JSON.stringify(fromTransport) === JSON.stringify(fromStore); // false
1. Scope / Trigger
在去重、冲突检测、幂等回执、缓存命中、持久化回读核验或安全策略判断中,需要决定两个对象、数组或嵌套 payload 是否等价时应用本规范。先由边界完成验证和规范化;不可把原始 unknown、host object 或序列化文本直接带入等价判断。
纯标量比较仍使用领域明确的 ===/范围比较。不要为了遵守本规范给简单标量、已规范化主键或只需比较一个字段的场景套递归比较。
2. Signatures
等价关系由拥有终态或写入决定的领域模块定义,名称必须表达关系,而不是导出无上下文的通用 isEqual:
type Equivalence<T> = (left: T, right: T) => boolean;
// 两侧已是同一 schema 的完整、规范化 plain-object 快照。
const sameSnapshot: Equivalence<CanonicalSnapshot> = (left, right) =>
isDeepStrictEqual(left, right);
// 仅当领域将成员定义为无序且无重复的集合时,另行定义集合等价。
const sameMemberSet: Equivalence<readonly MemberId[]> = (left, right) =>
sameValidatedMemberSet(left, right);
CanonicalSnapshot、MemberId 和集合约束由各自的 contract/model 所有;比较模块不能从序列化结果反推 schema,也不能替业务决定数组是否有序。
3. Contracts
- 先选择业务关系,再选择比较方式:主键相等、完整快照相等、指定字段相等和集合相等是不同契约,不能互相替代。
- 对已经由同一 schema 验证并规范化的完整 plain-object 快照,键的插入顺序不属于语义;在 Node 服务端使用
isDeepStrictEqual,或在其它运行时使用语义等价的结构比较。 - 结构比较仍比较字段名、值、值类型、嵌套值和数组顺序。数组只有在 contract 明确为无序集合时才可忽略顺序,且必须由领域比较器处理重复值与成员约束。
- 同一主键而其它规范字段不同是冲突,不是重复;必须走既有领域的不一致/未知/冲突处理,不能伪造成功终态。
JSON.stringify、数据库 JSON 文本、网络帧文本和日志文本只用于传输、展示或明确约定的 canonical hash 输入;未经显式 canonicalization,它们不得作为结构化业务值的等价判定。- 比较逻辑放在拥有该决定的最近领域边界。没有第二个消费者时不要抽成跨项目通用工具;有多个消费者时先把 schema、规范化和等价关系归属写清楚,再共享。
4. Validation & Error Matrix
| 输入和领域关系 | 应有行为 |
|---|---|
| 两个已验证完整快照仅对象键顺序不同 | 视为等价,继续既有成功/重复分支 |
| 主键相同但任一需要保真的字段、类型或嵌套值不同 | 视为不一致;不得确认、覆盖或发布为同一事实 |
| 数组顺序不同,contract 定义为序列 | 不等价 |
| 数组顺序不同,contract 明确定义为集合 | 用领域集合比较器比较成员和重复约束,不以递归比较或 JSON 文本猜测 |
| 任一输入尚未通过边界验证/规范化 | 先走既有解码或验证失败路径;不执行等价判断 |
| 只有序列化文本可用 | 回到拥有 schema 的边界取得结构化值;不得把文本相等当作事实等价 |
5. Good / Base / Bad Cases
- Good:持久化回读和入站回执都已被同一 contract 收窄为完整快照,终态 owner 用结构比较确认它们相同;仅对象键顺序变化不会改变结果。
- Good:成员列表 contract 明确为集合,领域 owner 用无序、无重复的成员比较器;它不把该规则推广到需要保留顺序的步骤列表。
- Base:只需要判断单一
id是否相同,直接比较该标量,不创建递归比较器。 - Bad:用
JSON.stringify(left) === JSON.stringify(right)判断业务事实;嵌套对象、数据库 JSON 重排或不同序列化器都会改变结论。 - Bad:只因复合主键相同就把两个不同完整快照当作重复,或为让比较通过而先全局排序、删除字段或补默认值。
6. Tests Required
- 为每一种完整快照等价判断构造两份独立对象:字段和值相同,但至少一层嵌套对象的键顺序不同;经真实判断边界后必须得到相同公开结果。
- 构造同一身份、一个受比较字段不同的对象;断言不会进入成功、提交、ACK 或发布分支。
- 当数组字段参与比较时,分别覆盖 contract 定义为有序序列和无序集合的情形;集合测试必须覆盖重复元素或由 decoder 明确拒绝重复。
- 通过实际 transport parse、持久化回读或 adapter(适用者)构造两侧值,不能让 fake service 和入站帧复用同一个对象字面量掩盖排序问题。
- 运行目标领域测试、严格类型检查、格式检查和 diff 审查;如比较位于服务端 Node 边界,覆盖构建产物测试。
7. Wrong vs Correct
// Wrong:序列化顺序和 formatter 实现不是事实语义。
const sameFact = JSON.stringify(stored) === JSON.stringify(incoming);
// Wrong:身份相同但完整快照不同仍被确认。
const sameFact = stored.id === incoming.id;
// Correct:两侧先被同一 schema 验证为完整快照,再按该关系的语义比较。
const sameFact = isDeepStrictEqual(stored, incoming);
if (!sameFact) return resolveInconsistentFact();
// Correct:仅在 contract 明确无序时,使用领域拥有的集合关系。
const sameMembers = sameValidatedMemberSet(stored.memberIds, incoming.memberIds);
这里的 resolveInconsistentFact 和 sameValidatedMemberSet 是领域占位名称;真实实现必须使用该领域既有的稳定错误/终态和其 contract 定义的成员约束。