返回文章列表
/更新于

文档分类要先定轴,再谈定义行为和定义实现

比起先说产品文档还是技术文档,更重要的是先固定统一的分类轴。沿内容性质这条轴看,很多文档都在定义行为,或者定义怎么实现。

  • Documentation
  • Design
  • Product
  • Library
  • Architecture

很多文档之所以越写越乱,不是因为内容不够多,而是因为分类时没有先固定逻辑轴。

一旦分类轴没定住,后面的讨论就会不断滑动:

  • 一会儿按职能分
  • 一会儿按内容性质分
  • 一会儿又按系统层级分

最后看起来像是在讨论“文档怎么分类”,其实是在混用几套不同的标准。

我现在更倾向于先把一个前提说清楚:

文档的每一种划分,都必须基于同一条逻辑轴。

在这个前提下,沿“内容性质”这条轴去看,很多文档里确实混着两种不同的问题。

一种问题是:

这个东西应该表现成什么样。

另一种问题是:

这个东西准备怎么做出来。

如果换个更直接的说法,那么沿着“内容性质”这条轴,很多文档都可以先分成两类:

  • 定义行为
  • 定义怎么实现

不管你写的是产品文档,还是 library 文档,这个观察都成立。

一、先定分类轴,比先分桶更重要

讨论文档分类时,至少常见有几条完全不同的轴:

  • 按职能领域分:产品、设计、研发、测试
  • 按内容性质分:行为、实现、约束、流程
  • 按系统层级分:系统级、子系统级、组件级、函数级
  • 按生命周期分:需求、设计、开发、验收、运维

这些轴都可以用,但不能混着当成同一层分类。

例如:

  • 产品文档 / 技术文档,更接近按职能领域分
  • 行为文档 / 实现文档,更接近按内容性质分
  • 系统级 / 组件级,更接近按抽象层级分

如果不先说明自己是在沿哪条轴分类,后面越讨论细节,边界越容易打架。

二、什么叫“定义行为”

定义行为,核心是在回答:

  • 系统对外应该做什么
  • 输入和输出是什么
  • 使用者会看到什么结果
  • 哪些情况算成功
  • 哪些情况应该失败
  • 边界和约束是什么

这类文档关注的是外部可观察结果,而不是内部代码怎么组织。

换句话说,它描述的是:

如果我是使用者,我应该期待这个系统怎么表现。

这类内容通常包括:

  • use case
  • 用户路径
  • 页面和状态变化
  • API contract
  • 错误语义
  • 数据约束
  • 示例输入输出
  • acceptance criteria
  • 测试用例的业务层描述

这里最重要的一点是:

行为文档不是“泛泛而谈的目标描述”,而是应该能被验证。

更进一步说:

定义行为的文档,应该配套相应的验收测试。

因为如果一个行为没有验收方式,它就很容易退化成口头理解、模糊共识,或者一段谁都能各自解释的描述。

也就是说,一个行为定义如果写得足够好,后面不只是“可以参考一下”,而是应该能自然落成对应的验收资产,例如:

  • E2E 测试
  • API contract test
  • 示例驱动测试
  • 验收标准

对于产品行为,这种验收通常表现为:

  • 用户路径级 E2E
  • 页面状态和错误态校验
  • 表单和流程的 acceptance test

对于 library 行为,这种验收通常表现为:

  • contract test
  • golden test
  • 输入输出示例测试
  • 错误语义测试

所以更准确地说,行为文档本身就应该带着“如何被验收”的意识去写。

三、什么叫“定义怎么实现”

定义怎么实现,核心是在回答:

  • 系统内部准备如何分层
  • 模块之间如何协作
  • 数据如何流动
  • 哪些步骤先做,哪些步骤后做
  • 哪些点 fail-fast
  • 性能、缓存、扩展性怎么处理
  • 当前代码准备怎么落地

这类文档关心的是内部结构和实现路径。

换句话说,它描述的是:

如果我是实现者,我应该按什么结构把这个系统做出来。

这类内容通常包括:

  • architecture overview
  • module split
  • pipeline stages
  • data model
  • storage schema
  • state machine internals
  • algorithm choice
  • trade-off
  • code anchor
  • rollout plan

这里要注意:

实现文档不是行为文档的重复版。它不应该重新发明需求,而是应该在已经明确行为约束之后,解释如何用一套稳定结构把这些行为实现出来。

四、如果固定在内容性质这条轴上,它比“产品文档 / 技术文档”更有意义

“产品文档”和“技术文档”这个分法当然也有用,但它不够本质。

因为它主要是在按作者角色或领域归档,而不是按文档真正回答的问题来归类。

例如一篇 API 文档,很多人会把它算成技术文档。

但如果它主要在定义:

  • 对外能力
  • 输入输出 contract
  • 错误语义
  • 成功条件

那它本质上其实是在定义行为。

