第二部分 · 报告三

属性(Slot)解剖报告

一张会自动长大的表格,加自动对账

📌 本部分为中文精读版作者的原创分析,原书没有对应章节,因此不提供英文对照。

一直击本质

一句话:属性就是贴在类上的一张“表格栏位”——它规定“凡是这个类的东西,都有一栏叫 X,可以填什么样的内容”。

先说生活直觉。

你去办一张表格。有的表格通用(“姓名”“日期”每一栏都得填),有的表格是专业表格(“血型”“过敏史”只有体检表上才有)。

属性就是类的“表格栏位设计”。 而它的妙处在于:子类的表格会自动继承父类的所有栏位。

正式定义(本书第 2 章):属性(slot,也叫 role 角色 或 property)描述类或实例的属性;属性的约束(facet,也叫 role restriction)是对属性取值的限制。

本书第 2 章还给了一个更形式化的视角(脚注 2):

类可以看成一元谓词——只有一个参数的问题,比如“这个对象是酒吗?”。
属性是二元谓词——有两个参数的问题,比如“这个对象的风味是什么?”
〔说人话〕这句话值得停一下:类回答“是不是”,属性回答“是什么/和谁”。 一问一答,一个主谓结构就齐了——这就是为什么本体能表达事实。

二差异特征:与【数据库表的列】的质的不同

这是最容易混的一对,因为两边都叫“字段”。

数据库表的列本体的属性
归属属于一张表,表之间靠外键连接挂在类层次上,会被子类继承
能否跨层复用不能。每张表要重新定义一遍列能。挂一次,整棵子树都有
能否指向对象外键是“一个值”(ID)值类型可以是 Instance,属性本身表达的是对象之间的语义关系
有无“反向”概念无。要反查得写 join有。maker 和 produces 是互为逆属性,填一边自动补另一边
约束的表达力类型、非空、唯一、外键类型 + 基数 + 枚举 + 互斥 + 挂载层级本身也是约束
运作逻辑存取:我要把这些数据存下来、取出来推理:从属性值推新属性值、从属性的约束校验数据合法性
适用场景数据要落库、要被查询知识要被共享、要被机器推理

最本质的那一条差异,是“继承 + 挂载层级”这两件事的组合。

在数据库里,你想表达“红酒有单宁含量、白酒没有”,你得:建两张表(wine_red、wine_white);把 4 个公共列在两张表里各写一遍;只在 wine_red 里加 tannin_level 列。

在本体里,你只需要:把 tanninLevel 属性挂在 RedWine 类上。公共属性由 Wine 继承而来,一处定义,两处生效。

这个差异的深层原因是:数据库的层次是“表”,本体的层次是“概念”。 表是平铺的容器,概念是可以论述的——所以才能说“红酒这一层才引入单宁含量”。

Wine 表格 颜色 酒体 生产者 葡萄品种 4 栏 = 4 栏 + RedWine 表格 颜色 酒体 生产者 葡萄品种 +单宁含量 = 波亚克 和 RedWine 完全一样 (没新栏目)
父类挂一次,整棵子树都有。一处定义,处处生效——数据库做不到这件事。
〔说人话〕用一句话概括:
数据库的列是“位置”,本体的属性是“承诺”。
列说“这里有个格子”。属性说“凡是这一类东西,都必须能回答这个问题”。前者管存取,后者管推理。

三存在意义:它解决了什么真实痛点

痛点的名字叫:类只是空壳,答不了任何问题。

原文第 3 章第 5 步第一句就是这个问题:

The classes alone will not provide enough information to answer the competency questions from Step 1.
(光有类,回答不了第 1 步那些能力问题。)

事故一:本体沦为一张废纸

只有类层次的本体,能力是“知道 A 是 B 的子类”。但你问它“波尔多配什么菜”“这款酒哪年产的”——它一无所知。

原文的 Winery / Wine 例子把这件事说得很清楚:类层次告诉你“Pauillac 是一种 RedWine”,但“这瓶酒是哪家酒庄生产的”这个信息,只有属性 maker 能承载。

事故二:属性挂错层级——机器开始说假话

