ai newsDo., 15. Okt.
EilmeldungMeta and Apple team up to make Muse AI models run better on Macs

Entwickler zerlegt 3200 Zeilen grosse Python-Klasse mit Claude Code in 14 Module

Eine Python-Klasse namens OrderService galt als unanfassbar, doch innerhalb von fuenf Wochen zerlegte der Autor sie mithilfe von Claude Code in 14 fokussierte Module ohne einen einzigen Produktionsvorfall. Entscheidend waren Characterization Tests und eine sorgfaeltig geplante Schnittstellenkarte, nicht der AI-geschriebene Code.

dev.toMittwoch 16:31 · 14.10.DE8 Min. Lesezeit
Bild: dev.to

I had a 3,200-line Python class called OrderService that everyone was afraid to touch. Over five weeks, I used Claude Code to break it into 14 focused modules without a single production incident. The trick wasn't the AI writing the code — it was characterization tests first, a seam map I drew myself, and one extraction per pull request.

Every codebase that lives long enough grows one. Ours was OrderService: a single class that handled pricing, discounts, tax, inventory reservation, payment capture, refunds, email notifications, audit logging, and — for reasons nobody remembered — CSV exports for the finance team.

The numbers, when I finally measured them:

That last number was the real cost. Every feature landed in this file, so every PR had merge conflicts, and every review needed someone who "knew the order code." That someone was usually me.

I'd tried splitting it by hand twice before. Both times I got about two days in, discovered some method mutated self._cart_snapshot in a way three other methods silently depended on, and rolled everything back.

This time I wanted to see if an AI coding agent could do the tedious parts while I kept control of the risky parts.

Setup: Claude Code (v2.x CLI), Python 3.12, pytest 8, a Django 5.1 app, roughly 92% line coverage on the service — which sounds great until you realize coverage tells you which lines ran, not which behaviors were asserted.

The whole thing came down to four phases.

flowchart LR A[Characterization tests] --> B[Seam map] B --> C[Extract one module per PR] C --> D[Delete the facade] C -->|tests red| A Phase 1: Pin the current behavior (even the weird parts) Before moving a single line, I asked Claude Code to write characterization tests — tests that record what the code currently does, not what it should do.

For each public method on OrderService, write pytest tests that capture its CURRENT behavior, including edge cases that look like bugs. Do not fix anything. If behavior looks wrong, add a comment "# CHARACTERIZED: looks wrong, preserved on purpose" and assert it anyway. Use real fixtures from tests/fixtures/, not mocks, wherever possible. Enter fullscreen mode Exit fullscreen mode That "don't fix anything" line was crucial. Without it, the agent kept "helpfully" asserting the correct behavior, which then failed against real code, which it then tried to fix. Exactly the spiral I wanted to avoid.

It produced 214 new tests in an afternoon. I reviewed them for about two hours and found 11 cases tagged CHARACTERIZED — real bugs, including a discount that stacked twice when a coupon was applied after a gift card. I logged those as tickets and left the behavior frozen. Refactoring and bug-fixing in the same diff is how you lose a weekend.

This is where I made the most important decision of the project: the agent does not pick the module boundaries.

I did ask Claude Code to analyze the class. This prompt gave me a genuinely useful report:

List every instance attribute on OrderService. For each one, show which methods read it and which methods write it. Output a table. Then group methods into clusters that share writes. Enter fullscreen mode Exit fullscreen mode The output was a read/write matrix that showed me the real coupling. A trimmed version:

Then I drew the boundaries on paper: pricing, tax, inventory, payments, refunds, notifications, audit, exports, and a few smaller ones — 14 in total. Some of the agent's suggested clusters were perfectly sensible on paper but would have split things our team thinks of as one concept, or merged things that change for different reasons.

An AI can tell you what's coupled. It can't tell you what your team will understand six months from now.

Every extraction followed the same recipe, which I wrote into a short instruction file the agent read at the start of each session:

Extraction rules: 1. Extract exactly ONE module per session. Name given by me. 2. OrderService keeps every public method. It delegates to the new module. 3. No behavior changes. No renames of public methods. No "while I'm here". 4. Run the full characterization suite before and after. Paste both summaries. 5. If any test changes, stop and explain why instead of editing the test. Enter fullscreen mode Exit fullscreen mode Rule 2 is the facade pattern, and it's what made the whole thing safe. Callers never noticed anything was happening. After extracting tax logic, OrderService looked like this:

