接口:不同对象的共同形状

第 3 课 · 共 6 课 约 8 分钟

定义 Schedulable,给三种订单做属性映射,按接口一次查询、汇总三种类型。

本课目标

读完这一课,你将能够

  • 说出接口解决什么问题,以及它和对象类型有什么不同
  • 定义 Schedulable,把三种订单映射到它上面
  • 按接口一次查询、汇总多个对象类型,并避开同名不同义的坑

三种订单,一个问题

前两节讲的是对象和对象之间的连线。这一节换一种「连接」:把形状相同的类型放在一起用。示例工厂里有三种带到期日的东西:销售订单要在承诺日期前发货,工单要在计划时间前完工,采购订单要在预计到货日前到货。计划员每天早上却问同一个问题:这两周哪些快到期了?

没有接口,只有两条路:写三遍查询、三遍汇总;或者把三种订单塞进一个大对象类型,让一半属性永远为空。接口(Interface)是第三条路:它只描述形状,也就是有哪些属性、哪些关系、哪些动作,让形状相同的对象类型去实现。之后按接口查,三种订单一起出来。

对象类型接口
是什么具体的,背后是数据抽象的,只有形状
有对象吗有,可以有很多个没有,对象总属于某个具体类型
值从哪来数据源或 Action实现它的对象类型

什么时候值得抽接口?三次法则:两个类型相像,先观察;第三个类型又重复同样的属性和查询,就该抽了。将来出现第四种可排期的类型,让它实现同一个接口,已有的查询不用改。

定义接口,再做属性映射

在 AIDC 里,接口的属性通常由共享属性承载(也可以直接写在接口上,下一节会看到),所以先建 dueAt 和 status 两个共享属性,再建接口。每个接口属性可以标必填或可选:必填的,实现方一定要映射。

[
  { "kind": "sharedPropertyType", "apiName": "dueAt", "title": "Due date", "schema": { "type": "date" } },
  { "kind": "sharedPropertyType", "apiName": "status", "title": "Status", "schema": { "type": "string" } },
  {
    "kind": "interfaceType",
    "apiName": "Schedulable",
    "title": "Schedulable",
    "schema": {
      "properties": {
        "dueAt": { "required": true },
        "status": { "required": true }
      }
    }
  }
]

必填还是可选,要想清楚。必填是对每个实现方的硬要求,接口用得越广,越难收紧;可选属性可以先不映射,方便一个类型一个类型地接入。给已有实现方的接口加必填属性,或者把可选改成必填,会被标为破坏性变更。所以新属性先写成可选,等实现方都补齐再收紧。

实现方在自己的定义里写 implements:接口属性名指向自己的属性名,名字相同的可以省略。下面是三个对象类型各自 schema.implements 的内容。

{
  "salesOrder":    [{ "interface": "Schedulable", "properties": { "dueAt": "promisedDate" } }],
  "purchaseOrder": [{ "interface": "Schedulable", "properties": { "dueAt": "expectedAt" } }],
  "workOrder":     [{ "interface": "Schedulable", "properties": { "dueAt": "dueDate" } }]
}

这里有一条硬规则:必填的接口属性必须映射到类型相同的属性。示例工厂里有个现成的坑:销售订单的 promisedDate 和采购订单的 expectedAt 是 date,工单的 dueAt 却是 timestamp。直接让工单实现接口,定义会被拒:

workOrder.dueAt: type timestamp differs from Schedulable.dueAt (date)

办法是给工单加一个 date 类型的 dueDate,映射到接口的 dueAt,也就是上面代码里的写法。「快到期」按天算就够了,工单原来的 dueAt 不动。不要图省事把类型硬改掉:改属性类型是破坏性变更,会牵连所有用到它的应用。

按接口查询与汇总

实现之后,接口就是一个对象集的入口(interfaceBase),SDK 里写 client.interface("Schedulable")。属性用接口的名字,__apiName 告诉你每个对象是哪个具体类型。

const soon = await client.interface("Schedulable")
  .where({ dueAt: { $lt: "2026-10-13" } })
  .fetchPage({ $orderBy: { dueAt: "asc" }, $pageSize: 50 });
// each object carries __apiName: salesOrder, workOrder or purchaseOrder

const byStatus = await client.interface("Schedulable")
  .aggregate({ $select: { $count: "unordered" }, $groupBy: { status: "exact" } });

Postgres 方言只为符合条件的 Interface 生成视图。Interface 要有属性,名称不能超过 63 字节,且至少一个实现它的 Object Type 有视图。视图用 UNION ALL 合并有视图的实现类型,并增加 __objectType 和 __primaryKey 两列。

SELECT "__objectType", "__primaryKey", "dueAt", "status"
FROM "Schedulable"
WHERE "dueAt" < '2026-10-13'
ORDER BY "dueAt";

一个容易踩的坑:接口只统一了名字,没有统一含义。三种订单的 status 取值不一样,订单有 draft、confirmed,工单有 planned、released。按接口过滤「未完成」时,不要假设取值相同;需要的话,另加一个含义统一的属性,比如布尔的 isOpen,再映射进接口。

下一节给接口加三样东西:继承、关系,以及接口上的 Action。

要点

  • 接口只描述形状,没有自己的对象;对象类型实现它,之后按接口查就能跨类型。
  • 三个类型开始重复同样的属性和查询时,就该抽接口。
  • 必填的接口属性必须映射到类型相同的属性;类型不同,先解决类型,别硬改。
  • 入口是 interfaceBase 对象集(SDK 的 client.interface());Semantic 数据库里的接口视图把实现方合在一起。
  • 接口统一名字,不统一含义:同名属性的取值不同时要小心。

练一练

把三种订单接到 Schedulable 上

在自己的 ontology/ 目录里做。

写出 Schedulable 的三份定义(两个共享属性加接口),再给 purchaseOrder 写 implements。status 为什么可以不写映射?

小测

选一个答案,马上看解析。

Q1Schedulable 接口里存着哪些数据?

Q2工单的 dueAt 是 timestamp,接口的 dueAt 是 date。直接映射会怎样?

Q3什么时候最值得抽出一个接口?

延伸阅读