场景:为了省事,把 tanninLevel 直接挂在 Wine 类上。

具体后果:本体开始产生假命题。 每一个白酒实例现在都被要求填“单宁含量”——这个字段对白酒毫无意义。原文的判词:

再往上泛化一步——把 tannin level 挂到 Wine 类上——就不对了,因为我们不用单宁含量来描述白葡萄酒。

量化损失(见报告二 9.3 的精确度):精确度从 1.00 掉到 0.60。

事故三:range 写成 THING——约束形同虚设

场景:produces 属性的值域懒得定义,写成 THING(本体里最一般的类)。

具体后果:任何东西都能被塞进“酒庄生产了什么”这个属性里——包括另一个酒庄、一个葡萄品种、一个产区。这个属性从一个语义关系退化为一个无类型指针。所有基于它的推理全部失效。

原文对此的判词:

不要选一个过泛的类(比如,不该把值域设成 THING),而要选一个能覆盖所有填充值的类。

事故四:逆属性没有机制——数据对不上

场景:Wine.maker 和 Winery.produces 都靠人工录。

具体后果:两个方向的数据一定会漂移。 有人只填了酒的 maker,忘了更新酒庄的 produces;另一个场合又反过来。本来是同一条事实的两个视角,最后变成两份互相矛盾的记录。

原文的处方(5.1 节):把信息“两个方向都存一遍”是冗余的。……知识获取系统可以自动补上逆关系的值,从而保证知识库的一致性。

〔说人话〕属性的存在意义,可以压成三句话:
1. 没有属性,类只是空壳——答不了任何问题。
2. 属性挂错层,本体开始说谎——精确度从 1.0 掉到 0.6。
3. range 太泛,约束形同虚设——语义关系退化为无类型指针。

四流程定位

1)它处于哪个环节或接口上?

属性处在“类层次”和“实例数据”之间的那一层——它是结构,不是数据:

第4步 类层次(骨架:有哪些盒子)
          ↓
   ┌──────────────────┐
   │  ★ 第5步 定义属性 ★  │  ← 骨架上的"接口契约"
   │  ★ 第6步 定义约束 ★  │  ← 契约上的"验收标准"
   └──────────────────┘
          ↓
第7步 实例(数据:往盒子里放东西、填表)
          ↓
       应用推理 / 校验

三个关键定位:

2)启动条件:什么时候必须启动它?

触发条件原文出处
类层次已经搭出骨架第 3 章说“第 4、5 步紧密交织”,但属性必须先有可以挂的类
有新的属性需求第 5 步:从术语清单里挑完类之后,剩下的词大多是属性
某个属性是所有子类共有的第 5 步:body、color 应挂在 Wine,因为它是“能挂的最高层”
某个属性只有部分子类共有第 5 步:tanninLevel 应挂在 RedWine
两个属性表达同一条事实的两个方向5.1 节:该建逆属性了
某个属性值对大多数实例都一样5.2 节:该设默认值了
两个类的实例不可能重叠4.8 节:该声明互斥了

3)禁用条件:什么情况下绝对不该用它?

禁用条件一:类层次还没稳定的时候,不要批量定义属性。

理由:属性的挂载位置直接依赖类层次——“挂在能挂的最高层”这个“最高层”是由层次决定的。这正是原文在第 3 步后面那句提醒的实质:接下来两步——建类层次、定义属性——是紧密交织的。很难先做完一个再做另一个。

具体后果:如果你在类层次还只有 Wine 和 RedWine 两层时就把 tanninLevel 挂在 Wine 上(因为当时没有 WhiteWine),等 WhiteWine 一加进来,这个属性的位置就错了,而且已经有一批实例填过值了。

禁用条件二:不要把每一个跨类的区分都做成属性。

这是 4.4 节那条规则的镜像:我们不应该为每一个额外的约束就建一个子类。 反过来同样成立:也不该为每一个区分都建识别性的属性。 如果某个区分只在极少数实例上有意义,它更适合作为该实例的局部注解,而不是全类共享的属性。

禁用条件三:属性值不能当成默认值用,默认值也不能当成属性值用。

这是 5.2 节的明确警告,两个方向的混淆都会出事:

