文档分类要先定轴,再谈定义行为和定义实现
比起先说产品文档还是技术文档,更重要的是先固定统一的分类轴。沿内容性质这条轴看,很多文档都在定义行为,或者定义怎么实现。
- 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
- 当前代码锚点
也就是说,顺序最好是:
- 先定义行为
- 再定义实现
而不是反过来。
因为如果行为都还没钉住,越早讨论实现,后面返工越大。
十、文档写作里最值得避免的一种混写
最常见的一种坏味道是:
文档标题看起来像在讲行为,但正文很快滑进实现细节。
例如本来在写“这个 API 应该提供什么能力”,结果三段之后开始讨论:
- service 怎么拆
- 表怎么建
- helper 怎么复用
- 某个缓存层怎么接
这会让文档变得很难用。
因为对外行为问题,应该先独立收口;实现问题,应该在另一篇文档里展开。
同样,反过来也成立。
如果一篇实现设计文档还在反复改写 use case、改写产品目标、改写 API 语义,那通常说明行为层其实还没收口,不应该急着进入实现设计。
十一、当有了可执行资产,初级描述就应该让位
还有一个很重要的点是:
文档不是越堆越多越好。
很多自然语言描述,在项目早期是有价值的,因为那时候系统还没稳定,只能先用文字把问题说清楚。
但当系统逐渐收敛,已经有了稳定的代码、测试、模型和调用关系之后,这些早期描述就不应该继续长期和真实实现并列存在。
否则很容易出现两个版本的事实:
- 文档里写的是一个说法
- 代码和测试里体现的是另一个说法
最后团队会不断追问,到底哪个才是真的。
如果目标是保持 SSOT,那么更稳的做法通常是:
一旦某类信息已经有了稳定的可执行载体,就应该优先让可执行载体成为主事实源,而把初级描述退后,甚至被替代。
例如:
- 数据模型,尽量用真实的数据模型代码替代重复的文字字段表
- JSON 等编写结构,尽量用校验模型或 schema 替代手写格式说明
- 验收规则,尽量用测试用例代码替代重复的自然语言清单
- 流程,尽量用真实 call graph、路由关系或状态机结构替代口头流程描述
这不是说文档就不需要了。
而是说文档的职责会变化。
在早期,文档更像是在做:
- 问题澄清
- 边界定义
- 方案探索
- 初步约束
在后期,文档更应该做:
- 指向真实事实源
- 解释为什么这么设计
- 说明哪些部分是稳定 contract
- 说明应该看哪段代码、哪组测试、哪个模型
也就是说,成熟文档不一定自己重复保存全部内容,而更应该负责把读者准确带到 SSOT。
一个很实用的替代顺序通常是:
- 先用自然语言定义行为和边界
- 再把行为沉淀成验收测试
- 再把结构沉淀成真实模型、schema、代码和调用关系
- 最后让文档从“重复描述事实”转成“索引和解释事实”
这样文档不会越来越肿,事实源也不会越来越分裂。
可以把这条原则再压缩成一句很实用的话:
没有真实代码时,先用文字描述占位;有了真实代码后,就及时引用真实代码。
这里的“真实代码”不只指业务实现代码,也包括:
- 真实数据模型代码
- 真实 schema 或校验模型
- 真实测试用例
- 真实路由定义
- 真实 call graph 或状态机实现
这样做的关键不是“让文档少写一点”,而是尽量避免文档和事实源长期双写。
结尾
不管你写的是产品设计文档,还是 library 设计文档,都可以先问一句:
我现在是在沿哪一条逻辑轴讨论分类?
然后再继续问:
在当前这层边界里,我现在写的这一段,到底是在定义对外 contract,还是在定义内部实现?
很多文档质量问题,不是“写得不够细”,而是没有先定轴,也没有先把这两个问题拆开。
一旦拆开,很多事情会立刻清楚很多:
- 哪些内容应该先稳定
- 哪些内容可以后续重构
- 哪些话是给使用者看的
- 哪些话是给实现者看的
- 哪些行为必须被验收测试覆盖
- 哪些初级描述已经应该让位给可执行的 SSOT
文档不一定要一开始就写很多。
但至少要先分清,它到底在回答哪一类问题。