这篇文章介绍一种适合内部数据工具的代码结构范式,它遵循 Palantir 的 ontology 方法论:先把数据变成可理解的业务对象,再把对象变成可执行的动作,最后再把这些动作暴露给 CLI、WebUI 或其他应用。
核心不是“把代码分成几层”这么简单,而是先把系统里真正重要的东西定义出来:对象、关系、动作、权限,以及围绕这些能力搭建的应用。
如果要落到代码库里,最稳的做法通常是三段式:adapter、ontology、应用入口。adapter 负责把外部世界接进来,ontology 负责定义内部语义,应用入口只负责调用这些语义,不再自己解释数据。
三层结构
这类项目应该按同一条主线来组织:
- 从外部系统拉数据;
- 把数据冻结到 local storage (optional);
- 通过 ontology 层和轻量 CLI 暴露出来。
真正稳定的不是命令名,而是这条链路,以及链路上每一层的职责。
1 | |
这条线基本解释了代码库的大部分目录结构,也解释了为什么业务逻辑不应该堆进命令层。
这张图里最重要的不是箭头数量,而是边界:外部数据只从 adapter 进来,领域语义只在 ontology 里稳定,CLI 和 WebUI 都只是访问入口。
adapter 负责把世界变成可建模的数据
adapter 是唯一直接面对外部世界的一层。它的任务不是理解业务,而是把异构来源的数据整理成可建模、可版本化、可复用的输入。
它应该处理同步、转换、分页、批量拉取、字段归一化、主键对齐、时间分区对齐这些事。它可以复杂,但复杂必须停在这里。因为一旦外部系统的细节穿透到 ontology,后面的对象、动作和应用都会失去稳定性。
比如,入口层只应该把能力收拢起来:
1 | |
另一个入口层也只是一个路由器,向下分成 sync、query 和 report。
这说明入口层不是用来承载业务规则的。它只负责接入、分发和调用,真正的语义应该留在 ontology 里。
ontology 层定义对象、关系和动作
如果只保留一层,应该保留 ontology。
ontology 不应该只是“领域对象列表”,而应该至少包含三类东西:
- 对象:业务里的稳定名词;
- 关系:对象之间如何关联、归属、派生;
- 动作:对象能做什么、对象状态如何变化。
这类项目会把领域词汇放在中间层:
ProgramInvestmentEvaluationJiraIssueJiraFFM
它们可以是 dataclass 或者 Pydantic,这个不重要;重要的是对象是显式的。repository 层返回的是领域对象,而不是让原始 row 到处乱跑。
这样至少有四个好处。
第一,应用不再携带 schema 细节。它可以直接操作 Program、Investment、Evaluation,而不需要知道列名和 SQL 拼法。
第二,整个代码库会获得一套稳定的“句子”。一旦这些类型存在,后面的查询、报表、比较逻辑、权限判断都能复用同一套词汇。
第三,action 可以挂在 object 上,而不是散落成脚本。比如“审批”“标记”“导出”“生成报表”都应该是对象上的动作,而不是 CLI 里的临时分支。
第四,ontology 可以成为权限、审计和应用编排的共同入口。对象、属性、动作都能带上可见性和约束,应用只是按这些规则去消费它们。
应该保持同样的克制:
- repository 方法应该围绕业务对象命名,而不是围绕 SQL 语句结构命名;
- 分区解析、当前快照定位这类规则,也应该收在 repository 里;
- 对象关系、动作规则、权限约束应该由 ontology 统一表达,而不是散在各个入口里。
名字不同,但骨架应该一样:把数据模型做得乏味、稳定、可预测,并且足够语义化。
数据库边界只放在一个地方
数据库策略也应该只放在一个地方。
connection、默认路径、最新分区、过滤规则,这些东西最好都集中在一个地方。
这看起来很小,但恰恰是它让 ontology 能够稳定地站在上层。
如果没有这个边界,每个命令都会重复问同样的问题:
- 用哪个 DB 文件?
- 当前分区是哪一个?
- 这里拿到的是原始 row,还是领域对象?
边界放对了,这些问题就会留在 repository 里,不会跑到应用入口中间来。
应用入口只消费 ontology
CLI 不是中心,它只是 ontology 的一个消费端。
它可以暴露 sync、query、report 这类入口,也可以暴露更具体的领域动作,但它本身不应该变成策略中心。策略、领域规则、数据形状,都应该留在 adapter 和 ontology 里。
所以这套结构更准确的理解,不是“一个 CLI 加一个 repository”,而是:
- adapter 把外部数据接入;
- ontology 把数据组织成对象、关系和动作;
- CLI、WebUI、Bot 等入口消费这些动作。
只要 ontology 稳定,整个工具就稳定。
为什么这种结构有效
它有效,不是因为名字好听,而是因为它把“数据可用”提升成了“业务可操作”。
- adapter 负责和外部世界对话;
- ontology 负责把数据变成对象、关系和动作;
- 应用入口负责把人的请求映射到这些动作。
一旦职责分开,代码库长大时就不会把每个新命令都变成特例。
测试也会更轻松。真正值得测的,往往是对象关系、动作边界、权限约束和快照 fixture。那些缝隙只要稳住,剩下的大多只是 wiring。
这套结构的边界
这套结构当然也有边界。
它不适合把所有事情都抽象成一堆通用框架;它也不适合把 CLI 做成重业务层。它适合的是那种以业务对象为中心、以动作和权限为中心、需要不断扩展入口但又不能破坏语义的工具。
对这类工具来说,三层分工不是装饰,而是长期演化的前提。
如何复用
如果要做一个内部数据项目,应该直接从这套结构开始:
- 先把外部数据冻结成本地快照;
- 定义 ontology 层,放显式的业务对象、关系和动作;
- 让 repository 保持小而可预测;
- 让 CLI、WebUI、Bot 保持很薄;
- 把当前分区、DB 路径、权限边界这类问题留给 repository 和 ontology,而不是交给入口层。
这不是什么炫技架构,但它能让工具保持安静。
这样,复用性才会真正成立。