混淆后果
把默认值当成属性值本来“大部分酒体饱满”,被你写成“所有酒体饱满”。博若莱、LIGHT 酒体的酒全被写成 FULL —— 数据被静默篡改
把属性值当成默认值本来“甜酒的糖分必须是甜”,被你做成可改的默认值。于是有人把一款 DessertWine 的 sugar 改成 DRY —— 本体出现自相矛盾,而且机器不报错

禁用条件四:不要在范围(第 1 步)还没定的时候定 range。

理由:range 收得紧不紧,取决于你要服务于什么应用。印酒标的工厂本体:pairingGoodFor 这个属性根本不需要存在。酒菜搭配本体:pairingGoodFor 是本体的核心,range 得定得很细。

禁用条件五:不要为了“表示得完整”而加属性。

原文 4.7 节的硬性禁令:本体不应该包含层次中类之间所有可能的属性和区分。……我们甚至没有为了“让术语之间的所有互连都表示得更完整”,而在本体里加入 favorite wine 和 favorite food 这类关系。

五操作白盒化

输入:喂给属性定义的是什么?

输入说明来源
术语清单的“剩余项”挑了类之后剩下的词——“剩下的词大多是属性”第 3 步
类层次决定属性能挂多高(挂载位置的边界由层次给出)第 4 步
能力问题决定需要哪些属性(答不上来的问题,反查缺哪个属性)第 1 步
领域范围决定 range 收多紧第 1 步

传导:内部如何处理?

传导链条是一串“定位—定义约束—连边”的动作:

① 定位:这个属性描述哪个类?
        → 挂在"能挂的最高层",但**不能产生假命题**
        → 落地规则:挂 RedWine(不能挂 Wine,因为白酒没有单宁)

② 定义约束:这个属性能填什么?
        ├─ 值类型:String / Number / Boolean / Enumerated / Instance
        ├─ 基数:single / multiple / 最小基数 / 最大基数
        └─ 若是 Instance:还要定 range(允许类)
        → 化简规则三条(见下)

③ 连边:有没有逆属性?
        → 有的话声明为逆属性,让系统自动补值保证一致性

化简规则的形式化(原文第 6 步的三条)

规则一:D 里同时有类和它的子类 → 删子类(Anc 已隐含)
规则二:D 里有一个类的全部子类但没有它本身 → 改成只留那个类
规则三:D 里有一个类的“除少数之外”的全部子类 → 考虑换成一整个类

这三条规则背后的统一逻辑:

类层次本身就是“包含关系”的压缩表达。
把 RedWine 和 Wine 并列写进 range,等于说两遍同一件事——不是错,是重复;而重复会在维护时变成漏洞。

输出:最终产出什么?

一组三元组,外加两条自动推理规则:

属性定义 = ⟨ slot, domain, facet ⟩,其中 facet = ⟨ type, cardinality, range ⟩

两条自动推理规则(属性输出的隐藏价值):

规则从推出
继承传播x ∈ Pauillacx 有属性 color、body、maker、grape、tanninLevel
逆属性补值maker(sterlingMerlot) = sterlingVineyardssterlingVineyards.produces ∋ sterlingMerlot
〔说人话〕这两条规则是属性真正“活”的地方。 第一条让“结构”自动变成“可查的字段”,第二条让“录一遍”等于“录两遍”。

六代价权衡

得到 A:一个结构化、可继承、可校验、可推理的属性体系。

牺牲 B:

牺牲了什么具体代价什么场景下会真的疼
灵活性属性一旦挂在类上,整棵子树都被约束。挂在 Wine 上的属性,RoseWine 也逃不掉当某一小撮子类其实不需要这个属性时——你只能把挂载位置上移(污染更多类)或下移(冗余更多处),两个方向都有代价
属性是(半)全局的在多数系统里,同名属性共享同一套约束定义。想给不同类不同定义,需要额外机制当 name 在 Wine 上该是“酒的名称”、在 Winery 上该是“酒庄名称”时,同名属性会打架
挂载层级是个“金发姑娘”问题挂太高 → 假命题(精确度掉);挂太低 → 冗余(维护成本涨)。没有“两边都不吃亏”的选项原文 4.7 节的 Experimenter 场景:多挂一个属性,所有实例都被塞进无关字段
逆属性的自动补值有前提靠系统支持。没有系统支持时,得靠人工保证两个方向一致报告三 9.3 的例子:6 条 maker 对 5 条 produces,一致率 83.3%,漂移是默认结局
表达力和推理成本的交换基数、枚举、互斥写得越细,校验越强,但约束互相咬合后推理越贵本体大到几千个属性时,一致性检查可能跑不完