反过来,一篇产品方案文档,看起来像产品文档,但如果它主要在讨论:

  • 前后端如何拆分
  • 哪些逻辑放客户端,哪些放服务端
  • 数据模型怎么组织
  • 分阶段如何落地

那它本质上已经是在定义实现。

也就是说,在这里两者其实不是同一条轴上的分类:

  • “产品 / 技术”更像领域标签
  • “行为 / 实现”更像认知标签

前者只能告诉你这篇文档大概属于哪个职能范围。

后者才能直接告诉你:

  • 这篇文档在约束什么
  • 读者应该怎么使用它
  • 哪些内容应该被测试验证
  • 哪些内容未来可以调整重构

所以如果已经明确自己是在沿“内容性质”这条轴分类,那么“行为 / 实现”这个分法通常更有操作性。

因为它直接影响:

  • 文档怎么写
  • 文档怎么拆
  • 评审时看什么
  • 测试该跟着哪类文档走

五、产品设计文档里,这两类都存在

很多人一提产品文档,第一反应就是 PRD。

但如果往工程里走深一点,你会发现产品相关文档其实天然就分成两层。

第一层是行为定义,例如:

  • 用户是谁
  • 在什么场景下进入流程
  • 页面之间如何跳转
  • 页面上有哪些状态
  • 提交后会出现什么反馈
  • 什么情况允许继续,什么情况必须拦截

这些内容本质上都在定义产品行为。

它们回答的是:

这个产品从用户角度看,应该怎么工作。

但仅有这些还不够,因为团队还得把它做出来。

所以第二层会出现实现定义,例如:

  • 前后端边界怎么切
  • 页面路由怎么组织
  • API 怎么拆
  • domain logic 放在哪里
  • 哪些逻辑前端先 mock,哪些逻辑后端先落
  • 测试分层怎么配

这些内容已经不是在定义“产品体验本身”,而是在定义工程实现方案。

所以更准确地说,产品设计并不只产出一种文档,而是至少产出两类:

  • 一类定义产品行为
  • 一类定义产品实现方式

六、library 文档里,这个划分更明显

library 最容易写乱,因为作者很容易直接进入类、函数、目录和内部细节。

但 library 其实同样应该先分成行为和实现两层。

行为层文档

行为层要先回答:

  • 这个库对外解决什么问题
  • 主要入口有哪些
  • 每个入口接收什么输入
  • 返回什么结果
  • 错误语义是什么
  • 哪些约束必须满足
  • 使用者应该怎么调用

比如一个静态加载器类库,它的行为文档应该优先说明:

  • load_operator(...) 做什么
  • load_repo(...) 做什么
  • 什么时候返回 bundle 级结果
  • 什么时候返回 project closure 级结果
  • 什么错误会直接 fail-fast
  • 什么内容明确不负责

这些信息决定的是库的外部契约。

实现层文档

实现层才继续回答:

  • 内部 pipeline 分几步
  • read / normalize / bind / build / derive / finalize 怎么衔接
  • 每一步的中间产物是什么
  • 哪些 node 类型在哪一步分叉
  • 哪些校验在 bundle 级完成,哪些只能在 repo 级完成

这些信息决定的是库的内部结构。

如果一开始不先分层,文档就很容易变成一种很难读的混合体:

  • 一会儿在讲 API 语义
  • 一会儿在讲目录结构
  • 一会儿在讲某个 helper
  • 一会儿又回到用例

最后读者既没搞清对外行为,也没搞清内部实现。

七、这两类内容很重要,但不是绝对二分

这里还有一个很关键的限定:

行为和实现不是绝对二分,而是相对于当前抽象边界而言。

在系统级看,某些内容明显是在定义行为。

但当你把边界往里收,来到子系统级或者组件级时,同一段内容可能就会同时带上实现意味。

例如对整个系统来说:

  • “用户提交后 5 秒内看到结果”更像行为
  • “拆成哪些 service 和 component”更像实现

但如果继续往里看某个子系统:

  • 它的输入输出、错误语义、状态变化,又成了这个子系统自己的行为
  • 它内部再怎么拆模块,又成了这个子系统自己的实现

所以你会发现,越往细粒度走,行为和实现越容易重叠。

这不是分类失败了,而是又引入了另一条轴:

系统层级轴。

更准确的说法不是“所有文档天然绝对分成两类”,而是:

在固定抽象边界之后,要区分当前层的对外 contract 和当前层的内部组织。

这样比把“行为 / 实现”理解成一刀切的绝对分类更稳。

八、为什么这两类内容通常还是要分开写

因为它们服务的对象不同,变化频率也不同。

行为文档更接近稳定约束。

只要系统要解决的问题没变,很多行为定义通常应该相对稳定,比如:

  • 主场景
  • 外部 contract
  • 错误边界
  • 成功标准

实现文档则更容易随着技术方案演进而变化,比如:

  • 模块拆分
  • 存储结构
  • pipeline 细节
  • 性能优化
  • 缓存策略

