Vai al contenuto

Refactoring e modernizzazione legacy 🔧

Il refactoring di codice legacy è il caso d’uso dove l’agente è più utile e più pericoloso allo stesso tempo. Utile perché può analizzare centinaia di righe in pochi secondi e proporre pattern di migrazione. Pericoloso perché un refactoring senza test è un bug con un nome diverso. La regola d’oro: prima la rete di sicurezza, poi le forbici.

Refactoring senza test è come riparare un ponte while someone is driving on it.

Quando usare l’agente per questo caso 🎯

  • Modernizzazione di modulo legacy — migrare da un pattern obsoleto a uno moderno.
  • Estrazione di interfacce — definire contratti chiari da codice monolitico.
  • Riduzione della complessità — scomporre funzioni giganti in unità piccole e testabili.
  • Migrazione incrementale — strangler fig: avvolgere il legacy con strati nuovi, un pezzo alla volta.

Quando NON usare l’agente ⛔

  • Non ci sono test di regressione — senza rete di sicurezza, ogni modifica è un azzardo. Prima aggiungi test (vedi test e TDD), poi procedi con il refactoring.
  • Il legacy è critico per la produzione — se un bug può causare perdite finanziarie, il refactoring deve essere pianificato con test di integrazione robusti.
  • Non capisci il comportamento osservabile — se non sai cosa fa il codice legacy, non puoi verificare che il refactoring preservi il comportamento.

Prompt di apertura 📝

“Refactoring di [modulo] preservando il comportamento. Safety net: [test da far passare]. Vincoli: non modificare [API pubbliche / comportamento esterno]. Piano per sotto-task incrementali.”

Esempio concreto:

“Refactoring di legacy/auth.go preservando il comportamento osservabile. Safety net: tutti i test in auth_test.go devono passare. Vincoli: non modificare il contratto API HTTP, non cambiare il formato dei token JWT. Piano: prima estrarre le dipendenze, poi introdurre le interfacce, infine sostituire l’implementazione.”

Setup del contesto 🔧

  1. Identifica i test esistenti — sono la tua rete di sicurezza. Se non ci sono, crea prima una suite minimale di test di regressione.
  2. Documenta il comportamento osservabile — cosa fa il modulo? Quali input, quali output? Quali sono gli edge case?
  3. Definisci i vincoli — cosa NON deve cambiare? API pubbliche, formati di dato, comportamento esterno.

Ciclo di lavoro 💡

  1. Pianifica per sotto-task: ogni sotto-task deve essere verificabile singolarmente. Un sotto-task = una issue = un criterio di accettazione.
  2. Estendi la copertura dei test prima di modificare il codice. Più test hai, meno rischi corri.
  3. Esegui in micro-incrementi: dopo ogni modifica, esegui la suite di test. Se qualcosa fallisce, ferma e analizza.
  4. Usa plan mode per ogni sotto-task che attraversa più moduli. Il contesto deve essere focalizzato.
  5. Valuta la delega a sotto-agenti per task indipendenti — riduce durata e consumo di contesto della sessione principale.

Il pattern strangler fig è il tuo alleato: non riscrivere tutto d’un colpo. Avvolgi il legacy con strati nuovi, un pezzo alla volta, finché il vecchio non può essere rimosso.

Characterization testing: la rete di sicurezza reale 🧪

Prima di intervenire su qualunque cosa, devi bloccare il comportamento attuale — incluso i bug. I test di caratterizzazione (o golden master test) non documentano come il codice dovrebbe funzionare, ma come funziona ora, bug compresi.

Il ciclo di generazione funziona in 3 fasi:

  1. Scaffolding: l’agente crea test con asserzioni placeholder (es. assert result == None).
  2. Esecuzione: il test fallisce rivelando l’output reale del codice legacy.
  3. Capture: l’agente cattura l’output reale e lo converte nel baseline immutabile.

Un test che cattura il comportamento attuale — bug e tutto — è più prezioso di un test che descrive il comportamento ideale. Il primo ti dice quando qualcosa cambia. Il secondo non ti dice nulla.

Per verificare l’equivalenza comportamentale post-refactoring, usa un dual-run harness: codice legacy e refactored eseguiti in parallelo contro la stessa suite di caratterizzazione. L’equivalenza è provata solo quando output, mutazioni di stato ed eventi emessi corrispondono esattamente ai golden master.

Evoluzione dello schema database 🗄️

Le migrazioni database sono il punto di fallimento più critico. Il pattern Expand and Contract opera in 4 fasi sequenziali:

Fase Obiettivo Meccanismo
Expand Aggiunta non-breaking Nuove colonne/tabelle nullable, default value
Dual-Write + Backfill Scrittura concorrente su entrambi gli schema Job asincrono con rate limiting
Reader Migration Lettura dal nuovo schema via feature flag Verifica con test di caratterizzazione
Contract Rimozione dello schema legacy Drop colonne, NOT NULL constraints

La regola è: mai combinare modifica applicativa e migrazione nello stesso deploy. Ogni fase deve essere deployabile e verificabile indipendentemente.

Anti-pattern nel refactoring agentic 🚫

Anti-pattern Sintomo Remediation
Business logic hallucination L’agente “pulisce” codice che in realtà implementa regole business complesse Documentare ogni comportamento in linguaggio naturale prima del refactoring
Disguised big-bang rewrite PR da migliaia di righe, review impossibile Limite di 400 righe per PR; scomporre in sotto-task atomici
Feature + refactoring pollution Commit misti: refactor strutturale + nuova feature Branch dedicati al refactor, merge prima di iniziare feature
Eccessiva frammentazione Microservizi troppo piccoli, distributed monolith Partire da monolito modulare, estrarre solo su confini di dominio chiari

Task decomposition e agent-sized units 🧩

Ogni task di refactoring deve essere un’unità atomica con un contratto esplicito:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
## Refactor Target: [modulo]

**Permitted Edits**: src/payments/processor.py, src/payments/adapters.py
**Read-Only**: src/orders/models.py, tests/fixtures/

**Direttive**:
1. Estrarre le chiamate API inline in adapters.py
2. Implementare l'interfaccia PaymentGateway astratta
3. Preservare tutti gli handler di errore esistenti

**Verification Gate**:
- Target: pytest tests/unit/test_payments.py --cov=src/payments
- Floor: 100% pass rate, 92% branch coverage
- Lint: zero violazioni

Gli ambienti isolati (come OpenHands sandboxes) eseguono i task in containerizzati, impedendo modifiche fuori scope. Per refactoring su larga scala, un Architect Agent analizza i grafici di dipendenza e assegna moduli indipendenti a Worker Agent in sandbox separati.

LSTs vs long-context LLMs 🔬

Per la trasformazione precisa del codice, le piattaforme enterprise combinano ragionamento agentic con Lossless Semantic Trees (LSTs). Le LSTs rappresentano il codice alla fedeltà completa del compilatore, mantenendo binding di tipo, formattazione e grafi di dipendenza transitivi — senza richiedere compilation durante le iterazioni.

I long-context LLMs (1M+ token) sono utili per la mappatura architetturale ad alto livello, ma soffrono di degradazione del recupero su prompt lunghi e mancano di precisione a livello di compilatore per scope dei variabili e attribuzione dei tipi.

Criteri di accettazione ✅

  • Suite di regressione verde a ogni incremento.
  • Nessun cambiamento di comportamento osservabile.
  • Nessuna modifica API pubblica non concordata.
  • Ogni sotto-task verificabile indipendentemente.

Approfondimenti 📚

Ultimo aggiornamento il