最疼的那一刀,是“挂载层级的金发姑娘问题”。

它不是“选错了重选一次”的问题,而是:任何选择都要占一边的便宜、吃另一边的亏。 你只能问自己:在这个领域里,“说假话”和“要改多处”哪个更贵?

另一种疼法是“属性值 vs 默认值”的混淆(5.2 节)。这个不疼则已,一疼就是静默的数据损坏——不报错、不告警,只是所有数据都错了。

〔说人话〕权衡的实质:
属性用“结构上的提前承诺”换“运行时的可推理”。
承诺得太多(挂太高、range 太泛)→ 假命题;承诺得太少(挂太低、range 太窄)→ 冗余和返工。

而“属性值 vs 默认值”这个坑特别危险,因为它错了不报错。

七生态对接

上游:谁给它喂输入?

上游喂什么原文出处
第 3 步的术语清单“剩余项”——挑了类之后剩下的词大多是属性第 5 步
第 4 步的类层次硬依赖:挂载位置的“最高层”由层次决定第 5 步
第 1 步的能力问题决定需要哪些属性(答不了的问题反查缺什么属性)第 5 步
第 1 步的领域范围决定 range 收多紧第 6 步
知识表示系统的能力决定能不能声明逆属性、能不能设默认值、基数是单/多还是可以给上下界5.1、5.2、第 6 章
领域专家判断“哪个属性是所有子类共有的”——这是挂载层级的人工裁决第 5 步

下游:它的产出喂给了谁?

下游用途原文出处
第 7 步 实例实例填值填的就是属性——属性是实例的表格模板第 7 步
继承机制子类自动获得父类的属性第 5 步
逆属性机制填一边自动补另一边,保证一致性5.1 节
默认值机制新建实例自动填默认值5.2 节
互斥声明配合类的互斥,让系统能校验本体4.8 节
域/值域裁剪三条化简规则直接作用于 range第 6 步
分析工具Chimaera 这类工具诊断“属性挂载是否合理”第 7 章
应用层能力问题的答案最终从属性值里取第 1 章

反馈环:

定义属性 → 挂载到类 → 实例填值 → 应用跑起来
                                    ↓
                    发现"这个属性挂错层了"/"这个 range 太宽了"
                                    ↓
                            回头重定义 ←────┘

八旧知缝合

比方一(最好用的一个):像法律条文的立法层级

属性像法律条文,挂载层级像“这条法由哪一级立法机关颁布”。

法律世界本体世界后果
国家法律(管全国)属性挂在 Wine 上(管所有酒)定得对 → 一处生效;定错了 → 全国都得听错的
地方法规(只管本省)属性挂在 RedWine 上(只管红酒)精准,但每个省都得自己立一遍
县级文件(只管本县)属性挂在每个红酒子类上最精准,但维护成本最高

原文那条规则的立法语言版本:

“属性应该挂在能够拥有该属性的最一般的类上”
⟺
“这条法应该由能够管住所有适用对象的最高一级机关颁布。”

这个比方还解释了 4.7 节 Experimenter 那个案例:

把 Experimenter 建成 BiologicalOrganism 的子类,相当于把“劳动法”的适用对象扩大到了“全人类”——虽然逻辑上“实验者确实是人”,但在这部法律的语境里,实验者不是劳动法意义上的“人”。立法范围必须服务于立法目的。
国家法律(管全国) 禁燃令管到不该管的城市 ✗ 省级法规(管本省) 恰好管住所有该管的 ✓ 县级文件(管本县) 每个县都立一遍 ✗ ≈ 属性挂在 Wine 白酒也被塞进单宁栏 ≈ 属性挂在 RedWine 所有红酒都继承,白酒不受影响 ≈ 挂在每个红酒子类 改一处漏一处 「挂在能挂的最高层」⟺「由能管住所有适用对象的最高一级机关颁布」
医疗金融领域:「说假话」更贵 → 宁可多挂几处。快速原型:「改多处」更贵 → 宁可挂高一点。

