
L'informatica è un mondo vasto e periglioso, e può capitare che gli eventi ci portino a dover gestire un sistema difficile, che nelle prossime righe sarà inteso come "un blob informe di codice che non conosce nessuno e porta avanti logiche complesse che non si devono rompere perché siamo online e abbiamo utilizzatori che non vogliamo scontentare". Whew.
In un mondo ideale la soluzione è semplice: riscriviamolo. D'altra parte i vecchi sviluppatori erano notoriamente dei fricchettoni buoni a nulla, e sicuramente il meglio che sono riusciti a fare è niente rispetto a quello che possiamo riuscire a fare noi. Certo, ci vorrà un po' di tempo, ma che problema c'è?
Di solito quando il Developer di turno discute la cosa col Business le cose vanno più o meno così:
D: Dobbiamo riscrivere X
B: Perché?
D: Perché funziona male ed è difficile da far evolvere, ci perdiamo sempre un sacco di tempo e metà delle volte che andiamo online rompiamo qualcosa
B: Scrivere X è costato un mucchio di soldi e funziona. Voi quanto ci mettereste a rifarlo?
D: Circa 6 mesi
B: Non possiamo tener fermo lo sviluppo così a lungo. Continuate a lavorare su X, se andate un po' più piano non importa.
E così va in fumo il sogno di un X 2.0 più facile e veloce da sviluppare.
Ma noi non siamo certo gente che si arrende, e visto che lavoriamo in un'azienda agile siamo anche liberi di inventarci soluzioni creative a problemi difficili. Si tratta solo di trovare una soluzione che:
- consenta di sostituire il codice vecchio con codice nuovo
- consenta di proseguire nello sviluppo senza (eccessivi) rallentamenti
La strategia che finora ci ha dato la massima soddisfazione può essere descritta come "sostituzione di un pezzo alla volta". In pratica l'idea è quella di cercare delle "linee" nel sistema legacy in modo da poterlo "tagliare" e sostituire un pezzettino alla volta. Il tempo speso in questa attività, così come il tempo speso nel refactoring, è "spalmato" sullo sviluppo delle varie funzionalità chieste dal Business.
Facile. Ma come si fa a tagliare un sistema?
Se si ha a che fare con un'applicazione web, una linea che generalmente si presta bene è quella che separa frontend e backend. L'idea è quella di sostituire tutto il frontend vecchio con uno nuovo fiammante, lasciando il backend al suo triste destino.
La sostituzione del frontend a sua volta sarà generalmente fatta un pezzo alla volta. Ad esempio, un modo che si presta bene consiste nel sostituire una rotta ("pagina") alla volta, usando un load balancer di livello 7 (tipo haproxy) per smistare le richieste. Via via che si è completata nel nuovo sistema una pagina che deve sostituirne una vecchia, si modifica il load balancer dicendogli che quella rotta ora deve essere servita dal nuovo sistema.
E' importante procedere per passi. Il primo è senza dubbio quello di preparare il balancer in modo che tutte le richieste vadano al vecchio sistema. Il passo successivo è quello di inventarsi una rotta non di produzione e configurare il balancer in modo che richieste per quella rotta vadano non più al vecchio sistema ma da qualche altra parte.
A questo punto si può dire che il meccanismo per la sostituzione progressiva del frontend è pronto, e si può procedere a scrivere la nuova implementazione.
Le prime pagine sostituite possono essere quelle più semplici e statiche (tipo le pagine "Chi siamo", "Termini e condizioni", ...) che ci consentono di impratichirci e di andare online (è fondamentale andare online con le nuove pagine più frequentemente possibile) per ricevere prezioso feedback dalla produzione. Non va trascurato questo aspetto: molti problemi importanti che potrebbero condurre il progetto al fallimento (es. con la tecnologia che abbiamo scelto le pagine sono troppo lente/pesanti) possono essere individuati presto, e prima si individuano meno lavoro si butta.
Le pagine dinamiche sono più complesse, perché è più difficile trovare le informazioni per riempirle.
La buona notizia è che generalmente i dati sono all'esterno anche dell'applicazione vecchia, e vivono sereni su un DB di qualche genere (relazionale o no poco importa). Questo significa che possiamo scrivere delle API nuove di zecca interrogando direttamente le basi di dati e ignorando la vecchia applicazione. Certo, in questo modo ci accoppiamo alla base dati e quindi diventerà difficile migliorarla, però guardiamo in faccia la realtà: se il DB è così messo male da non poterlo nemmeno sopportare durante la migrazione dal vecchio sistema al nuovo, allora non ci sono molte speranze di successo.
I casi nei quali i dati non dobbiamo solo mostrarli, ma anche cambiarli, possono presentare qualche insidia in più.
Se l'applicazione vecchia non usa cache, cambiarli direttamente su DB non pone particolari problemi. Il vero divertimento si ha quando l'applicazione vecchia usa cache, e quindi modificare i dati su DB crea disallineamenti di dati destinati a creare problemi difficili da gestire.
Un modo abbastanza semplice per gestire questa situazione consiste nell'implementare nel vecchio sistema una chiamata tipo "drop caches" che svuoti tutte le cache, così da forzarlo a ricaricare i dati da DB mantenendo così la consistenza. Secondo la linea della semplicità sarebbe bello avere una sola chiamata che svuoti tutte le cache, ma se ciò non fosse possibile per questioni di performance è anche possibile rendere l'API un po' più complessa per consentire l'eliminazione solo di una parte della cache. La granularità va decisa caso per caso, tenendo conto che per evitare errori difficili da diagnosticare è meglio sacrificare un po' di velocità e avere un'API più semplice.
Se il vecchio sistema è anche parte di un cluster, ricordarsi di fare in modo che tutti i nodi cancellino la cache, e non solo quello che ha avuto la ventura di essere destinatario della chiamata. In caso contrario si vedranno effetti strani (tipo pagine che cambiano in modo incomprensibile quando vengono ricaricate perché la richiesta è stata servita da nodi diversi). Come fare a propagare la richiesta è nuovamente dipendente dal caso, però c'è da aspettarsi che se un sistema ha cache e vive in cluster allora ci sarà già un qualche meccanismo di questo genere.
Ultimo caso interessante è quello della sessione utente, che dovendo essere condivisa tra i due sistemi pone nuovi interessanti problemi.
E' possibile che la sessione viva su DB come un qualsiasi altro dato, nel qual caso si tratta solo di scrivere una nuova implementazione e interfacciarla come descritto sopra. Il fatto è che spesso le applicazioni usano svariati framework e a volte una singola sessione utente è in realtà costituita dall'unione delle sessioni gestite dai vari layer che le compongono.
In questo caso una "pezza" consiste nello scrivere API nella vecchia applicazione che si aspettino di essere invocate dall'utente, e chiamarle server-to-server copiando nella richiesta tutti i cookie dell'utente. In questo modo l'onere di identificare l'utente resta alla vecchia applicazione per il tempo necessario alla migrazione di tutte le pagine. Una volta migrate tutte sarà possibile reimplementare la gestione della sessione e staccarsi da quella vecchia.
Tutto ciò, com'è facile intuire, non è una passeggiata. C'è un sacco di lavoro da fare, e visto che non ci si può lavorare a tempo piano ci vorrà anche molto tempo. Ne vale la pena?
Noi pensiamo di sì. Nella nostra azienda abbiamo almeno 3 sistemi già in produzione che sono nati come sostituti di omologhi vecchi, e un quarto è in fase di transizione. I benefici di poter progressivamente essere veloci sulle parti sostituite possono essere grandi, e c'è anche da tenere in considerazione che il morale tende a essere più alto se la quotidianità del lavoro non è solo spaccarsi la testa su un sistema orribile ma anche trovare soluzioni creative per allontanarsene.
Buona riscrittura incrementale a tutti!
0 commenti:
Posta un commento