Le 5 cose da sapere prima di una migrazione da legacy code
“È solo una piccola migrazione”, dicevano. “Il codice è ben documentato”, dicevano.
Nove anni e 47 crisi esistenziali dopo, sono qui a raccontarvi cosa succede quando ti ritrovi a fare l’archeologo del software, scavando in strati di codice legacy che risalgono a quando “cloud” era solo qualcosa che vedevi in cielo e “agile” era un aggettivo, non una religione.
Benvenuti nel meraviglioso mondo dell’archeologia del software, dove ogni git blame
è una sorpresa e ogni refactoring un’avventura.
Come sopravvissuto a una migrazione pluriennale da VB.NET a PHP (sì, avete letto bene), permettetemi di condividere alcune lezioni apprese sul campo.
Lo sconforto
La prima volta che aprii il progetto legacy, attraversai varie fasi che ricordano stranamente i cinque stadi del lutto:
- Negazione: “Non può essere così male…”
- Rabbia: “CHI HA SCRITTO QUESTO CODICE?!”
- Contrattazione: “Ok, forse se riscrivo solo questa parte…”
- Depressione: “Non finiremo mai questa migrazione”
- Accettazione: “È solo codice. Un commit alla volta.”
Lezione 1: Il codice legacy è come un sito archeologico
Proprio come gli archeologi trovano strati su strati di civiltà antiche, nel codice legacy trovi strati di sviluppatori che si sono succeduti nel tempo. Ogni strato ha il suo stile, i suoi pattern, e soprattutto i suoi misteri.
' Strato 1 (2002) - L'Era Pre-Framework
Public Class UserManager
Private connString As String = ConfigurationSettings.AppSettings("ConnString")
Public Function GetUserData(ByVal id As Integer) As DataSet
Dim ds As New DataSet()
Dim conn As New SqlConnection(connString)
Dim cmd As New SqlCommand("SELECT * FROM Users WHERE UserId = " & id)
' Injection SQL? Mai sentita ?...
cmd.Connection = conn
Dim da As New SqlDataAdapter(cmd)
da.Fill(ds)
Return ds
End Function
End Class
' Strato 2 (2008) - Il Tentativo di OOP
Public Class UserManagerV2
Private _userRepository As UserRepository ' Mai inizializzato
Private Shared _instance As UserManagerV2 ' Singleton perché era di moda
' 300 righe di codice che mescolano logica di business,
' accesso ai dati e probabilmente anche il caffè del mattino
End Class
' Strato 3 (2011) - "Modernizzazione"
<Serializable()> _
Public Class UserEntity
' 47 proprietà pubbliche
' Nessuna validazione
' Logica di business nei setter
Public Property UserName As String
Get
Return m_UserName
End Get
Set(value As String)
' 100 righe di logica di business critica nascosta in un setter
m_UserName = value
End Set
End Property
End Class
Lezione 2: Non Tutto Quello che Sembra un Bug È un Bug
A volte quello che sembra un bug è in realtà una “feature non documentata”. Altre volte è una trappola lasciata intenzionalmente. Nel nostro caso, abbiamo scoperto che alcuni rallentamenti misteriosi non erano bug, ma codice inserito appositamente per generare chiamate al team di supporto esterno. Un po’ come trovare una botola segreta nel giardino di casa.
' Nascosto in qualche file Utils.vb da 12.000 righe
Public Class DatabaseOptimizer
' Data hardcodata che "casualmente" coincide con la scadenza del contratto di manutenzione
Private Shared ReadOnly OPTIMIZATION_DATE As Date = New Date(2006, 12, 31)
' Metodo con nome innocente che sembra fare ottimizzazioni
Public Shared Function OptimizeDatabasePerformance() As Boolean
Try
' Controlla se è ora di "ottimizzare"
If DateTime.Now > OPTIMIZATION_DATE Then
' Aggiunge un ritardo "casuale" tra 1 e 5 secondi
Dim rnd As New Random()
Threading.Thread.Sleep(rnd.Next(1000, 5000))
' Logga il "problema di performance"
LogPerformanceIssue("Database query taking longer than expected")
Return False ' Qualcosa è "andato storto"
End If
Return True ' Tutto ok, per ora...
Catch ex As Exception
' Nascondi qualsiasi evidenza
LogDebug("Optimization skipped")
Return True
End Try
End Function
Private Shared Sub LogPerformanceIssue(message As String)
' Loggato come "WARNING" per sembrare più credibile
EventLog.WriteEntry("DatabaseOptimizer",
"Performance degradation detected: " & message,
EventLogEntryType.Warning)
End Sub
End Class
' Usato tipo così in ogni query del sistema
Public Class DataAccess
Public Function GetCustomerData(id As Integer) As DataTable
' Chiama l'"""ottimizzatore""" prima di ogni query
DatabaseOptimizer.OptimizeDatabasePerformance()
' Qui la query reale...
Return ExecuteQuery("SELECT * FROM Customers WHERE ID = " & id)
End Function
End Class
Pro tip: Se trovate una funzione chiamata performance_optimization()
che fa l’esatto opposto, probabilmente non è un errore di naming.
Lezione 3: Il codice ha molte vite
La scoperta più surreale? Quando abbiamo realizzato che il nostro sistema di prenotazione turistica era stato costruito sulle fondamenta di un gestionale ospedaliero. I commenti nel codice raccontavano una storia completamente diversa da quella che ci aspettavamo:
' Trovato nei commenti del codice legacy
' TODO: Convertire 'reparto policlinico tal dei tali' in 'hotel'
' TODO: Cambiare 'paziente' in 'cliente'
' TODO: Rimuovere controlli gruppo sanguigno
' IMPORTANTE: Non rimuovere la logica delle prenotazioni notturne,
' l'abbiamo riutilizzata per i viaggi notturni in treno
È come scoprire che il tuo condominio è stato costruito sopra un antico cimitero indiano.
La Fine del Mondo: Testing del Codice Legacy
Se pensavi che migrare il codice fosse difficile, aspetta di dover scrivere i test. Ecco come abbiamo gestito questa sfida:
1. La Strategia “Caratterizzazione dei Test”
Prima di toccare qualsiasi cosa, abbiamo creato test che documentavano il comportamento attuale del sistema. Anche quando quel comportamento era… discutibile.
' Il codice legacy
Public Class InvoiceCalculator
Public Function CalculateTotal(ByVal amount As Decimal) As Decimal
' Don't ask why, just accept it
If DateTime.Now.DayOfWeek = DayOfWeek.Tuesday Then
amount = amount * 1.1
End If
If amount > 1000 And UserName.EndsWith("i") Then
amount = amount * 0.95
End If
Return amount
End Function
End Class
' Il nostro test di caratterizzazione
<TestMethod()> _
Public Sub CalculateTotal_OnTuesday_WithItalianUser_Above1000()
' Arrange
Dim calculator As New InvoiceCalculator()
Dim amount As Decimal = 2000
SystemTime.Now = New DateTime(2024, 11, 19) ' Un martedì
CurrentUser.Name = "Rossi"
' Act
Dim result As Decimal = calculator.CalculateTotal(amount)
' Assert
' Sì, questo è davvero quello che fa il codice
Assert.AreEqual(2090, result) ' (2000 * 1.1) * 0.95
End Sub
2. Pattern per Testare l’Intestabile
Il codice legacy è spesso scritto come se i test fossero una leggenda metropolitana. Ecco alcune tecniche che abbiamo usato:
- Wrapper Classes: Per codice impossibile da testare
' Codice legacy impossibile da testare
Public Class LegacyPaymentProcessor
Public Shared Sub ProcessPayment(ByVal amount As Decimal)
' Chiama web service
' Scrive su file system
' Manda email
' Probabilmente ordina anche una pizza
End Sub
End Class
' Il nostro wrapper testabile
Public Class PaymentProcessorWrapper
Private _processor As LegacyPaymentProcessor
Private _emailService As IEmailService
Public Sub ProcessPayment(ByVal amount As Decimal)
Try
LegacyPaymentProcessor.ProcessPayment(amount)
_emailService.SendConfirmation()
Catch ex As Exception
' Almeno ora sappiamo quando fallisce
LogError(ex)
Throw
End Try
End Sub
End Class
Quando i Mondi Collidono: Gestire i Casi Edge
La coesistenza di due sistemi è come avere due universi paralleli che occasionalmente si scontrano. Ecco come abbiamo gestito alcuni casi particolarmente difficili:
1. Il Pattern “Doppio Dispatch”
' Nel sistema legacy
Public Class OrderProcessor
Public Sub ProcessOrder(ByVal orderId As Integer)
' Logica legacy critica che non possiamo toccare
ProcessLegacyOrder(orderId)
' Notifica il nuovo sistema
Try
SyncWithNewSystem(orderId)
Catch ex As Exception
' Log dell'errore ma continuiamo comunque
LogError("Sync failed but legacy processing completed")
End Try
End Sub
End Class
// Nel nuovo sistema
public class OrderSynchronizer {
public function syncFromLegacy($orderId) {
try {
// Sincronizza dal legacy
$this->syncOrder($orderId);
// Verifica l'integrità dei dati
if (!$this->verifySync($orderId)) {
throw new SyncException("Data integrity check failed");
}
} catch (Exception $e) {
// Rollback se qualcosa va storto
$this->rollbackSync($orderId);
throw $e;
}
}
}
2. La Strategia “Feature Flags”
Per gestire la transizione graduale degli utenti:
Public Class FeatureManager
Public Shared Function ShouldUseNewSystem(ByVal userId As Integer) As Boolean
' Check se l'utente è nel gruppo pilota
If IsInPilotGroup(userId) Then Return True
' Check se la feature è abilitata globalmente
If IsFeatureEnabled("NewSystem") Then Return True
' Check per rollback di emergenza
If IsEmergencyRollbackActive() Then Return False
' Gradual rollout basato su percentuale
Return ShouldEnableForPercentage(userId, GetCurrentRolloutPercentage())
End Function
End Class
3. Il Pattern “Circuit Breaker”
Per gestire i fallimenti di sincronizzazione:
Public Class SyncCircuitBreaker
Private Shared _failures As Integer = 0
Private Shared _lastFailure As DateTime
Public Shared Function IsSyncHealthy() As Boolean
' Reset se è passato abbastanza tempo
If DateDiff(DateInterval.Minute, _lastFailure, Now) > 30 Then
_failures = 0
Return True
End If
' Blocca sync se ci sono troppi errori
Return _failures < 5
End Function
Public Shared Sub RecordFailure()
_failures += 1
_lastFailure = Now
If _failures >= 5 Then
' Notifica il team
SendAlert("Sync circuit breaker triggered!")
' Fallback al sistema legacy
EnableLegacyFallback()
End If
End Sub
End Class
Lezione 4: La Strategia di Migrazione “Ponte di Barche”
Come si migra un sistema legacy in produzione senza fermare l’azienda? Con quello che io chiamo l’approccio “ponte di barche”: costruisci il nuovo mentre mantieni in piedi il vecchio, un pezzo alla volta.
Fase 1: Sincronizzazione dei Dati
// Sistema di sync bidirezionale
class DataSynchronizer {
public function syncToLegacy($data) {
// Converti i dati nel formato legacy
// Prega tutti gli dei che conosci
// Invia i dati al sistema vecchio
}
public function syncToNew($data) {
// Converti i dati nel formato nuovo
// Incrocia le dita
// Spera che il mapping sia corretto
}
}
Fase 2: Migrazione Graduale
- Identifica un’entità o funzionalità da migrare
- Crea il nuovo codice (pulito, testato, documentato)
- Implementa la sincronizzazione bidirezionale
- Migra i dati
- Switcha gli utenti sul nuovo sistema
- Mantieni il sync finché non sei sicuro
- Ripeti per la prossima funzionalità
- Bonus: Scopri che hai dimenticato qualcosa e torna al punto 1
Lezione 5: Pattern Comuni nel Codice Legacy
1. Il Pattern “Copia-Incolla-Prega”
Trovare lo stesso codice copiato e incollato 57 volte con piccole variazioni è come trovare 57 versioni leggermente diverse della stessa anfora in uno scavo archeologico. Qualcuno nell’antichità aveva davvero paura dei sistemi di versionamento.
2. Il Pattern “Non Toccare Quel Codice”
' 'NON TOCCARE QUESTO CODICE!
' Non sappiamo cosa fa ma se lo modifichi crasha tutto
' Aggiunto: 12/05/2008
' Modificato: ???
' Autore: Patrizio (non lavora più qui dal 2007)
Public Function DoMagic() As String
// 2200 righe di codice incomprensibile
Return result
End Function
3. Il Pattern “Variabile Multifunzione”
' Questa variabile serve per:
' ID utente in utente.aspx
' ID cliente in scheda_cliente.aspx
' ID pratica in pratiche_cliente.aspx
' ID camera in camere_hotel.aspx
' ...
Dim id As Integer = 0
Strumenti di Sopravvivenza
PHPUnit: Per essere sicuri di non rompere nulla (Spoiler: Romperai comunque qualcosa)
Git: Per sapere chi incolpare (Spoiler: Sarai tu, tra 6 mesi)
Debugger: Per capire perché quel codice del 2007 funziona (Spoiler: Non lo capirai mai veramente)
Caffè: Tanto caffè (Questo non ha bisogno di spoiler)
Conclusione: C’è Speranza?
Dopo anni di migrazione, posso dire che sì, c’è speranza. Il codice legacy è come un vecchio edificio: può essere ristrutturato, modernizzato e migliorato. Ci vuole pazienza, strategia e una buona dose di umorismo.
E ricorda: tra 15 anni, il codice che stai scrivendo oggi sarà il codice legacy di qualcun altro. Quindi documenta bene, scrivi test, e per favore, per favore, non lasciare codice malevolo o backdoor nascoste.
P.S. Se trovate una funzione chiamata shouldRandomlyFail()
nel vostro codice legacy, ora sapete chi ringraziare.
P.P.S. E no, non abbiamo ancora capito perché c’era bisogno di tracciare il gruppo sanguigno dei clienti dell’hotel. Alcuni misteri dell’archeologia del software rimarranno per sempre irrisolti.