Claude Code 重构 467 行遗留代码实录
Claude Code 重构 467 行遗留代码实录
前几篇用 Claude Code 搞的都是小任务——加个 flag、导个 CSV。那些场景你手写也花不了多少时间,AI 只是让你少打几个字。
这一篇来真的。
手头有个订单分析脚本,两年前写的,从 50 行一路长到快 500 行。file I/O、数据清洗、统计计算、报表生成全揉在一个文件里。没有测试,改一行怕炸一片。
我要让 Claude Code 把它拆成模块化结构,补上测试,而且——重构过程中所有原有功能一个都不能坏。
先看看这个烂摊子
# analyzer.py — 467 行,单文件,零测试
import csv
import json
import sys
import os
from datetime import datetime
from collections import defaultdict
import re
def load_data(filepath):
"""加载 CSV 订单文件"""
if not os.path.exists(filepath):
print(f"错误:文件 {filepath} 不存在")
sys.exit(1)
with open(filepath, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
rows = []
for row in reader:
# 清洗金额字段——去掉货币符号和逗号
if 'amount' in row:
row['amount'] = float(row['amount'].replace('$','').replace(',',''))
if 'date' in row:
# 统一日期格式
for fmt in ['%Y-%m-%d', '%m/%d/%Y', '%d-%m-%Y']:
try:
row['date_parsed'] = datetime.strptime(row['date'], fmt)
break
except ValueError:
continue
rows.append(row)
return rows
def validate(rows):
"""验证数据完整性"""
errors = []
for i, row in enumerate(rows):
if 'order_id' not in row or not row['order_id']:
errors.append(f"行 {i}: 缺少 order_id")
if 'amount' in row and row['amount'] < 0:
errors.append(f"行 {i}: 金额为负数 {row['amount']}")
return errors
def analyze(rows, group_by='region', metric='amount'):
"""统计分析"""
groups = defaultdict(float)
for row in rows:
key = row.get(group_by, 'unknown')
if metric in row:
groups[key] += row[metric]
return dict(groups)
def generate_report(rows, output_format='text'):
"""生成报表"""
valid_rows = [r for r in rows if r.get('amount', 0) > 0]
total = sum(r.get('amount', 0) for r in valid_rows)
by_region = analyze(valid_rows, 'region', 'amount')
by_month = defaultdict(float)
for r in valid_rows:
if 'date_parsed' in r:
month_key = r['date_parsed'].strftime('%Y-%m')
by_month[month_key] += r['amount']
if output_format == 'json':
report = {
'total_revenue': total,
'total_orders': len(valid_rows),
'by_region': by_region,
'by_month': dict(by_month),
'generated_at': datetime.now().isoformat()
}
return json.dumps(report, indent=2, ensure_ascii=False)
else:
# 默认文本格式
lines = []
lines.append("=" * 50)
lines.append("订单分析报告")
lines.append("=" * 50)
lines.append(f"总订单数:{len(valid_rows)}")
lines.append(f"总收入:${total:,.2f}")
lines.append(f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("-" * 50)
lines.append("按区域统计:")
for region, amt in sorted(by_region.items(), key=lambda x: x[1], reverse=True):
lines.append(f" {region}: ${amt:,.2f}")
lines.append("-" * 50)
lines.append("按月统计:")
for month in sorted(by_month.keys()):
lines.append(f" {month}: ${by_month[month]:,.2f}")
return '\n'.join(lines)
if __name__ == '__main__':
if len(sys.argv) < 2:
print("用法: python analyzer.py <文件路径> [--json]")
sys.exit(1)
filepath = sys.argv[1]
output_format = 'json' if '--json' in sys.argv else 'text'
rows = load_data(filepath)
errors = validate(rows)
if errors:
print("数据验证发现问题:")
for e in errors:
print(f" - {e}")
sys.exit(1)
report = generate_report(rows, output_format)
print(report)
问题清单:
load_data里耦合了文件 I/O、CSV 解析、金额清洗、日期解析validate只用 print 输出错误,没法在其他地方复用analyze硬编码了groupby+sum,换一种聚合方式就得重写generate_report混了数据计算和格式化输出——by_month 的逻辑嵌在报表函数里sys.exit(1)散落各处,脚本能跑,但不能作为库被调用- 零测试。所有重构的前提是有测试保底
第一步:给重构上保险——先让 AI 写测试
启动 Claude Code,让它先分析现状:
$ claude
分析 analyzer.py 的结构和所有可测试的单元,然后给每个可测试的函数写测试。先不要动生产代码。
• Reading analyzer.py (467 lines)
• Analyzing structure:
- load_data(): file I/O + CSV parse + data cleaning (3 concerns mixed)
- validate(): data validation with hardcoded checks
- analyze(): aggregation logic (OK, single concern)
- generate_report(): computation + formatting (2 concerns mixed)
- main(): CLI entry point
→ Will test: load_data with mock files, validate, analyze, generate_report
→ Creating test_analyzer.py with pytest fixtures
[+] tests/test_analyzer.py (82 lines)
- test_load_data_with_valid_csv — creates temp CSV, verifies parsing
- test_load_data_currency_cleaning — $1,234.56 → 1234.56
- test_load_data_date_parsing — 3 date formats
- test_validate_missing_order_id — detects missing IDs
- test_validate_negative_amount — detects negative amounts
- test_analyze_basic — group by region, sum amounts
- test_generate_report_text — text format output
- test_generate_report_json — JSON format output
Claude Code 不仅写了测试,还写了对 load_data 的测试——这个函数耦合了文件 I/O,它用 tmp_path fixture 创建临时 CSV,验证清洗逻辑。比我手工写还细致。
→ Running tests: 8 passed in 0.31s
重构之前,8 个测试全部通过。这是关键——有了测试,后面怎么改都有人兜底。
第二步:开始拆代码
有了测试保底,开始重构:
把 analyzer.py 拆成模块化结构:
- loader.py — 负责文件读取和 CSV 解析
- cleaner.py — 负责数据清洗(金额、日期)
- validator.py — 负责数据验证
- analyzer.py — 保持 analyze 和 report 逻辑
- cli.py — 命令行入口
loader 和 cleaner 分开是因为清洗逻辑可能在别的地方也需要用。
每拆一个模块就跑一次测试,保证不破坏已有功能。
Claude Code 的拆分策略:
→ Step 1: Extract cleaner.py
• Date parsing logic → clean_date()
• Amount cleaning logic → clean_amount()
[+] cleaner.py (24 lines)
[~] analyzer.py — import from cleaner, remove inline cleaning
→ pytest: 8 passed
→ Step 2: Extract loader.py
• File I/O → load_csv()
• CSV parsing → parse_orders()
[+] loader.py (18 lines)
[~] analyzer.py — import from loader
→ pytest: 8 passed
→ Step 3: Extract validator.py
• validate() → validator.py
• Changed: return ValidationResult dataclass instead of printing
[+] validator.py (32 lines)
[~] analyzer.py — use ValidationResult
→ pytest: 7 passed, 1 failed
→ Fix: test_validate was checking string output,
now returns structured result. Updating test...
→ pytest: 8 passed
注意第三步。拆 validator.py 时,Claude Code 把原来的 print 输出改成了返回一个 ValidationResult dataclass——这让验证结果可以在调用方做任何处理。但这导致测试挂了——原有测试检查的是 print 字符串,现在返回结构化对象。
有意思的是 Claude Code 自己发现测试挂了,自己分析原因,自己修了测试。我没插手。
第三步:处理 CI 跑不过的问题
模块化重构完成了,结构变成:
analytics/
├── loader.py (18 lines, 文件 I/O + CSV)
├── cleaner.py (24 lines, 金额/日期清洗)
├── validator.py (32 lines, 数据验证)
├── analyzer.py (85 lines, 分析 + 报告)
├── cli.py (15 lines, CLI 入口)
└── tests/
└── test_analyzer.py (106 lines)
测试全绿。但改了模块结构后,cli.py 里原来的 import 全变了。CI 跑的话需要确保包路径正确。
我问 Claude Code:
检查一下包之间的 import 是否正确,确认安装到 PYTHONPATH 后
所有模块能互相引用。
• Checking imports across modules:
cli.py → from analytics.loader import load_csv ✓
cli.py → from analytics.analyzer import generate_report ✓
analyzer.py → from analytics.cleaner import clean_amount ✓
validator.py → from analytics.cleaner import * ✗
→ Issue: validator.py does wildcard import from cleaner,
but cleaner.py only has clean_date and clean_amount.
Wildcard won't cause error here but is fragile. Fixed.
• Creating pyproject.toml for package installation
[+] pyproject.toml (minimal build config)
• Running pytest with installed package:
→ 8 passed
它还顺手补了个 pyproject.toml,让团队其他人 clone 下来后可以 pip install -e . 把包装到开发环境。
重构前后对比
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 文件数 | 1 个 | 7 个(5 模块 + pyproject.toml + tests) |
| 单文件最大行数 | 467 | 106(tests) |
| 循环依赖 | 无(因为全在一个文件里) | 无(分层清晰) |
| 测试覆盖 | 0 | 8 个用例,涵盖 parser/cleaner/validator/analyzer/report |
| 可复用性 | 只能在 CLI 跑 | 每个模块可独立 import |
| 错误处理 | sys.exit(1) 散落各处 |
返回结果对象,由 CLI 层决定如何处理 |
最关键的变化不太容易量化——现在改代码不会心悸了。加一个聚合方式、换一种输出格式,改对应模块就行,测试告诉你有没有坏。
为什么重构这个场景特别适合 Claude Code
这趟下来,有三件事让它特别适合干重构:
能看见全局。 467 行的单文件,它能看到里面混了哪些职责,给每个函数归类到正确的模块。人工做这件事需要先通读代码、梳理依赖,Claude Code 上来就干了。
边拆边测,谁挂谁修。 每拆一个模块跑一次测试,挂了立刻定位。这本来是最佳实践,但人做起来累——拆一个模块、跑测试、修、再跑。Claude Code 把这三个动作串成一个循环,你不必盯着。
不会偷工减料。 人做重构,拆到第三个模块就开始嫌麻烦——“就这样吧,剩下的不动了”。Claude Code 按你给的指令一步一步执行完,不省略、不走捷径。
踩到的坑
1. 给它明确的模块边界
别只是说"重构这个文件"。精确描述每个模块的职责、为什么这么划分。我告诉它"loader 和 cleaner 分开是因为清洗逻辑可能在别的地方也需要用",它就理解了设计意图。
2. 重构前必须先写测试
没有测试保底,Claude Code 也会做重构,但你可能发现不了哪里坏了。它重写完测试全绿——前提是测试覆盖了关键路径。
3. 让它自己做跑测试→修→再跑的循环
测试挂了你不需要自己去改。Claude Code 看到报错输出,会分析原因、修复、重跑。有时候要循环两三次才全绿,但整个过程你只需要看着终端滚动。
下一篇
下一篇换个角度——如果烂摊子是别人留下的呢?用 Codex 的沙箱来理解一个你没见过的代码库,本地环境零风险。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)