比方二:像类的“接口定义”和“可选参数”

写过代码的人都熟悉这个:

这个比方能立刻暴露一个误区:在代码里你可以随时加一个可选字段,随便谁都能改类型定义。在本体里不行——因为本体是被多个下游依赖的契约,改一次属性定义,所有填过值的实例都要重新校验。

这就是“属性值不可改、默认值可改”这条区分在代码世界的对应物:默认值 ≈ 函数默认参数(调用方可以覆盖);属性值 ≈ 类型约束(sugar: 'SWEET' 写在 DessertWine 上,就像泛型约束——子类不能违反)。

比方三:像一棵树上的“挂果位置”

这个比方专门用来理解继承 + 挂载层级:

判据:挂在“最上方的、所有该结果子的枝都在它下面的”那个节点。

属性 = 类上的一张「表格栏位设计」
挂载层级 = 这份表格从哪一级开始生效
约束 = 这一栏的填写规则
逆属性 = 两栏之间的自动对账
全部学问就一句话:挂多高,才既不说假话、又不重复劳动

九数值案例串讲

场景:一个 4 家酒庄、6 款酒的小本体。这个案例会串起继承闭包、挂载精确度、range 化简、逆属性一致性四件事。

9.1 场景与数据

类层次(复用报告一的 10 个类)。

属性定义:

属性domainfacet(值类型 / 基数 / range)
colorWineSymbol / single / {RED, WHITE, ROSE}
bodyWineSymbol / single / {FULL, MEDIUM, LIGHT}
makerWineInstance / single / {Winery} ← 逆属性对
grapeWineInstance / multiple / {WineGrape},最小基数 1
tanninLevelRedWineSymbol / single / {LOW, MODERATE, HIGH}
sugarDessertWineSymbol / single / 定值 SWEET
producesWineryInstance / multiple / 待化简 ← 逆属性对

实例数据:

酒(实例)所属类maker 填了谁
sterlingMerlotRedWinesterlingVineyards
sterlingChardonnayWhiteWinesterlingVineyards
sterlingCabernetRedWinesterlingVineyards
morgonBeaujolaisBeaujolaischateauMorgon
lafitePauillacPauillacchateauLafite
margauxRougeMargauxchateauMargaux

逆属性 produces 的实际填值(人工录入,用来演示漂移):

酒庄(实例)produces 填了哪些酒条目数
sterlingVineyardssterlingMerlot, sterlingChardonnay, sterlingCabernet3
chateauMorgonmorgonBeaujolais1
chateauLafitelafitePauillac1
chateauMargaux(空)0 ← 漏了

9.2 公式

公式 1:继承的属性(同报告一公式 2)

Slots(c) = Local(c) ∪ ⋃p ∈ Super(c) Slots(p)

公式 2:挂载精确度(同报告二公式 2)

Precision(s, c) = | Instance(c) ∩ ShouldHave(s) | / | Instance(c) |

公式 3:值域化简(同报告一公式 4)

D' = { x ∈ D | ¬∃ y ∈ D, y ≠ x, y ∈ Anc(x) }

公式 4:逆属性一致率(本报告新增)

Cons(s, s⁻¹) = |{ x : s(x) = y ∧ x ∈ s⁻¹(y) }| / |{ x : s(x) ≠ ∅ }|
符号含义
s正向属性(这里是 maker)
s⁻¹它的逆属性(这里是 produces)
x填了 s 的实例(这里是酒)
s(x) = y酒 x 的生产者是酒庄 y
x ∈ s⁻¹(y)酒庄 y 的 produces 列表里确实有 x
分子两个方向对得上的事实条数
分母正向填了值的事实总条数

值域 [0,1]。1.0 表示两个方向完全一致;低于阈值就该告警。

