pstack 拆解 05 · 领域与边界
模型只看到它打开的文件,照抄最近的形状。所以领域规则如果散在条件判断里,它就会在某个新路径上漏掉;校验如果散在业务逻辑里,它就会给你一种虚假的安全感。
这一篇讲两条:把领域编码进一个结构,把校验集中到边界。
model-the-domain
领域建模的背后是这条原则最想让人接受的那句话:程序等于数据结构加算法,而算法通常是平凡的,数据是核心。
在业务系统里,真正复杂的不是计算,是领域本身:一个订单有哪些状态,状态之间怎么迁移,哪些组合根本不可能发生,谁在什么条件下能改哪个字段。这些规则一旦散落在条件判断里,系统就要求每个读到它的人都把这些规则在脑子里重新拼一遍,而且每加一条新路径,就多一个可能漏掉的地方。
把这些规则编码进一个数据结构,代码的性质就变了:它不再是需要你逐处小心的东西,而变成从模型里自然掉出来的东西。这是函数式编程最核心的一着:代数数据类型(algebraic data type)——用和类型把“要么这样、要么那样”的领域编码进类型,让非法状态无法表达,用结构消灭分支。
举个最小的例子。你要表示一个订单“已归档”。常见写法是加一个布尔:{ status: 'active' | 'shipped'; isArchived: boolean }。它允许 status: 'active' 同时 isArchived: true,也允许 status: 'shipped' 同时 isArchived: false——四种组合里有两种没有意义,而编译器不会拦。和类型的写法是把它并进枚举本身:{ status: 'active' | 'shipped' | 'archived' }。表达不出来的组合就不存在,用到它的地方也不用再判“这两个字段谁优先”。
判断有没有做到,有一个很直接的信号:一个新功能是不是只是给已有的 if/else 又加了一支,或者又加了一个必须和另一个布尔保持同步的布尔。是的话,问题不在逻辑,在模型。
它有一句判据:把领域编码进一个数据结构,而不是散落到各个条件判断里。
原文为什么:写的时候选对结构很便宜,事后找回来就变成一次重构,而重构通常会被推迟。
它的做法是换结构。可以换成这些:
- 用状态机,替换散落的布尔、阶段和生命周期检查。
- 用带类型的对象或模型,替换松散的参数和重复的形状假设。
- 用 map、registry、查找表、可辨识联合,替换散到多个文件里的分支。
- 用 reducer 或命令事件模型,替换临时的状态修改。
- 用围绕一块领域知识组织的模块,替换"加载、校验、转换、保存"这种按执行顺序命名的模块。执行顺序不等于归属。
- 用队列、缓存、索引、图或规范化集合,来匹配真正的访问方式。
它同时刹车:不要硬塞抽象。如果当前的形状已经清楚、局部、且不太会长大,就选无聊的代码。对一个只增加间接、却不删分支和重复规则、也不消除非法状态的抽象,保持怀疑。
按执行阶段命名模块是另一个信号,它会在每一步里重复同一套领域规则。
boundary-discipline
边界纪律的背后是函数式架构里那条很实用的分层:功能核心加命令外壳(functional core, imperative shell)。这里的“外壳”指和外部世界打交道的那层:接参数、连数据库、调框架、写日志;它应该越来越薄。“核心”指业务逻辑,写成纯函数,不依赖框架,可以直接测试。 不纯的东西,IO、校验、框架接线、网络,都被挤到系统边缘那层薄壳里;系统的核心留成纯函数,可以脱离框架单独测试。
它还有一个更精确的说法:解析,不要校验。 在数据穿过边界的那一刻,把它从无类型的原始形态一次性解析成领域类型;进去之后,代码只信任类型,不再到处加防御性检查。散落各处的校验之所以不好,不是因为它们多余,而是因为它们给出一种虚假的安全感,同时让业务逻辑和框架绑死在一起。
它有一句判据:校验、类型收窄、错误处理,放在系统边界上;系统内部无条件信任。
原文为什么:把逻辑从框架的接线里拿出来,才能不依赖框架地测它。
它的做法是按位置分三层。
在边界上(CLI 参数、配置文件、外部 API、网络协议):校验,返回错误,防御性处理。
在系统内部:带类型的数据,错误向上传播,不重复校验,信任类型。
跨边界时:暴露领域概念,不要暴露边界自己的私有表示。通用机制留在里面,特殊策略放在边缘。
几个具体的落点。配置在解析时校验(那是边界),不要在业务逻辑里校验。原始数据在边界就解析成领域类型。不要把传输层、存储层、框架、wire 类型从公共接口再导出去。边界已经校验过的数据,不要在调用链深处再加一次多余的 nil 检查。
它的检验有两个问题。这份数据此刻正在穿过系统边界吗?不是的话,校验是冗余的。这能不能是一个纯函数,让外壳只负责调用?能的话就抽出来。
参考
原文都在 pstack 上游仓库(cursor/plugins):