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 拆成模块化结构:

  1. loader.py — 负责文件读取和 CSV 解析
  2. cleaner.py — 负责数据清洗(金额、日期)
  3. validator.py — 负责数据验证
  4. analyzer.py — 保持 analyze 和 report 逻辑
  5. 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 的沙箱来理解一个你没见过的代码库,本地环境零风险。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