| name | incremental-implementation |
| description | 增量交付变更。在实现任何涉及多个文件的变更或功能时使用。当你即将一次性编写大量代码,或感觉一个任务太大无法一步到位时使用。 |
增量实现
概述
以薄垂直切片方式构建——实现一个部分,测试它,验证它,然后扩展。避免一次实现整个功能。每个增量应该让系统保持在一个可工作、可测试的状态。这是使大功能可管理的执行纪律。
何时使用
- 实现任何多文件变更
- 从任务分解构建新功能
- 重构现有代码
- 每当你想在测试之前编写超过约 100 行代码时
何时不使用: 单文件、单函数的变更,范围已经是最小的。
增量循环
┌──────────────────────────────────────┐
│ │
│ 实现 ──→ 测试 ──→ 验证 ──┐ │
│ ▲ │ │
│ └───── 提交 ◄────────┘ │
│ │ │
│ ▼ │
│ 下一个切片 │
│ │
└──────────────────────────────────────┘
对于每个切片:
- 实现最小的完整功能片段
- 测试——运行测试套件(或者如果没有测试就编写一个)
- 验证——确认切片按预期工作(测试通过、构建成功、手动检查)
- 提交——用描述性消息保存你的进度(参见
git-workflow-and-versioning 了解原子提交指南)
- 进入下一个切片——继续前进,不要重新开始
切片策略
垂直切片(首选)
构建贯穿技术栈的一个完整路径:
切片 1:写入 key(WAL + 内存表 + 基本 CLI)
→ 测试通过,用户可以通过 CLI 写入 key
切片 2:读取 key(读路径 + CLI)
→ 测试通过,用户可以读回他们的数据
切片 3:删除 key(tombstone + CLI)
→ 测试通过,删除在重启后仍然生效
切片 4:范围扫描(迭代器 + CLI)
→ 测试通过,完整 CRUD 完成
每个切片交付可工作的端到端功能。
契约优先切片
当服务端和客户端 SDK 需要并行开发时:
切片 0:定义 RPC 契约(protobuf 定义、接口、兼容性承诺)
切片 1a:根据契约实现服务端 + 单元测试
切片 1b:根据匹配契约的 Mock 服务端实现客户端 SDK
切片 2:集成并进行端到端测试
风险优先切片
首先处理风险最高或最不确定的部分:
切片 1:证明节点间 gRPC 流式连接能工作(最高风险)
切片 2:在已验证的连接上构建复制日志同步
切片 3:添加断线重连和背压
如果切片 1 失败,你在投入切片 2 和 3 之前就能发现。
实现规则
规则 0:简单优先
在编写任何代码之前,问:"什么是最简单的可行方案?"
在编写代码之后,对照这些检查来审视它:
- 这能用更少的行数完成吗?
- 这些抽象值得它们的复杂度吗?
- 一位资深工程师看了会说"你为什么不直接……"吗?
- 我是在为假设的未来需求而构建,还是为当前任务?
简单性检查:
✗ 为一个通知使用带中间件管道的通用 EventBus
✓ 简单的函数调用
✗ 为两个相似处理器使用抽象工厂模式
✓ 两个带共享工具的直接处理器
✗ 为三个 RPC 处理器使用配置驱动的处理器生成器
✓ 三个直接的处理器函数
三行相似的代码比一个过早的抽象更好。先实现朴素、显然正确的版本。仅在由测试证明正确性之后再进行优化。
规则 0.5:范围纪律
只触碰任务需要的内容。
不要做:
- "清理"与你变更相邻的代码
- 重构你没有在修改的文件中的 import
- 删除你没有完全理解的注释
- 添加不在规格中的功能因为它们"看起来有用"
- 在你只是读取的文件中现代化语法
如果你注意到任务范围之外值得改进的东西,记录它——不要修复它:
注意到但未触碰的:
- internal/storage/format.go 有一个未使用的 import(与此任务无关)
- 认证拦截器可以使用更好的错误消息(单独任务)
→ 需要我为这些创建任务吗?
规则 1:一次一件事情
每个增量只改变一件逻辑上的事情。不要混合关注点:
坏: 一个提交同时添加新模块、重构现有模块并更新构建配置。
好: 三个独立的提交——一个变更一个。
规则 2:保持可编译
每个增量之后,项目必须能构建,现有测试必须通过。不要在切片之间让代码库处于损坏状态。
规则 3:为未完成功能使用功能标志
如果功能尚未准备好给用户使用,但你需要合并增量:
var enableTieredStorage = os.Getenv("FEATURE_TIERED_STORAGE") == "true"
if enableTieredStorage {
}
这让你可以在不暴露未完成工作的情况下将小增量合并到主分支。
规则 4:安全默认值
新代码应默认安全、保守的行为:
func CreateTask(data TaskInput, opts ...CreateOption) (*Task, error) {
cfg := defaultCreateConfig
for _, o := range opts {
o(&cfg)
}
if cfg.Notify == nil {
notify := false
cfg.Notify = ¬ify
}
}
规则 5:回滚友好
每个增量应可独立回滚:
- 增量的变更(新文件、新函数)易于回滚
- 对现有代码的修改应最小化且专注
- 数据库迁移应有相应的回滚迁移
- 避免在一个提交中删除某物并在同一提交中替换它——分开它们
与智能体协同工作
指示智能体增量实现时:
"让我们实现计划中的任务 3。
先只做存储引擎数据布局和 RPC 端点。
先不要碰客户端 SDK ——我们将在下一个增量中做那部分。
实现后,运行 `go test ./... -race` 和 `go build ./...` 来验证
没有东西被破坏。"
对每个增量在范围内和不在范围内的内容保持明确。
增量检查清单
每个增量之后,验证:
注意: 在每个可能影响它的变更之后运行验证命令。成功运行后,除非代码自上次运行以来发生了变化,否则不要重复相同命令——在没有更改代码的情况下重新运行不会增加任何信息。
常见合理化借口
| 合理化借口 | 现实 |
|---|
| "我最后一起测试就行" | 缺陷会累积。切片 1 中的一个缺陷会让切片 2-5 出错。测试每个切片。 |
| "一次性全做完更快" | 它感觉更快,直到某样东西坏了,而你找不到 500 行变更中的哪一行造成的。 |
| "这些变更太小了,不值得单独提交" | 小提交是免费的。大提交隐藏缺陷并使回滚痛苦。 |
| "我之后再加功能标志" | 如果功能尚未完成,它不应该对用户可见。现在就加标志。 |
| "这个重构足够小可以包含在内" | 与功能混合的重构使两者都更难审查和调试。分开它们。 |
| "让我再运行一次构建命令以确保" | 成功运行后,重复相同命令不会增加任何东西,除非代码自上次以来发生了变化。在后续编辑后再运行,而非作为安慰剂。 |
红旗警告
- 编写超过 100 行代码却没有运行测试
- 单个增量中有多个无关的变更
- "让我再快速加一个这个"的范围蔓延
- 为了更快而跳过测试/验证步骤
- 增量之间构建或测试处于损坏状态
- 大量未提交的变更积累
- 在第三个用例要求之前构建抽象
- "既然我在这里"就触碰任务范围之外的文件
- 为一次性操作创建新的工具文件
- 连续两次运行相同的构建/测试命令且没有任何中间代码变更
验证
完成一个任务的所有增量后:
参见
每个增量的验证是本地的检查。在宣布任务完成之前,应用项目范围的完成定义作为最终门禁,这是每个增量都需要清除的常设标准,无论任务如何。参见 references/definition-of-done.md。