9.3 手工验算

① 继承闭包(公式 1)

Local:Wine 加 {color, body, maker, grape}(4 个);RedWine 加 {tanninLevel};DessertWine 加 {sugar}。

类计算|Slots|
Wine{color, body, maker, grape}4
WhiteWineSlots(Wine)4
RedWineSlots(Wine) ∪ {tanninLevel}5
Pauillac= Slots(Medoc) = Slots(Bordeaux) = Slots(RedWine)5
DessertWineSlots(Wine) ∪ {sugar}5
PortSlots(RedWine) ∪ Slots(DessertWine)6
验算 Port:{color, body, maker, grape, tanninLevel} ∪ {color, body, maker, grape, sugar} = {color, body, maker, grape, tanninLevel, sugar},6 个。

关键观察:Port 同时具备 tanninLevel(来自红酒那一支)和 sugar(来自甜酒那一支)。这就是多重继承的实际收益——两个维度的属性在一个类上汇合。

② 挂载精确度(公式 2)

把 tanninLevel 挂在两个候选位置:

挂载位置Instance(c)其中该有单宁的Precision
Wine10 个(全部酒)6 个(红酒)0.60
RedWine6 个(红酒)6 个1.00
提升 0.40。 而且方案 A(挂 Wine)在语义上是错的——它断言“每一款酒都有单宁含量”。

原文的判词(第 5 步):再往上泛化一步——把 tannin level 挂到 Wine 类上——就不对了,因为我们不用单宁含量来描述白葡萄酒。

③ 值域化简(公式 3)

开发者把 produces 的 range 写成 D = {Wine, RedWine, WhiteWine, RoseWine}(“列全一点总没错”)。逐个检查是否有祖先也在 D 里:

xAnc(x)祖上在 D 里吗判决
Wine∅否保留
RedWine{Wine}是删
WhiteWine{Wine}是删
RoseWine{Wine}是删
D' = {Wine},   (4−1)/4 = 75% 的条目被删掉
这不是“少写三行”的问题。 假设某天要引入 SparklingWine(起泡酒):
· D 版本:得记得再加一行,否则起泡酒进不了 produces 的允许列表——一个静默的功能缺陷。
· D' 版本:SparklingWine ⊂ Wine,自动被覆盖,一行都不用改。

化简的真实价值 = 消除未来的静默缺陷。

④ 逆属性一致率(公式 4)

用 9.1 的两张表对照:

酒 xmaker(x)该酒庄的 produces 里有 x 吗?一致?
sterlingMerlotsterlingVineyards✅1
sterlingChardonnaysterlingVineyards✅1
sterlingCabernetsterlingVineyards✅1
morgonBeaujolaischateauMorgon✅1
lafitePauillacchateauLafite✅1
margauxRougechateauMargaux❌(produces 是空的)0
Cons(maker, produces) = 5/6 = 0.8333
这就是没有逆属性机制时的真实处境。 六条记录里有一条漂了——margauxRouge 的生产者填了 chateauMargaux,但 chateauMargaux 的 produces 是空的。

后果非常具体:查询“玛歌酒庄生产哪些酒”,答案会是空。而事实是它生产 margauxRouge。同一条事实,两个方向,一真一假。

有了逆属性机制:填 margauxRouge.maker = chateauMargaux 的那一刻,系统自动把 margauxRouge 加进 chateauMargaux.produces。Cons 恒为 1.0——不是靠人仔细,是靠机制保证。

9.4 Java 代码

import java.util.*;

/**
 * 属性审计器:实现本报告 9.2 节的四条公式
 */
public class SlotAudit {

    // ---------- 输入:类层次(第四步的产出) ----------
    static final Map<String, List<String>> SUPER = new LinkedHashMap<>();
    static {
        SUPER.put("Wine",        List.of());
        SUPER.put("WhiteWine",   List.of("Wine"));
        SUPER.put("RedWine",     List.of("Wine"));
        SUPER.put("RoseWine",    List.of("Wine"));   // 必须定义!否则 simplifyRange 看不见它,会误判
        SUPER.put("DessertWine", List.of("Wine"));
        SUPER.put("Beaujolais",  List.of("RedWine"));
        SUPER.put("Bordeaux",    List.of("RedWine"));
        SUPER.put("Medoc",       List.of("Bordeaux"));
        SUPER.put("Pauillac",    List.of("Medoc"));
        SUPER.put("Margaux",     List.of("Medoc"));
        SUPER.put("Port",        List.of("RedWine", "DessertWine"));
    }

