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,encodingutf-8)asf:readercsv.DictReader(f)rows[]forrowinreader:# 清洗金额字段——去掉货币符号和逗号ifamountinrow:row[amount]float(row[amount].replace($,).replace(,,))ifdateinrow:# 统一日期格式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):iforder_idnotinrowornotrow[order_id]:errors.append(f行{i}: 缺少 order_id)ifamountinrowandrow[amount]0:errors.append(f行{i}: 金额为负数{row[amount]})returnerrorsdefanalyze(rows,group_byregion,metricamount):统计分析groupsdefaultdict(float)forrowinrows:keyrow.get(group_by,unknown)ifmetricinrow:groups[key]row[metric]returndict(groups)defgenerate_report(rows,output_formattext):生成报表valid_rows[rforrinrowsifr.get(amount,0)0]totalsum(r.get(amount,0)forrinvalid_rows)by_regionanalyze(valid_rows,region,amount)by_monthdefaultdict(float)forrinvalid_rows:ifdate_parsedinr:month_keyr[date_parsed].strftime(%Y-%m)by_month[month_key]r[amount]ifoutput_formatjson: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,indent2,ensure_asciiFalse)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(),keylambdax:x[1],reverseTrue):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)filepathsys.argv[1]output_formatjsonif--jsoninsys.argvelsetextrowsload_data(filepath)errorsvalidate(rows)iferrors:print(数据验证发现问题)foreinerrors:print(f -{e})sys.exit(1)reportgenerate_report(rows,output_format)print(report)问题清单load_data里耦合了文件 I/O、CSV 解析、金额清洗、日期解析validate只用 print 输出错误没法在其他地方复用analyze硬编码了groupbysum换一种聚合方式就得重写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 wont 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单文件最大行数467106tests循环依赖无因为全在一个文件里无分层清晰测试覆盖08 个用例涵盖 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 的沙箱来理解一个你没见过的代码库本地环境零风险。