一等公民:能连 ≠ 能深
跨全仓设计公理。上位
docs/truth-system.md(判断函数)。 结论先行:通用抽象让你「连得上」,一等公民才让你「做得深」。深 = 把领域知识升为系统认识的一等能力,不是把通用抽象做得更精致。
0. 苹果的 AirPods:公理的原型
老蓝牙配对是通用抽象——所有设备一个模型:设备名 + 配对 + 连接。AirPods 在这套里就是列表里一行字 + 一个连接按钮,跟杂牌音箱没区别。通用抽象连得上 AirPods,但给不了 AirPods 该有的体验。
苹果没有去把通用蓝牙列表做得更好。它为 AirPods 单独做了一张弹出卡:开盖靠近就弹,AirPods 的图、左耳/右耳/充电盒分开的实时电量、一个连接大按钮——根本不走那个通用设备列表。
关键不在卡好看。在于那张卡把 AirPods 的领域知识做成了系统认识的东西:左右耳分开耗电、入耳检测、自动切换设备、空间音频。这些在「通用蓝牙设备」抽象里不存在——通用抽象只认识「一个设备」,不认识「一对会分开耗电、会感知你戴没戴的耳机」。
一等公民不是换个 UI,是把业务的领域知识变成系统认识的能力。
1. 公理
通用抽象 → 能连(覆盖广,每个对象都是抽象的一个实例)
一等公民 → 能深(领域知识成为系统的一等能力,而非被压扁的参数)
深 ≠ 把通用抽象做得更精致
深 = 让系统「认识这是什么」,于是该业务的难点变成系统能力通用化的代价是压扁:把世界塞进一个统一模型(设备、f(x)=y、资源),横得越宽,业务里真正难的东西在压扁中丢得越多。要把丢掉的捡回来,不是优化通用模型,是为该业务单独立一个认识它的一等公民。
2. 本仓的实例
域 | 通用抽象(能连) | 业务一等公民(能深) | 压扁丢了什么 |
|---|---|---|---|
蓝牙(苹果) |
| AirPods 弹出卡 | 左右耳分电量、入耳、切换 |
测试 | teact | aihand 的 DOM 命中 测试层 | 命中消歧、命中理由、DOM 魔鬼值(同名/隐藏/aria 盖文本) |
teact 是「纯函数测试」域的正确解,横扫全仓纯函数。但 aihand 的核心业务是「在 DOM 里做对决策」,被通用 teact 压成 expect(findElement(...)).toBe(submit)——退回裸 vitest、手搭 DOM、引用相等断言,「为什么选 Submit 而不是 Cancel」这条命中理由彻底丢失。findElement 的真 bug 不藏在 NaN,藏在「两按钮同名选谁」「一可见一 display:none 选谁」——DOM 语义的边界,通用 typegen 的 0/''/NaN 一个都撞不到。详见 packages/teact/DESIGN-verifier-role.md §8。
3. 决断标准:该单独做就单独做,别和稀泥
最容易犯的错是和稀泥:「通用框架做地基、业务做深井、两者不互斥」——这话拦住了"该为业务单独建层"的决断。苹果的答案干脆得多:该单独做就单独做,通用蓝牙列表它根本没碰。
判断「该不该为某业务立一等公民」:
通用抽象能连上它吗? 能 → 别急着满足,连上只是底线。
这业务的难点,在通用抽象里有名字吗? 没有(像「左右耳分电量」「DOM 命中消歧」在通用模型里根本不存在)→ 信号:该立一等公民。
立了之后,难点变成系统能力了吗? 是 → 立。只是 UI 更漂亮、抽象更精致 → 不是一等公民,别立。
通用抽象守住自己那个域做到极致(teact 守纯函数、CAS 守资源寻址);难的业务域单独长出认识它的一等公民层。两条线都做满,但别用"通用做地基"和稀泥拦住该单独做的决断。
4. 砍掉的(别加回来)
为了"通用"硬把业务塞进统一模型:DOM/组件/store 塞进
value→string只产假快照;这是压扁不是覆盖。「两者不互斥」式和稀泥:它听起来全面,实际是回避决断。该单独做就单独做。
把"一等公民"误解成换 UI / 加抽象层:一等公民的判据是「领域知识是否升为系统能力」,不是界面或代码结构好不好看。
元真理检查:去掉这条公理会怎样?→ 团队会无限优化通用抽象,以为"做得更精致 = 做得更深",而真正难的业务域永远被压成通用模型的一个贫瘠实例。有影响,留——它把「该不该为业务单独立层」从品味降成可判断的标准。