增量实现
概览
用薄的纵向切片来构建系统:实现一小块,测试它,验证它,再继续扩展。避免一次把整个功能全部做完。每一个增量都应该让系统保持在可工作、可测试的状态。这种执行纪律,能让大型功能变得可控。
何时使用
- 实现任何多文件改动
- 根据任务拆解落地新功能
- 重构现有代码
- 任何你想在测试前一口气写超过约 100 行代码的时候
不适用的场景: 单文件、单函数,并且范围已经很小的改动。
增量循环
┌──────────────────────────────────────┐
│ │
│ Implement ──→ Test ──→ Verify ──┐ │
│ ▲ │ │
│ └───── Commit ◄─────────────┘ │
│ │ │
│ ▼ │
│ Next slice │
│ │
└──────────────────────────────────────┘
对于每个切片:
- Implement:实现最小但完整的一块功能
- Test:运行测试,如果还没有测试就补测试
- Verify:确认这一块真的符合预期,例如测试通过、构建成功、手工检查正常
- Commit:用清晰的提交信息保存进度,参见
git-workflow-and-versioning - Move to the next slice:继续推进,不要推倒重来
切片策略
纵向切片(首选)
一次打通一条完整路径:
Slice 1: Create a task (DB + API + basic UI)
→ Tests pass, user can create a task via the UI
Slice 2: List tasks (query + API + UI)
→ Tests pass, user can see their tasks
Slice 3: Edit a task (update + API + UI)
→ Tests pass, user can modify tasks
Slice 4: Delete a task (delete + API + UI + confirmation)
→ Tests pass, full CRUD complete
每个切片都能交付一个真实可用的端到端能力。
先定义契约的切片方式
当前后端需要并行开发时:
Slice 0: Define the API contract (types, interfaces, OpenAPI spec)
Slice 1a: Implement backend against the contract + API tests
Slice 1b: Implement frontend against mock data matching the contract
Slice 2: Integrate and test end-to-end
风险优先切片
先处理最危险、最不确定的部分:
Slice 1: Prove the WebSocket connection works (highest risk)
Slice 2: Build real-time task updates on the proven connection
Slice 3: Add offline support and reconnection
如果 Slice 1 失败,你会在投入 Slice 2 和 3 之前就知道。
实现规则
规则 0:简单优先
在写代码之前,先问一句:“能工作的最简单方案是什么?”
写完之后,再用下面这些问题回看:
- 能不能用更少的代码实现?
- 这些抽象真的值回它们的复杂度吗?
- 资深工程师会不会看完后问一句“你为什么不直接……?”
- 我是在为假想的未来需求设计,还是只为当前任务服务?
SIMPLICITY CHECK:
✗ Generic EventBus with middleware pipeline for one notification
✓ Simple function call
✗ Abstract factory pattern for two similar components
✓ Two straightforward components with shared utilities
✗ Config-driven form builder for three forms
✓ Three form components
三行相似代码,通常也比过早抽象更好。先实现朴素、显然正确的版本,只有在正确性已经通过测试证明后再考虑优化。
规则 0.5:范围纪律
只改任务要求你改的内容。
不要:
- 顺手“清理”你改动旁边的代码
- 在没修改的文件里重排 import
- 删除你并未完全理解的注释
- 因为“看起来有用”就擅自加功能
- 只是阅读某个文件,却顺手把它语法现代化
如果你发现范围外有值得改进的地方,记录下来,但不要顺手修:
NOTICED BUT NOT TOUCHING:
- src/utils/format.ts has an unused import (unrelated to this task)
- The auth middleware could use better error messages (separate task)
→ Want me to create tasks for these?
规则 1:一次只做一件事
每个增量只改变一个逻辑点,不要混杂关注点:
坏例子: 一个提交同时新增组件、重构旧组件并修改构建配置。
好例子: 拆成三个提交,每个只做一类改动。
规则 2:始终保持可编译
每个增量之后,项目都必须能构建,已有测试都必须通过。不要在切片之间让代码库处于损坏状态。
规则 3:未完成功能用 Feature Flag 包住
如果功能还没准备好给用户用,但你需要提前合并增量:
// Feature flag for work-in-progress
const ENABLE_TASK_SHARING = process.env.FEATURE_TASK_SHARING === 'true';
if (ENABLE_TASK_SHARING) {
// New sharing UI
}
这样你就能把小步增量合并到主干,同时不暴露未完成功能。
规则 4:安全默认值
新代码默认应该采取安全、保守的行为:
// Safe: disabled by default, opt-in
export function createTask(data: TaskInput, options?: { notify?: boolean }) {
const shouldNotify = options?.notify ?? false;
// ...
}
规则 5:对回滚友好
每个增量都应该能独立回退:
- 增量式改动,比如新增文件、新增函数,最容易回滚
- 修改现有代码时,改动要小而聚焦
- 数据库迁移要有对应的回滚迁移
- 不要在同一个提交里既删除旧东西又放入新替代品,拆开提交
与 Agent 协作
当你指挥 agent 以增量方式实现时:
"Let's implement Task 3 from the plan.
Start with just the database schema change and the API endpoint.
Don't touch the UI yet — we'll do that in the next increment.
After implementing, run `npm test` and `npm run build` to verify
nothing is broken."
对每个增量,明确说明哪些在范围内,哪些不在范围内。
增量检查清单
每做完一个增量,都确认:
- 改动只做了一件事,而且做完整了
- 所有现有测试仍通过(
npm test) - 构建成功(
npm run build) - 类型检查通过(
npx tsc --noEmit) - Lint 通过(
npm run lint) - 新功能按预期工作
- 已用清晰提交信息完成提交
常见自我安慰
| 自我安慰 | 现实 |
|---|---|
| “最后一起测就行” | Bug 会叠加。Slice 1 的 bug 会让 Slice 2-5 全部建立在错误基础上。每个切片都要测。 |
| “一次做完更快” | 只有在一切正常时它才“看起来更快”。一旦出问题,你根本不知道 500 行改动里是哪一行引入了故障。 |
| “这些改动太小了,不值得分开提交” | 小提交几乎没有成本。大提交会隐藏 bug,也让回滚变痛苦。 |
| “Feature flag 之后再加” | 如果功能还没完成,就不应该让用户看见。现在就加。 |
| “这个重构很小,可以顺便做” | 功能和重构混在一起,会让评审和排障都更困难。拆开做。 |
危险信号
- 在不跑测试的情况下连续写超过 100 行代码
- 一个增量里混入多个无关改动
- 出现“我顺手再加一下这个”的范围膨胀
- 为了追求速度跳过测试或验证
- 切片之间构建或测试处于失败状态
- 大量未提交改动越积越多
- 在第三个用例出现前就先设计抽象
- 因为“反正我都在这了”而去动范围外文件
- 为一次性操作新建通用工具文件
验证
完成一个任务的所有增量后,确认:
- 每个增量都已单独测试并提交
- 全量测试通过
- 构建干净
- 功能按 spec 端到端正常工作
- 没有遗留未提交改动