Claude Code 重构 467 行遗留代码实录
前几篇用 Claude Code 搞的都是小任务——加个 flag、导个 CSV。那些场景你手写也花不了多少时间,AI 只是让你少打几个字。
这一篇来真的。
手头有个订单分析脚本,两年前写的,从 50 行一路长到快 500 行。file I/O、数据清洗、统计计算、报表生成全揉在一个文件里。没有测试,改一行怕炸一片。
我要让 Claude Code 把它拆成模块化结构,补上测试,而且——重构过程中所有原有功能一个都不能坏。
先看看这个烂摊子
# analyzer.py — 467 行,单文件,零测试importcsvimportjsonimportsysimportosfromdatetimeimportdatetimefromcollectionsimportdefaultdictimportredefload_data(filepath):"""加载 CSV 订单文件"""ifnotos.path.exists(filepath):print(f"错误:文件{filepath}不存在")sys.exit(1)withopen(filepath,'r',encoding='utf-8')asf:reader=csv.DictReader(f)rows=[]forrowinreader:# 清洗金额字段——去掉货币符号和逗号if'amount'inrow:row['amount']=float(row['amount'].replace('$','').replace(',',''))if'date'inrow:# 统一日期格式forfmtin['%Y-%m-%d','%m/%d/%Y','%d-%m-%Y']:try:row['date_parsed']=datetime.strptime(row['date'],fmt)breakexceptValueError:continuerows.append(row)returnrowsdefvalidate(rows):"""验证数据完整性"""errors=[]fori,rowinenumerate(rows):if'order_id'notinrowornotrow['order_id']:errors.append(f"行{i}: 缺少 order_id")if'amount'inrowandrow['amount']<0:errors.append(f"行{i}: 金额为负数{row['amount']}")returnerrorsdefanalyze(rows,group_by='region',metric='amount'):"""统计分析"""groups=defaultdict(float)forrowinrows:key=row.get(group_by,'unknown')ifmetricinrow:groups[key]+=row[metric]returndict(groups)defgenerate_report(rows,output_format='text'):"""生成报表"""valid_rows=[rforrinrowsifr.get('amount',0)>0]total=sum(r.get('amount',0)forrinvalid_rows)by_region=analyze(valid_rows,'region','amount')by_month=defaultdict(float)forrinvalid_rows:if'date_parsed'inr:month_key=r['date_parsed'].strftime('%Y-%m')by_month[month_key]+=r['amount']ifoutput_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()}returnjson.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("按区域统计:")forregion,amtinsorted(by_region.items(),key=lambdax:x[1],reverse=True):lines.append(f"{region}: ${amt:,.2f}")lines.append("-"*50)lines.append("按月统计:")formonthinsorted(by_month.keys()):lines.append(f"{month}: ${by_month[month]:,.2f}")return'\n'.join(lines)if__name__=='__main__':iflen(sys.argv)<2:print("用法: python analyzer.py <文件路径> [--json]")sys.exit(1)filepath=sys.argv[1]output_format='json'if'--json'insys.argvelse'text'rows=load_data(filepath)errors=validate(rows)iferrors:print("数据验证发现问题:")foreinerrors: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 outputClaude Code 不仅写了测试,还写了对load_data的测试——这个函数耦合了文件 I/O,它用tmp_pathfixture 创建临时 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输出改成了返回一个ValidationResultdataclass——这让验证结果可以在调用方做任何处理。但这导致测试挂了——原有测试检查的是 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 的沙箱来理解一个你没见过的代码库,本地环境零风险。