Le 5 cose da sapere prima di una migrazione da legacy code

Archeologia del software
Sviluppatore prova a modificare codice di applicazione legacy in produzione
Matteo 9 min

“È 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:

  1. Negazione: “Non può essere così male…”
  2. Rabbia: “CHI HA SCRITTO QUESTO CODICE?!”
  3. Contrattazione: “Ok, forse se riscrivo solo questa parte…”
  4. Depressione: “Non finiremo mai questa migrazione”
  5. 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:

  1. 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

  1. Identifica un’entità o funzionalità da migrare
  2. Crea il nuovo codice (pulito, testato, documentato)
  3. Implementa la sincronizzazione bidirezionale
  4. Migra i dati
  5. Switcha gli utenti sul nuovo sistema
  6. Mantieni il sync finché non sei sicuro
  7. Ripeti per la prossima funzionalità
  8. 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

  1. PHPUnit: Per essere sicuri di non rompere nulla (Spoiler: Romperai comunque qualcosa)

  2. Git: Per sapere chi incolpare (Spoiler: Sarai tu, tra 6 mesi)

  3. Debugger: Per capire perché quel codice del 2007 funziona (Spoiler: Non lo capirai mai veramente)

  4. 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.

content_copy Copiato