    // ---------- 输入:属性挂在哪一层(第五步的产出) ----------
    // 关键设计:tanninLevel 挂 RedWine 而不是 Wine,对应"挂最高层但不能产生假命题"
    static final Map<String, Set<String>> LOCAL = new LinkedHashMap<>();
    static {
        LOCAL.put("Wine",        new LinkedHashSet<>(List.of("color", "body", "maker", "grape")));
        LOCAL.put("RedWine",     new LinkedHashSet<>(List.of("tanninLevel")));
        LOCAL.put("DessertWine", new LinkedHashSet<>(List.of("sugar")));
    }

    // ---------- 公式1:继承的属性 ----------
    static final Map<String, Set<String>> SLOT_CACHE = new HashMap<>();

    static Set<String> slots(String c) {
        Set<String> hit = SLOT_CACHE.get(c);
        if (hit != null) return hit;
        Set<String> acc = new LinkedHashSet<>(LOCAL.getOrDefault(c, Set.of()));  // 本层新增
        for (String p : SUPER.getOrDefault(c, List.of())) {
            acc.addAll(slots(p));                        // ∪ 并集:多重继承不重复计数
        }
        SLOT_CACHE.put(c, acc);
        return acc;
    }

    // ---------- 公式2:挂载精确度 ----------
    static double precision(Set<String> instances, Set<String> shouldHave) {
        if (instances.isEmpty()) return 0.0;
        Set<String> hit = new HashSet<>(instances);
        hit.retainAll(shouldHave);
        return (double) hit.size() / instances.size();
    }

    // ---------- 公式3:值域化简 ----------
    static Set<String> ancestors(String c, Set<String> seen) {
        Set<String> acc = new LinkedHashSet<>();
        for (String p : SUPER.getOrDefault(c, List.of())) {
            if (seen.add(p)) {                           // seen 同时兼作环检测
                acc.add(p);
                acc.addAll(ancestors(p, seen));
            }
        }
        return acc;
    }

    static Set<String> simplifyRange(Set<String> d) {
        Set<String> out = new LinkedHashSet<>();
        for (String x : d) {
            Set<String> anc = ancestors(x, new HashSet<>(Set.of(x)));
            boolean redundant = anc.stream().anyMatch(d::contains);   // 祖上也在集合里 → 冗余
            if (!redundant) out.add(x);
        }
        return out;
    }

    // ---------- 公式4:逆属性一致率 ----------
    static double inverseConsistency(Map<String, String> maker,           // 酒 -> 酒庄
                                     Map<String, Set<String>> produces) { // 酒庄 -> 酒集合
        long total = maker.values().stream().filter(Objects::nonNull).count();
        if (total == 0) return 1.0;
        long ok = maker.entrySet().stream()
                .filter(e -> e.getValue() != null)
                .filter(e -> produces.getOrDefault(e.getValue(), Set.of()).contains(e.getKey()))
                .count();
        return (double) ok / total;
    }

