我们用 Claude 删除了 5,600 行代码(并发现了 1 个 Bug)
Source: Dev.to
我构建的过度设计系统
我需要管理提供商(Google Cloud、AWS、Postgres)及其服务(BigQuery、Firestore、DynamoDB)。看起来很简单。
但我却做出了下面这个结构:
| 位置 | 存储内容 |
|---|---|
seeds/sources_seed.go | 包含服务数组的源实体 |
seeds/templates_seed.go | 包含模式的 ConnectionTemplate 实体 |
seeds/constants.go | 硬编码的 UUID |
frontend/components/connections/index.ts | UUID → 组件映射 |
frontend/components/connections/*.tsx | 硬编码的 selectedServices 数组 |
frontend/lib/services/google-cloud-capabilities.ts | 每个服务的 OAuth 范围 |
| 数据库(Firestore) | Sources 和 ConnectionTemplates 集合 |
八个以上文件以不同方式定义相同信息。
- 想要添加一个新提供商?需要修改 8 个文件。
- 想要在已有提供商中添加服务?需要修改 6 个文件。
- 想要弄清楚它们是如何关联的?祝你好运。
我当时告诉自己这很“灵活”“可扩展”。
领悟
然后我真的思考了一下:
- 没有任何动态性。 OAuth 范围在 OAuth 应用层面被锁定。你无法为每个连接授予不同的范围——它们已经写入 OAuth 同意屏幕。
- 没有部分服务访问的使用场景。 “这个 Google Cloud 连接拥有 BigQuery 但没有 Firestore”——这种情况什么时候会出现?连接仅仅是凭证。如果这些凭证无法访问 BigQuery,API 会返回 401。就这么简单。
- 我在为从未使用的灵活性维护复杂性。
“高级”架构在解决一个根本不存在的问题。
替代方案:20 行
我还没有任何用户,没有向后兼容性的顾虑,也没有数据需要迁移。
于是我没有去重构,而是问自己:如果我直接把所有东西都删掉,改用一个配置文件会怎样?
// frontend/lib/providers.ts
export const PROVIDERS = {
'google-cloud': {
name: 'Google Cloud Platform',
auth: 'oauth2',
services: ['bigquery', 'firestore', 'gcs', 'pubsub'],
},
'aws': {
name: 'Amazon Web Services',
auth: 'iam',
services: ['dynamodb'],
},
'postgres': {
name: 'PostgreSQL',
auth: 'database',
services: ['postgres'],
},
} as const
export type ProviderId = keyof typeof PROVIDERS
就这么简单——大约 20 行代码取代了数据库表、种子数据、UUID、能力文件以及注册表。
前后对比:连接模型
之前: 6 个字段,UUID 查询,冗余数据
// Before
Connection = {
id: 'abc123',
sourceId: 'c7b3d8e9-5f2a-4b1c-9d6e-8a3b5c7d9e1f', // UUID 查询
templateId: 'template-google-cloud-oauth2', // 另一个 UUID
services: ['bigquery', 'firestore'], // 冗余
connectionConfig: {
selectedServices: ['bigquery', 'firestore'], // 重复
projectId: 'my-project',
},
credentials: { /* … */ }
}
之后: 4 个字段,字符串 ID,无冗余
// After
Connection = {
id: 'abc123',
providerId: 'google-cloud', // 直接使用 PROVIDERS 中的键
config: { projectId: 'my-project' }, // 供应商特定的配置
credentials: { /* … */ }
}
Claude 如何实现这一点
这并不是“让 Claude 写点代码”。这是一场 AI 辅助的架构手术。
1. 绘制影响范围
我向 Claude 提供了代码库的上下文,并让它找出所有引用旧系统的文件。它识别出了:
- 所有旧类型的导入
- 所有 UUID 常量的使用
- 使用
sourceId或templateId的前端组件 - 需要更新的测试文件
- 为避免破坏中间状态而必须遵循的操作顺序
2. 系统化执行
194 个文件手动修改既繁琐,又难以 正确 完成。Claude 按部就班地完成了这些工作:
后端 (Go)
connections/model.go– 删除services、templateId;添加providerIdconnections/service.go– 更新创建/校验逻辑connections/handler.go– 更新 API 请求/响应connections/dao.go– 更新 Firestore 查询router.go– 删除/v1/sources/*路由- 删除整个
sources/包(9 个文件) - 删除整个
connectiontemplate/包(10 个文件) - 删除
seeds/sources_seed.go、templates_seed.go
前端 (TypeScript)
lib/providers.ts– 新的静态配置(20 行)- 删除
hooks/use-source.ts - 删除
hooks/use-connection-template.ts - 删除
services/source-service.ts - 删除
services/google-cloud-capabilities.ts - 更新 40 多个使用旧类型的组件文件
3. 同步更新测试
关键点: 我们没有删除测试,而是对其进行了更新。
- 删除
sources/包时,也同步删除了对应的测试。 - 简化连接模型时,更新了连接相关的测试。
整个重构过程中,测试套件始终保持绿色。
为什么只有一个 bug?
在修改了 194 个文件后,我们在端到端测试中恰好发现了 一个 bug。这不是运气——而是以下因素的结果:
- 全面的测试覆盖。 测试能够立即捕获回归。当我更改连接模型时,测试准确指出了哪些处理程序和服务需要更新。
- 在有测试的情况下进行重构,而不是事后重构。 每一次更改都伴随相应的测试更新,确保测试套件在每一步都保持可靠。
结论
过度工程化会让你陷入维护噩梦。通过剔除不必要的层级并让 AI 帮助映射影响,我将一个 5,600 行的系统压缩到整洁的 20 行配置——节省了时间、降低了复杂度,并避免了未来的 bug。
同一次提交中的测试更新
测试不是事后才考虑的。
3. AI 帮助你系统化
Claude 不会忘记在代码库的某个偏远角落更新文件。它不会在第 80 个文件后疲惫而开始出错。
4. 你拥有清晰的架构愿景
我在动手写代码之前写了一份详细的计划文档。目标状态明确无误:一个配置文件,基于字符串的提供者 ID,连接上没有服务数组。
我学到的
复杂性是可以选择的
我构建了这个复杂的系统。没有人强迫我为静态数据创建基于 UUID 的查找和数据库种子。我这么做是因为觉得“合适”。
有时候,合适的方案只需要一个 20 行的配置文件。
AI 最适合用于架构,而不仅仅是自动补全
价值不在于“Claude 写代码更快”。价值在于:
- Claude 帮助识别了旧系统的所有触角
- Claude 在 194 个文件之间保持了上下文
- Claude 采用系统化的方式,而我会感到疲惫
经过充分测试的代码让删除无所畏惧
我可以批量删除代码,因为我信任我的测试。每一次删除都有验证。没有“我觉得这安全”的说法——要么测试通过,要么不通过。
没有用户 = 没有借口
目前还没有用户,这意味着我没有理由保留复杂性。没有向后兼容的需求。没有迁移脚本。只需删除并继续前进。
如果你处于早期阶段并且背负技术债务,现在是修复它的最便宜时机。
新开发者体验
添加新提供者
- 在
PROVIDERS配置中添加条目(1 行) - 创建 connection‑form 组件
- 创建 service‑config 组件
- 创建后端处理程序
为现有提供者添加服务
- 向
PROVIDERS[providerId].services数组中添加(1 行) - 创建 service‑config 组件
- 创建后端处理程序
无种子数据。无迁移。无 UUID。无模板实体。
亲自尝试
如果你正盯着一个感觉比实际需要的更臃肿的系统:
- 询问它在解决什么问题。 这个问题是真实的还是假设的?
- 检查是否真的有动态部分。 如果“灵活”的部件从不灵活,它们只是增加了复杂度。
- 如果你有完善的测试,就相信它们。 测试会捕捉你的错误。
- 使用 AI 来绘制影响范围。 它比你更擅长找出所有引用。
有时答案就是大规模删除。
你是否曾经删除过自己搭建的系统,因为意识到它被过度设计了?是什么帮助你做出这个决定的?