重构之后,文档怎样才不会再次过期

很多项目的文档并不是一开始就不可用,而是在几轮需求之后慢慢失真:目录已经迁移了,README 还写着旧命令;接口已经改了,架构图还停留在上个版本;新人照着文档找入口,最后还是只能全局搜索。

所以重构时,文档的目标不应该只是“写一份完整说明”。更好的目标是:让文档本身变成项目的一部分,能随着代码变化低成本更新。

我更倾向把它拆成两类内容:

  • 人工维护:架构原则、业务边界、关键决策、约定和取舍。
  • 自动同步:目录树、路由表、入口清单、接口索引、模块依赖图。

前者需要人解释“为什么”,后者应该尽量交给脚本回答“现在是什么”。

先体检,再重构

重构最容易踩的坑,是先设计一个理想目录,然后立刻开始搬文件。目录变漂亮了,但业务边界、依赖方向和运行方式没有一起变清楚,维护成本可能只是换了一种形态。

更稳的第一步是项目体检。先记录真实现状:

  • 项目的启动入口在哪里。
  • 页面、服务、数据模型、工具函数分别集中在哪些目录。
  • 哪些模块被大量引用,属于事实上的核心层。
  • 哪些依赖方向不清晰,比如 UI 层直接调用底层实现。
  • 哪些文件职责过宽,已经很难安全修改。
  • 当前构建、测试、打包命令是否可靠。

这一步的产物不需要漂亮,它要诚实。可以先生成一份 ProjectStructure.md,只描述真实文件树和当前职责,不急着写理想架构。

重构的目标是边界,不是目录

好维护的项目通常不是因为目录名字高级,而是因为边界稳定。一个功能应该知道自己能依赖什么、对外暴露什么、内部实现藏在哪里。

一个常见的目标结构可以是这样:

src/
  app/          # 应用启动、路由、全局装配
  features/     # 按业务功能组织的模块
  shared/       # 可复用 UI、工具、通用类型
  services/     # 外部服务、接口客户端、平台能力适配
  domain/       # 核心业务模型与规则

这只是示例,不应该机械套用。真正应该固定下来的是依赖规则:

flowchart TD
  App[app] --> Features[features]
  Features --> Domain[domain]
  Features --> Shared[shared]
  Features --> Services[services]
  Services --> Domain
  Shared --> Domain

当依赖方向明确之后,文档才有长期价值。因为它不只是解释“文件在哪”,还解释“代码应该往哪里放”。

文档应该分层

重构后的文档可以按读者任务来组织,而不是按作者心情堆在一个大文件里。

README.md 负责让人跑起来:

  • 环境要求。
  • 安装依赖。
  • 本地启动。
  • 测试和构建命令。
  • 常见问题。

docs/Architecture.md 负责解释系统形状:

  • 分层方式。
  • 依赖方向。
  • 关键模块职责。
  • 不推荐的调用方式。

docs/ProjectStructure.md 负责索引目录:

  • 当前目录树。
  • 每个一级目录的职责。
  • 重要入口文件。
  • 生成时间和同步命令。

docs/FeatureMap.md 负责连接业务和代码:

  • 功能名称。
  • 相关页面。
  • 相关状态、接口、模型。
  • 修改时需要关注的路径。

docs/DataFlow.md 负责解释关键流程:

  • 数据从哪里来。
  • 在哪里转换。
  • 哪些状态会影响 UI。
  • 错误、空态、加载态怎么流转。

docs/adr/ 负责记录决策:

  • 为什么这么拆模块。
  • 为什么替换某个库。
  • 为什么保留某段历史逻辑。
  • 哪些方案被放弃了。

ADR 不必写得很重,一个文件能说清背景、决策、影响就够了。

自动同步的部分,不要靠人记

最容易过期的文档,往往也是最适合自动生成的文档。比如目录树、路由清单、导出入口、接口定义索引,这些信息都可以从代码里读出来。

可以给项目加一个命令:

{
  "scripts": {
    "docs:update": "node scripts/update-docs.mjs"
  }
}

脚本不需要一开始就很复杂。第一版可以只做几件事:

1. 扫描 src 目录,生成目录树。
2. 识别入口文件和路由文件。
3. 统计 features 下的模块。
4. 更新 docs/ProjectStructure.md 中的自动生成区块。
5. 保留人工维护区块不覆盖。

关键是把文档拆成“脚本可覆盖”和“人工可编辑”两个区域:

# Project Structure

## 维护说明

运行 `npm run docs:update` 刷新自动生成内容。

## 人工说明

这里写目录设计原则、命名约定、例外情况。

<!-- AUTO-GENERATED:START -->
这里由脚本更新。
<!-- AUTO-GENERATED:END -->

这样后续代码变了,不需要人手动同步整篇文档,只需要跑一次命令。

重构过程也要留下轨迹

重构不是一次性工程,更像是一串可验证的小迁移。每次迁移最好满足三个条件:

  • 改动范围清楚。
  • 行为验证清楚。
  • 文档同步清楚。

例如一次迁移可以这样定义:

目标:把用户配置相关逻辑从通用工具目录迁移到 settings feature。
验证:原有配置读取、保存、默认值回退行为保持一致。
文档:更新 FeatureMap 和 ProjectStructure 的相关条目。

这比“整理目录结构”更容易检查,也更容易回滚。

一个实用的落地顺序

我会按这个顺序推进:

  1. 扫描当前项目,生成第一版真实结构文档。
  2. 标出核心入口、业务模块、共享工具和高耦合区域。
  3. 制定目标分层和依赖规则。
  4. 先迁移一个低风险模块,验证目录和文档同步方式。
  5. 加入 docs:update,自动刷新结构类文档。
  6. 按模块逐步重构,每次迁移都补一条决策或变更记录。
  7. 最后把 README、架构文档、功能地图收敛成稳定版本。

这个顺序的好处是,文档不会等到重构结束才补。它从第一天就参与重构,帮助我们判断每一步是不是让项目更清楚。

判断文档有没有用

一份重构文档是否可维护,可以用几个问题检查:

  • 新人能不能在十分钟内跑起项目。
  • 修改一个功能时,能不能从功能地图找到主要文件。
  • 新增模块时,能不能知道应该放在哪个目录。
  • 文档里的目录树是否能通过命令刷新。
  • 重要架构决定是否能追溯到原因,而不只是看到结果。
  • 删除或迁移模块时,是否知道要同步哪些文档。

如果这些问题都有答案,文档就不只是“项目说明”,而是维护系统的一部分。

结语

重构后的文档最怕两件事:过度依赖人工记忆,或者过度追求一次性完整。

更好的做法是先承认现实:代码会变,目录会变,架构也会变。于是文档体系应该允许变化发生,并且让同步成本足够低。

人工写清楚原则和决策,脚本同步事实和索引。这样文档才不会在重构完成那天达到巅峰,然后开始缓慢过期。