    public static void main(String[] args) {
        System.out.println("=== 公式1:继承的属性 ===");
        for (String c : List.of("Wine", "WhiteWine", "RedWine", "Pauillac", "Port")) {
            System.out.printf("  Slots(%-10s) |Slots|=%d  %s%n", c, slots(c).size(), slots(c));
        }

        System.out.println("\n=== 公式2:tanninLevel 挂哪里 ===");
        Set<String> allWines = Set.of("w1","w2","w3","w4","w5","w6","w7","w8","w9","w10");
        Set<String> redWines = Set.of("w1","w2","w3","w4","w5","w6");
        System.out.printf("  挂在 Wine    : Precision = %.2f%n", precision(allWines, redWines));
        System.out.printf("  挂在 RedWine : Precision = %.2f%n", precision(redWines, redWines));

        System.out.println("\n=== 公式3:值域化简 ===");
        Set<String> raw = new LinkedHashSet<>(List.of("Wine","RedWine","WhiteWine","RoseWine"));
        System.out.println("  原始 range : " + raw);
        System.out.println("  化简后     : " + simplifyRange(raw));

        System.out.println("\n=== 公式4:逆属性一致率 ===");
        Map<String, String> maker = new LinkedHashMap<>();
        maker.put("sterlingMerlot",     "sterlingVineyards");
        maker.put("sterlingChardonnay", "sterlingVineyards");
        maker.put("sterlingCabernet",   "sterlingVineyards");
        maker.put("morgonBeaujolais",   "chateauMorgon");
        maker.put("lafitePauillac",     "chateauLafite");
        maker.put("margauxRouge",       "chateauMargaux");

        Map<String, Set<String>> produces = new HashMap<>();
        produces.put("sterlingVineyards", Set.of("sterlingMerlot","sterlingChardonnay","sterlingCabernet"));
        produces.put("chateauMorgon",     Set.of("morgonBeaujolais"));
        produces.put("chateauLafite",     Set.of("lafitePauillac"));
        produces.put("chateauMargaux",    Set.of());                 // ← 漏录,导致漂移

        double cons = inverseConsistency(maker, produces);
        System.out.printf("  Cons(maker, produces) = %.4f%n", cons);
        if (cons < 1.0) {
            System.out.println("  ⚠️  两个方向的事实不一致——没有逆属性机制时,漂移是必然结局");
        }
    }
}

运行结果(已在本机 OpenJDK 17 上实际执行,与 9.3 手工验算一致):

=== 公式1:继承的属性 ===
  Slots(Wine      ) |Slots|=4  [color, body, maker, grape]
  Slots(WhiteWine ) |Slots|=4  [color, body, maker, grape]
  Slots(RedWine   ) |Slots|=5  [tanninLevel, color, body, maker, grape]
  Slots(Pauillac  ) |Slots|=5  [tanninLevel, color, body, maker, grape]
  Slots(Port      ) |Slots|=6  [tanninLevel, color, body, maker, grape, sugar]

=== 公式2:tanninLevel 挂哪里 ===
  挂在 Wine    : Precision = 0.60
  挂在 RedWine : Precision = 1.00

=== 公式3:值域化简 ===
  原始 range : [Wine, RedWine, WhiteWine, RoseWine]
  化简后     : [Wine]

=== 公式4:逆属性一致率 ===
  Cons(maker, produces) = 0.8333
  ⚠️  两个方向的事实不一致——没有逆属性机制时,漂移是必然结局

9.5 逐行对照八步

代码块对应哪一步为什么
SUPER 表第四步·启动条件类层次是定义属性的前置条件——没有层次,就没有“挂哪一层”这个选择
LOCAL 里 tanninLevel 挂 RedWine第二步·差异特征这是与“数据库列”最本质的差异点:挂载层级本身就是一条语义断言。数据库做不到
LOCAL 里 sugar 挂 DessertWine 且是定值第六步·代价权衡5.2 节的“属性值不可改”——用结构表达“甜酒必须是甜的”
slots() 的 addAll 和 SLOT_CACHE第五步·传导公式 1 落地。缓存对应“本体是图,祖先的属性会被反复查”
precision() 两个方案的对比第六步·代价权衡公式 2 落地。0.60 vs 1.00 就是“挂错层”的直接代价
ancestors() 里 seen 兼作环检测第四步·禁用条件4.1 节的“避免类环”——有环时 simplifyRange 会无限递归,必须挡住
simplifyRange() 的 anyMatch第五步·白盒化公式 3 落地,对应第 6 步的三条化简规则
inverseConsistency() 的双重过滤第七步·生态对接公式 4 落地。逆属性是“生态对接”最典型的机制:两个下游视图必须对账
main() 里打印 ⚠️ 一致率 0.8333第七步·下游这个数字直接说明“为什么需要逆属性机制”——靠人仔细是不可靠的,要靠机制
← 上一章 下一章:术语表与文献 →