一种基于 Ontology + CLI 的代码结构范式

这篇文章介绍一种适合内部数据工具的代码结构范式,它遵循 Palantir 的 ontology 方法论:先把数据变成可理解的业务对象,再把对象变成可执行的动作,最后再把这些动作暴露给 CLI、WebUI 或其他应用。

核心不是“把代码分成几层”这么简单,而是先把系统里真正重要的东西定义出来:对象、关系、动作、权限,以及围绕这些能力搭建的应用。

如果要落到代码库里,最稳的做法通常是三段式:adapter、ontology、应用入口。adapter 负责把外部世界接进来,ontology 负责定义内部语义,应用入口只负责调用这些语义,不再自己解释数据。

三层结构

这类项目应该按同一条主线来组织:

  1. 从外部系统拉数据;
  2. 把数据冻结到 local storage (optional);
  3. 通过 ontology 层和轻量 CLI 暴露出来。

真正稳定的不是命令名,而是这条链路,以及链路上每一层的职责。

1
external system -> sync/adapter -> local storage (optional) -> ontology layer -> CLI command

这条线基本解释了代码库的大部分目录结构,也解释了为什么业务逻辑不应该堆进命令层。

架构图

这张图里最重要的不是箭头数量,而是边界:外部数据只从 adapter 进来,领域语义只在 ontology 里稳定,CLI 和 WebUI 都只是访问入口。

adapter 负责把世界变成可建模的数据

adapter 是唯一直接面对外部世界的一层。它的任务不是理解业务,而是把异构来源的数据整理成可建模、可版本化、可复用的输入。

它应该处理同步、转换、分页、批量拉取、字段归一化、主键对齐、时间分区对齐这些事。它可以复杂,但复杂必须停在这里。因为一旦外部系统的细节穿透到 ontology,后面的对象、动作和应用都会失去稳定性。

比如,入口层只应该把能力收拢起来:

1
2
3
4
5
cli.add_command(investment)
cli.add_command(program)
cli.add_command(evaluation)
cli.add_command(jira_issue)
cli.add_command(sync)

另一个入口层也只是一个路由器,向下分成 syncqueryreport

这说明入口层不是用来承载业务规则的。它只负责接入、分发和调用,真正的语义应该留在 ontology 里。

ontology 层定义对象、关系和动作

如果只保留一层,应该保留 ontology。

ontology 不应该只是“领域对象列表”,而应该至少包含三类东西:

  • 对象:业务里的稳定名词;
  • 关系:对象之间如何关联、归属、派生;
  • 动作:对象能做什么、对象状态如何变化。

这类项目会把领域词汇放在中间层:

  • Program
  • Investment
  • Evaluation
  • JiraIssue
  • JiraFFM

它们可以是 dataclass 或者 Pydantic,这个不重要;重要的是对象是显式的。repository 层返回的是领域对象,而不是让原始 row 到处乱跑。

这样至少有四个好处。

第一,应用不再携带 schema 细节。它可以直接操作 ProgramInvestmentEvaluation,而不需要知道列名和 SQL 拼法。

第二,整个代码库会获得一套稳定的“句子”。一旦这些类型存在,后面的查询、报表、比较逻辑、权限判断都能复用同一套词汇。

第三,action 可以挂在 object 上,而不是散落成脚本。比如“审批”“标记”“导出”“生成报表”都应该是对象上的动作,而不是 CLI 里的临时分支。

第四,ontology 可以成为权限、审计和应用编排的共同入口。对象、属性、动作都能带上可见性和约束,应用只是按这些规则去消费它们。

应该保持同样的克制:

  • repository 方法应该围绕业务对象命名,而不是围绕 SQL 语句结构命名;
  • 分区解析、当前快照定位这类规则,也应该收在 repository 里;
  • 对象关系、动作规则、权限约束应该由 ontology 统一表达,而不是散在各个入口里。

名字不同,但骨架应该一样:把数据模型做得乏味、稳定、可预测,并且足够语义化。

数据库边界只放在一个地方

数据库策略也应该只放在一个地方。

connection、默认路径、最新分区、过滤规则,这些东西最好都集中在一个地方。

这看起来很小,但恰恰是它让 ontology 能够稳定地站在上层。

如果没有这个边界,每个命令都会重复问同样的问题:

  • 用哪个 DB 文件?
  • 当前分区是哪一个?
  • 这里拿到的是原始 row,还是领域对象?

边界放对了,这些问题就会留在 repository 里,不会跑到应用入口中间来。

应用入口只消费 ontology

CLI 不是中心,它只是 ontology 的一个消费端。

它可以暴露 syncqueryreport 这类入口,也可以暴露更具体的领域动作,但它本身不应该变成策略中心。策略、领域规则、数据形状,都应该留在 adapter 和 ontology 里。

所以这套结构更准确的理解,不是“一个 CLI 加一个 repository”,而是:

  • adapter 把外部数据接入;
  • ontology 把数据组织成对象、关系和动作;
  • CLI、WebUI、Bot 等入口消费这些动作。

只要 ontology 稳定,整个工具就稳定。

为什么这种结构有效

它有效,不是因为名字好听,而是因为它把“数据可用”提升成了“业务可操作”。

  • adapter 负责和外部世界对话;
  • ontology 负责把数据变成对象、关系和动作;
  • 应用入口负责把人的请求映射到这些动作。

一旦职责分开,代码库长大时就不会把每个新命令都变成特例。

测试也会更轻松。真正值得测的,往往是对象关系、动作边界、权限约束和快照 fixture。那些缝隙只要稳住,剩下的大多只是 wiring。

这套结构的边界

这套结构当然也有边界。

它不适合把所有事情都抽象成一堆通用框架;它也不适合把 CLI 做成重业务层。它适合的是那种以业务对象为中心、以动作和权限为中心、需要不断扩展入口但又不能破坏语义的工具。

对这类工具来说,三层分工不是装饰,而是长期演化的前提。

如何复用

如果要做一个内部数据项目,应该直接从这套结构开始:

  1. 先把外部数据冻结成本地快照;
  2. 定义 ontology 层,放显式的业务对象、关系和动作;
  3. 让 repository 保持小而可预测;
  4. 让 CLI、WebUI、Bot 保持很薄;
  5. 把当前分区、DB 路径、权限边界这类问题留给 repository 和 ontology,而不是交给入口层。

这不是什么炫技架构,但它能让工具保持安静。

这样,复用性才会真正成立。