class OrderService: def __init__ ( self, order, *, tax_calculator = None ): self. order = order self. _tax = tax_calculator or TaxCalculator ( order. region ) def compute_tax ( self, lines ): # Delegates to extracted module; kept for existing callers. return self. _tax. compute ( lines ) Enter fullscreen mode Exit fullscreen mode And the new module was small, boring, and testable on its own:

@dataclass ( frozen = True ) class TaxCalculator: region: str def compute ( self, lines: list [ LineItem ]) -> Decimal: rate = RATES. get ( self. region, DEFAULT_RATE ) taxable = sum ( l. amount for l in lines if not l. tax_exempt ) return ( taxable * rate ). quantize ( Decimal ( " 0.01 " )) Enter fullscreen mode Exit fullscreen mode Notice frozen=True and no mutable cache. The old _tax_cache turned out to save about 0.4ms per checkout, and removing it was one of the few deliberate changes I allowed — in its own PR, with a benchmark attached.

Each extraction PR averaged about 180 lines changed, which meant reviews took 15 minutes instead of an hour. My teammates could actually review them, which mattered as much as the code itself.

Extraction #6 was refunds. The agent moved the methods, the delegation looked clean, and all 214 characterization tests passed.

Then staging broke. Partial refunds on orders with a coupon were refunding the pre-discount amount.

What happened: the old refund() read self._cart_snapshot, which was set by apply_coupon() earlier in the same request — because the view called both on the same OrderService instance. The new RefundProcessor got its own fresh snapshot, built from the database, which didn't have the coupon applied yet because that transaction hadn't committed.

None of my characterization tests called apply_coupon() and refund() on the same instance. Each test built a fresh service. The tests pinned methods, not sequences.

The fix took ten minutes once I understood it — pass the snapshot explicitly instead of letting the refund module rebuild it. The lesson took longer to sink in. I went back and asked Claude Code to do something I should have done in Phase 1:

Search the codebase for every call site that invokes two or more OrderService methods on the same instance. List the method sequences. Then write one characterization test per unique sequence. Enter fullscreen mode Exit fullscreen mode That found 23 distinct sequences across views, Celery tasks, and management commands. Three of them exercised shared state in ways that would have broken in later extractions. Those tests caught two more problems before they left my laptop.

Once all 14 modules existed, OrderService was about 260 lines of pure delegation. I had the agent migrate call sites to use the new modules directly — this was the most mechanical part and where AI speed really paid off. It updated 142 call sites across 38 files in an afternoon, in batches of roughly 20 per commit so I could review each batch.

Then I deleted OrderService. Felt great. 🚀

1. Characterization tests are the real deliverable. 💡 The refactor was almost a side effect. Once behavior was pinned, extracting modules became low-stakes. If you only take one thing from this post, it's this: have the agent freeze current behavior before it changes anything, and explicitly tell it not to fix what it finds.

2. Test sequences, not just methods. God classes hide coupling in call order on a shared instance. Per-method tests miss it entirely. Grepping for multi-method call sites should be step one, not something you learn after staging breaks.

3. Let the agent analyze coupling; you draw the boundaries. The read/write matrix was one of the most useful outputs of the whole project. The suggested clusters were not. Module boundaries are a team-communication decision as much as a technical one, and that's still on you.

4. Small PRs are a feature for humans, not just for safety. ⚠️ An AI can happily produce a 3,000-line diff. Nobody can review that. Capping the agent to one extraction per session kept every PR reviewable and kept my teammates involved, which is how knowledge of the new structure actually spread.

5. Write the rules down once, not in every prompt. The five-rule instruction file did more to keep the agent on track than any clever phrasing. "No 'while I'm here' changes" alone probably saved me a dozen rejected PRs.

There are two more classes in the same codebase over 1,500 lines — a reporting service and a user-permissions module. I'm going to reuse the same playbook, but this time start with the sequence-scan prompt before anything else.

I'm also experimenting with having the agent generate the read/write matrix in CI on every PR that touches a large file. If a new method starts writing to an attribute that three other clusters read, that's a smell worth flagging early — before you end up with another 3,200-line monster.

God classes don't need a heroic rewrite. They need pinned behavior, boundaries a human chose, and a lot of small, boring, reviewable steps — which is exactly the kind of work an AI coding agent is great at, as long as you keep the steering wheel.

If you've got your own monster class, I'd love to hear how you're approaching it — drop it in the comments. 👇

Follow me here on Dev.to for more build logs on working with Claude Code and AI coding agents on real, messy codebases. And if you haven't tried Claude Code on a refactor yet, start with the characterization-test prompt above on your scariest file — it's the safest first step you can take.

Originaltitel: How I Split a 3,200-Line God Class Into 14 Modules With Claude CodeQuelle: dev.to (dev.to). Original öffnen