如果把这两类内容写在一起,会产生几个典型问题:

  • 读者不知道哪些内容是稳定约束,哪些只是当前实现选择
  • 一次实现调整,会把整篇文档都污染
  • API 行为和内部代码结构互相覆盖,导致讨论跑偏
  • AI 或开发者在读取文档时,很难区分“必须遵守的 contract”和“可以调整的实现”

所以把它们拆开,不是为了形式整齐,而是为了让文档真正可维护。

九、一个很实用的组织方式

如果你现在就在写文档,一个很实用的方法是直接按这个结构组织。

先写行为文档:

  • 目标用户 / use case
  • 主入口
  • 输入输出 contract
  • 状态和错误语义
  • acceptance 标准
  • 对应验收测试

再写实现文档:

  • 架构总览
  • 模块边界
  • 主流程阶段
  • 数据模型
  • 关键 trade-off
  • 当前代码锚点

也就是说,顺序最好是:

  1. 先定义行为
  2. 再定义实现

而不是反过来。

因为如果行为都还没钉住,越早讨论实现,后面返工越大。

十、文档写作里最值得避免的一种混写

最常见的一种坏味道是:

文档标题看起来像在讲行为,但正文很快滑进实现细节。

例如本来在写“这个 API 应该提供什么能力”,结果三段之后开始讨论:

  • service 怎么拆
  • 表怎么建
  • helper 怎么复用
  • 某个缓存层怎么接

这会让文档变得很难用。

因为对外行为问题,应该先独立收口;实现问题,应该在另一篇文档里展开。

同样,反过来也成立。

如果一篇实现设计文档还在反复改写 use case、改写产品目标、改写 API 语义,那通常说明行为层其实还没收口,不应该急着进入实现设计。

十一、当有了可执行资产,初级描述就应该让位

还有一个很重要的点是:

文档不是越堆越多越好。

很多自然语言描述,在项目早期是有价值的,因为那时候系统还没稳定,只能先用文字把问题说清楚。

但当系统逐渐收敛,已经有了稳定的代码、测试、模型和调用关系之后,这些早期描述就不应该继续长期和真实实现并列存在。

否则很容易出现两个版本的事实:

  • 文档里写的是一个说法
  • 代码和测试里体现的是另一个说法

最后团队会不断追问,到底哪个才是真的。

如果目标是保持 SSOT,那么更稳的做法通常是:

一旦某类信息已经有了稳定的可执行载体,就应该优先让可执行载体成为主事实源,而把初级描述退后,甚至被替代。

例如:

  • 数据模型,尽量用真实的数据模型代码替代重复的文字字段表
  • JSON 等编写结构,尽量用校验模型或 schema 替代手写格式说明
  • 验收规则,尽量用测试用例代码替代重复的自然语言清单
  • 流程,尽量用真实 call graph、路由关系或状态机结构替代口头流程描述

这不是说文档就不需要了。

而是说文档的职责会变化。

在早期,文档更像是在做:

  • 问题澄清
  • 边界定义
  • 方案探索
  • 初步约束

在后期,文档更应该做:

  • 指向真实事实源
  • 解释为什么这么设计
  • 说明哪些部分是稳定 contract
  • 说明应该看哪段代码、哪组测试、哪个模型

也就是说,成熟文档不一定自己重复保存全部内容,而更应该负责把读者准确带到 SSOT。

一个很实用的替代顺序通常是:

  1. 先用自然语言定义行为和边界
  2. 再把行为沉淀成验收测试
  3. 再把结构沉淀成真实模型、schema、代码和调用关系
  4. 最后让文档从“重复描述事实”转成“索引和解释事实”

这样文档不会越来越肿,事实源也不会越来越分裂。

可以把这条原则再压缩成一句很实用的话:

没有真实代码时,先用文字描述占位;有了真实代码后,就及时引用真实代码。

这里的“真实代码”不只指业务实现代码,也包括:

  • 真实数据模型代码
  • 真实 schema 或校验模型
  • 真实测试用例
  • 真实路由定义
  • 真实 call graph 或状态机实现

这样做的关键不是“让文档少写一点”,而是尽量避免文档和事实源长期双写。

结尾

不管你写的是产品设计文档,还是 library 设计文档,都可以先问一句:

我现在是在沿哪一条逻辑轴讨论分类?

然后再继续问:

在当前这层边界里,我现在写的这一段,到底是在定义对外 contract,还是在定义内部实现?

很多文档质量问题,不是“写得不够细”,而是没有先定轴,也没有先把这两个问题拆开。

一旦拆开,很多事情会立刻清楚很多:

  • 哪些内容应该先稳定
  • 哪些内容可以后续重构
  • 哪些话是给使用者看的
  • 哪些话是给实现者看的
  • 哪些行为必须被验收测试覆盖
  • 哪些初级描述已经应该让位给可执行的 SSOT

文档不一定要一开始就写很多。

但至少要先分清,它到底在回答哪一类问题。