一张会自动长大的表格,加自动对账
📌 本部分为中文精读版作者的原创分析,原书没有对应章节,因此不提供英文对照。
一句话:属性就是贴在类上的一张“表格栏位”——它规定“凡是这个类的东西,都有一栏叫 X,可以填什么样的内容”。
先说生活直觉。
你去办一张表格。有的表格通用(“姓名”“日期”每一栏都得填),有的表格是专业表格(“血型”“过敏史”只有体检表上才有)。
属性就是类的“表格栏位设计”。 而它的妙处在于:子类的表格会自动继承父类的所有栏位。
Wine(酒)这张表有:颜色、酒体、生产者、葡萄品种。RedWine(红酒)这张表 = 上面四栏 + 单宁含量。Pauillac(波亚克酒)这张表 = 和 RedWine 完全一样(它没有额外要填的东西)。正式定义(本书第 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 继承而来,一处定义,两处生效。
这个差异的深层原因是:数据库的层次是“表”,本体的层次是“概念”。 表是平铺的容器,概念是可以论述的——所以才能说“红酒这一层才引入单宁含量”。
痛点的名字叫:类只是空壳,答不了任何问题。
原文第 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。
场景:produces 属性的值域懒得定义,写成 THING(本体里最一般的类)。
具体后果:任何东西都能被塞进“酒庄生产了什么”这个属性里——包括另一个酒庄、一个葡萄品种、一个产区。这个属性从一个语义关系退化为一个无类型指针。所有基于它的推理全部失效。
原文对此的判词:
不要选一个过泛的类(比如,不该把值域设成 THING),而要选一个能覆盖所有填充值的类。
场景:Wine.maker 和 Winery.produces 都靠人工录。
具体后果:两个方向的数据一定会漂移。 有人只填了酒的 maker,忘了更新酒庄的 produces;另一个场合又反过来。本来是同一条事实的两个视角,最后变成两份互相矛盾的记录。
原文的处方(5.1 节):把信息“两个方向都存一遍”是冗余的。……知识获取系统可以自动补上逆关系的值,从而保证知识库的一致性。
属性处在“类层次”和“实例数据”之间的那一层——它是结构,不是数据:
第4步 类层次(骨架:有哪些盒子)
↓
┌──────────────────┐
│ ★ 第5步 定义属性 ★ │ ← 骨架上的"接口契约"
│ ★ 第6步 定义约束 ★ │ ← 契约上的"验收标准"
└──────────────────┘
↓
第7步 实例(数据:往盒子里放东西、填表)
↓
应用推理 / 校验
三个关键定位:
| 触发条件 | 原文出处 |
|---|---|
| 类层次已经搭出骨架 | 第 3 章说“第 4、5 步紧密交织”,但属性必须先有可以挂的类 |
| 有新的属性需求 | 第 5 步:从术语清单里挑完类之后,剩下的词大多是属性 |
| 某个属性是所有子类共有的 | 第 5 步:body、color 应挂在 Wine,因为它是“能挂的最高层” |
| 某个属性只有部分子类共有 | 第 5 步:tanninLevel 应挂在 RedWine |
| 两个属性表达同一条事实的两个方向 | 5.1 节:该建逆属性了 |
| 某个属性值对大多数实例都一样 | 5.2 节:该设默认值了 |
| 两个类的实例不可能重叠 | 4.8 节:该声明互斥了 |
禁用条件一:类层次还没稳定的时候,不要批量定义属性。
理由:属性的挂载位置直接依赖类层次——“挂在能挂的最高层”这个“最高层”是由层次决定的。这正是原文在第 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(允许类)
→ 化简规则三条(见下)
③ 连边:有没有逆属性?
→ 有的话声明为逆属性,让系统自动补值保证一致性
规则一:D 里同时有类和它的子类 → 删子类(Anc 已隐含)
规则二:D 里有一个类的全部子类但没有它本身 → 改成只留那个类
规则三:D 里有一个类的“除少数之外”的全部子类 → 考虑换成一整个类
这三条规则背后的统一逻辑:
类层次本身就是“包含关系”的压缩表达。
把RedWine和Wine并列写进 range,等于说两遍同一件事——不是错,是重复;而重复会在维护时变成漏洞。
一组三元组,外加两条自动推理规则:
两条自动推理规则(属性输出的隐藏价值):
| 规则 | 从 | 推出 |
|---|---|---|
| 继承传播 | x ∈ Pauillac | x 有属性 color、body、maker、grape、tanninLevel |
| 逆属性补值 | maker(sterlingMerlot) = sterlingVineyards | sterlingVineyards.produces ∋ sterlingMerlot |
得到 A:一个结构化、可继承、可校验、可推理的属性体系。
牺牲 B:
| 牺牲了什么 | 具体代价 | 什么场景下会真的疼 |
|---|---|---|
| 灵活性 | 属性一旦挂在类上,整棵子树都被约束。挂在 Wine 上的属性,RoseWine 也逃不掉 | 当某一小撮子类其实不需要这个属性时——你只能把挂载位置上移(污染更多类)或下移(冗余更多处),两个方向都有代价 |
| 属性是(半)全局的 | 在多数系统里,同名属性共享同一套约束定义。想给不同类不同定义,需要额外机制 | 当 name 在 Wine 上该是“酒的名称”、在 Winery 上该是“酒庄名称”时,同名属性会打架 |
| 挂载层级是个“金发姑娘”问题 | 挂太高 → 假命题(精确度掉);挂太低 → 冗余(维护成本涨)。没有“两边都不吃亏”的选项 | 原文 4.7 节的 Experimenter 场景:多挂一个属性,所有实例都被塞进无关字段 |
| 逆属性的自动补值有前提 | 靠系统支持。没有系统支持时,得靠人工保证两个方向一致 | 报告三 9.3 的例子:6 条 maker 对 5 条 produces,一致率 83.3%,漂移是默认结局 |
| 表达力和推理成本的交换 | 基数、枚举、互斥写得越细,校验越强,但约束互相咬合后推理越贵 | 本体大到几千个属性时,一致性检查可能跑不完 |
最疼的那一刀,是“挂载层级的金发姑娘问题”。
它不是“选错了重选一次”的问题,而是:任何选择都要占一边的便宜、吃另一边的亏。 你只能问自己:在这个领域里,“说假话”和“要改多处”哪个更贵?
另一种疼法是“属性值 vs 默认值”的混淆(5.2 节)。这个不疼则已,一疼就是静默的数据损坏——不报错、不告警,只是所有数据都错了。
| 上游 | 喂什么 | 原文出处 |
|---|---|---|
| 第 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 上(只管红酒) | 精准,但每个省都得自己立一遍 |
| 县级文件(只管本县) | 属性挂在每个红酒子类上 | 最精准,但维护成本最高 |
原文那条规则的立法语言版本:
“属性应该挂在能够拥有该属性的最一般的类上”
⟺
“这条法应该由能够管住所有适用对象的最高一级机关颁布。”
tanninLevel 挂 Wine)。tanninLevel 挂到每个红酒子类)。RedWine)——能管住所有该管的,不越界管不该管的。这个比方还解释了 4.7 节 Experimenter 那个案例:
把Experimenter建成BiologicalOrganism的子类,相当于把“劳动法”的适用对象扩大到了“全人类”——虽然逻辑上“实验者确实是人”,但在这部法律的语境里,实验者不是劳动法意义上的“人”。立法范围必须服务于立法目的。
写过代码的人都熟悉这个:
name: string、maker: Winery)body: 'FULL'|'MEDIUM'|'LIGHT',必填/可选)inverse="true")chilled = false)这个比方能立刻暴露一个误区:在代码里你可以随时加一个可选字段,随便谁都能改类型定义。在本体里不行——因为本体是被多个下游依赖的契约,改一次属性定义,所有填过值的实例都要重新校验。
这就是“属性值不可改、默认值可改”这条区分在代码世界的对应物:默认值 ≈ 函数默认参数(调用方可以覆盖);属性值 ≈ 类型约束(sugar: 'SWEET' 写在 DessertWine 上,就像泛型约束——子类不能违反)。
这个比方专门用来理解继承 + 挂载层级:
判据:挂在“最上方的、所有该结果子的枝都在它下面的”那个节点。
tanninLevel 的果钩:往上找,Wine 太高(白酒那枝不该有),Pauillac 太低(只用管住红酒这一片)→ 挂在 RedWine 这一节。场景:一个 4 家酒庄、6 款酒的小本体。这个案例会串起继承闭包、挂载精确度、range 化简、逆属性一致性四件事。
类层次(复用报告一的 10 个类)。
属性定义:
| 属性 | domain | facet(值类型 / 基数 / range) |
|---|---|---|
color | Wine | Symbol / single / {RED, WHITE, ROSE} |
body | Wine | Symbol / single / {FULL, MEDIUM, LIGHT} |
maker | Wine | Instance / single / {Winery} ← 逆属性对 |
grape | Wine | Instance / multiple / {WineGrape},最小基数 1 |
tanninLevel | RedWine | Symbol / single / {LOW, MODERATE, HIGH} |
sugar | DessertWine | Symbol / single / 定值 SWEET |
produces | Winery | Instance / multiple / 待化简 ← 逆属性对 |
实例数据:
| 酒(实例) | 所属类 | maker 填了谁 |
|---|---|---|
sterlingMerlot | RedWine | sterlingVineyards |
sterlingChardonnay | WhiteWine | sterlingVineyards |
sterlingCabernet | RedWine | sterlingVineyards |
morgonBeaujolais | Beaujolais | chateauMorgon |
lafitePauillac | Pauillac | chateauLafite |
margauxRouge | Margaux | chateauMargaux |
逆属性 produces 的实际填值(人工录入,用来演示漂移):
| 酒庄(实例) | produces 填了哪些酒 | 条目数 |
|---|---|---|
sterlingVineyards | sterlingMerlot, sterlingChardonnay, sterlingCabernet | 3 |
chateauMorgon | morgonBeaujolais | 1 |
chateauLafite | lafitePauillac | 1 |
chateauMargaux | (空) | 0 ← 漏了 |
公式 1:继承的属性(同报告一公式 2)
公式 2:挂载精确度(同报告二公式 2)
公式 3:值域化简(同报告一公式 4)
公式 4:逆属性一致率(本报告新增)
| 符号 | 含义 |
|---|---|
| s | 正向属性(这里是 maker) |
| s⁻¹ | 它的逆属性(这里是 produces) |
| x | 填了 s 的实例(这里是酒) |
| s(x) = y | 酒 x 的生产者是酒庄 y |
| x ∈ s⁻¹(y) | 酒庄 y 的 produces 列表里确实有 x |
| 分子 | 两个方向对得上的事实条数 |
| 分母 | 正向填了值的事实总条数 |
值域 [0,1]。1.0 表示两个方向完全一致;低于阈值就该告警。
① 继承闭包(公式 1)
Local:Wine 加 {color, body, maker, grape}(4 个);RedWine 加 {tanninLevel};DessertWine 加 {sugar}。
| 类 | 计算 | |Slots| |
|---|---|---|
Wine | {color, body, maker, grape} | 4 |
WhiteWine | Slots(Wine) | 4 |
RedWine | Slots(Wine) ∪ {tanninLevel} | 5 |
Pauillac | = Slots(Medoc) = Slots(Bordeaux) = Slots(RedWine) | 5 |
DessertWine | Slots(Wine) ∪ {sugar} | 5 |
Port | Slots(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 |
|---|---|---|---|
Wine | 10 个(全部酒) | 6 个(红酒) | 0.60 |
RedWine | 6 个(红酒) | 6 个 | 1.00 |
提升 0.40。 而且方案 A(挂Wine)在语义上是错的——它断言“每一款酒都有单宁含量”。
原文的判词(第 5 步):再往上泛化一步——把tannin level挂到Wine类上——就不对了,因为我们不用单宁含量来描述白葡萄酒。
③ 值域化简(公式 3)
开发者把 produces 的 range 写成 D = {Wine, RedWine, WhiteWine, RoseWine}(“列全一点总没错”)。逐个检查是否有祖先也在 D 里:
| x | Anc(x) | 祖上在 D 里吗 | 判决 |
|---|---|---|---|
Wine | ∅ | 否 | 保留 |
RedWine | {Wine} | 是 | 删 |
WhiteWine | {Wine} | 是 | 删 |
RoseWine | {Wine} | 是 | 删 |
这不是“少写三行”的问题。 假设某天要引入SparklingWine(起泡酒):
· D 版本:得记得再加一行,否则起泡酒进不了produces的允许列表——一个静默的功能缺陷。
· D' 版本:SparklingWine ⊂ Wine,自动被覆盖,一行都不用改。
化简的真实价值 = 消除未来的静默缺陷。
④ 逆属性一致率(公式 4)
用 9.1 的两张表对照:
| 酒 x | maker(x) | 该酒庄的 produces 里有 x 吗? | 一致? |
|---|---|---|---|
sterlingMerlot | sterlingVineyards | ✅ | 1 |
sterlingChardonnay | sterlingVineyards | ✅ | 1 |
sterlingCabernet | sterlingVineyards | ✅ | 1 |
morgonBeaujolais | chateauMorgon | ✅ | 1 |
lafitePauillac | chateauLafite | ✅ | 1 |
margauxRouge | chateauMargaux | ❌(produces 是空的) | 0 |
这就是没有逆属性机制时的真实处境。 六条记录里有一条漂了——margauxRouge的生产者填了chateauMargaux,但chateauMargaux的produces是空的。
后果非常具体:查询“玛歌酒庄生产哪些酒”,答案会是空。而事实是它生产margauxRouge。同一条事实,两个方向,一真一假。
有了逆属性机制:填margauxRouge.maker = chateauMargaux的那一刻,系统自动把margauxRouge加进chateauMargaux.produces。Cons 恒为 1.0——不是靠人仔细,是靠机制保证。
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
⚠️ 两个方向的事实不一致——没有逆属性机制时,漂移是必然结局
| 代码块 | 对应哪一步 | 为什么 |
|---|---|---|
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 | 第七步·下游 | 这个数字直接说明“为什么需要逆属性机制”——靠人仔细是不可靠的,要靠机制 |