Settembre 21, 2017 - Commenti disabilitati su Refactoring e architettura emergente di un’applicazione Zeppelin

Refactoring e architettura emergente di un’applicazione Zeppelin

Condividiamo alcune esperienze sulla gestione di un progetto complesso in ambiente Apache Spark/Apache Zeppelin.

Uno dei progetti su cui il team di XPeppers ha lavorato negli ultimi mesi è stato lo sviluppo di un'applicazione di software quality dashboard per un grande gruppo bancario. Il compito dell'applicazione è fornire un pannello di controllo centralizzato attraverso cui monitorare la qualità del software rilasciato dai numerosi fornitori del gruppo. I dati da gestire sono generati da SonarQube, aggregati e rappresentati secondo logiche custom definite dal Cliente. I progetti da tenere sotto monitoring sono molti, e per ciascuno di essi si devono processare sia dati attuali che trend storici; serviva quindi una piattaforma di big data management. La scelta è stata di usare la coppia Apache Zeppelin / Apache Spark, sviluppando il codice necessario per le elaborazioni e visualizzazioni custom in Scala (per l'elaborazione dati) e AngularJS (per la UI), e usando SBT come strumento di build.

Il primo spike

La prima versione dell'applicazione è stata sviluppata in modo un po' quick & dirty: volevamo arrivare velocemente a mostrare un prototipo funzionante agli stakeholder e capire se la scelta dello stack tecnologico (e soprattutto di Zeppelin come front-end) fosse accettabile. La demo è piaciuta e ci siamo trovati con la "release 0" (ci siamo concessi un approccio "Spike and Stabilize").

Questa prima versione era costituita da una singola nota Zeppelin, suddivisa in una ventina di paragrafi, molti dei quali contenenti diverse decine di righe di codice Scala e SQL più o meno complicato. Soltanto due servizi erano forniti da classi Scala compilate separatamente: il dialogo HTTP con i Web Service di SonarQube, e la gestione di una base dati locale in formato Parquet.

noterel0

Primo refactoring

Era il momento di fare un po' di stabilize, e il problema principale della release 0 era evidente: troppo codice troppo complicato inserito nel corpo della nota Zeppelin, e quindi di gestione incredibilmente difficile (versioning quasi inutile, impossibilità di usare IDE o editor evoluti per modificarlo, ...)

Abbiamo quindi cominciato a migrare il codice dalla nota Zeppelin verso classi Scala, cogliendo l'occasione per aggiungere dove possibile test unitari (in Scalatest). Il codice ha cominciato ad assumere una forma vagamente object-oriented, anche se ancora un po' rudimentale.

La relazione fra la nota e le classi Scala importate da JARera un po' migliorata:

notes2.png

Nel frattempo il PO ha cominciato a chiedere modifiche alla nota originale, e un bel giorno le dashboard richieste sono diventate due, molto simili fra loro come layout, ma con diversi set di dati visualizzati (dati di leak period vs. dati storici per una data scelta in input). Questo è stato anche il momento in cui abbiamo agganciato per la prima volta una callback (scritta in Scala) alla GUI AngularJS della dashboard (usando il metodo angularWatch dello ZeppelinContext).

Il secondo punto di partenza

Il fatto di aver migrato una parte del codice dalla nota ai sorgenti Scala, ovviamente, si è rivelato provvidenziale nel passaggio da una a due note. Tuttavia, le due note attingevano a fonti di dati diverse (il database Parquet per la nota principale, i WS Sonar per la nuova dashboard "di leak period") e per farlo usavano metodi nati indipendentemente e collocati in classi diverse. Inoltre, entrambe le note contenevano ancora parecchio codice; andavano lente, ed erano instabili. La manutenzione e il refactoring erano sempre molto difficili. Sapevamo che le classi Scala avevano parecchi problemi (a partire da un forte difetto di SRP: avevamo 2 o 3 classi “demi-god” con decine di metodi di accesso ai dati), ma le possibilità di refactoring erano limitate dal fatto che mancavano ancora molti test unitari e prevedere/verificare le conseguenze delle modifiche sulle note era molto complesso.

Nel frattempo, erano entrate nuove richieste dal PO, tra cui una per una terza dashboard (sempre simile come layout ma diverse come logica di caricamento dati), e ogni tanto ci veniva chiesto di clonare una nota per una demo. Queste note “demo” diventavano tipicamente inutilizzabili dopo qualche settimana, perché legate a codice modificato in continuazione, e non soggette a manutenzione.

Con la terza nota avevamo una valida giustificazione per un nuovo refactoring. A questo punto era evidente che l'applicazione sarebbe stata soggetta a ulteriori evoluzioni per i mesi a venire, e che avevamo raggiunto la soglia di design payoff: per non rallentare dovevamo fermarci, e rivedere più profondamente il design.

Secondo refactoring

Nelle settimane successive non abbiamo rilasciato quasi nessuna major feature, e abbiamo lavorato sul debito tecnico, in modo mirato, fino a portare a casa due breakthrough principali:

  • abbiamo trovato un’implementazione molto più efficiente per le note, che ha reso possibile provare il funzionamento in poche decine di secondi (vs. tempi sopra la decina di minuti) e cominciare a parlare di una ten-minute build comprensiva di integration test;
  • abbiamo definito un’interfaccia comune (in effetti una classe astratta) da cui far dipendere, in modo uniforme, tutte le note, migrando faticosamente la vecchia pletora di metodi simili-ma-diversi sotto il cappello della nuova interfaccia. L’abbiamo chiamata Report (nel senso di un report Sonar).

A questo punto la differenza principale fra le diverse note consisteva nella classe concreta che veniva istanziata per popolare la nota. La “Leak Period Dashboard” istanziava un LeakPeriodReport, la “Dashboard” principale (che mostra un report riferito a una data fornita in input) istanziava un TimeReport, la “Comparative Dashboard” (che mostra un report che è il delta fra due date, una generalizzazione del concetto di leak period) istanziava un DeltaReport (dandogli in pasto in costruzione altri due Report fra cui calcolare il delta). Una cosa così:

uml1

Le differenze fra le varie dashboard non erano proprio tutte solo nell’istanziazione del Report concreto: alcune note avevano bisogno di agganciare callback e altre no, c’erano delle variazioni nelle visualizzazioni di alcuni valori, e così via. Ma eravamo arrivati a note con 3-4 paragrafi ciascuna:

  • inizializzazione delle dipendenze (con l'interpretespark.dep)
  • setup di eventuali dati specifici (per esempio, le date fra cui scegliere nelle note "temporali")
  • istanziazione di un (certo tipo) diReport
  • popolamento della dashboard a partire dal report (una serie di binding delle variabili Angular a partire dai dati forniti dalReport)
  •  eventuale aggancio di callback

Terzo refactoring

A questo punto è arrivata una richiesta del PO di tipo nuovo:

"Belle. Ora fatemele uguali anche per questi altri 4 gruppi di progetti."

Si trattava di passare da 3 dashboard (simili fra loro)... a 15!

Se non avessimo già estratto il codice dalle note nelle classi compilate, sarebbe stato un bagno di sangue (sia creare le nuove note che, soprattutto, manutenerle). In realtà le note erano già molto piccole e facilmente clonabili senza incorrere in un effort di manutenzione enorme. C’era comunque ancora un po’ di codice, e come detto sopra, alcune piccole differenze di logica (aggancio di callback, setup specifici, ecc.).

A questo punto abbiamo fatto un ultimo passo di refactoring/DRY: abbiamo spostato TUTTO il codice dalle note nelle classi Scala compilate separatamente, inclusi i binding di variabili Angular e gli altri statement che facevano uso delloZeppelinContext(per inciso, questo ha richiesto l'aggiunta di una nuova dipendenza nel nostrobuild.sbt: "org.apache.zeppelin" %% "zeppelin-spark" % "0.6.1").

Finalmente tutto il codice era gestibile via IDE e alla portata dei test unitari, e non abbiamo avuto difficoltà a fare al volo un ulteriore passo di riorganizzazione, portando a fattor comune in una classe astratta tutto il codice invariante fra le diverse note (p.es. il popolamento delle variabili Angular a partire dai valori tornati dal Report), e isolando le differenze in tre sottoclassi concrete:  BasicDashboardNote,  LeakPeriodDashboardNote, e ComparativeDashboardNote:

uml2

Su Zeppelin abbiamo lasciato solo il minimo indispensabile, ovvero due righe essenzialmente uguali in ogni nota:

val projects = z.input("Elenco dei progetti sotto analisi")
new BasicDashboardNote(z, projects)

Nota: i diagrammi sono fatti con i tool di yUML.

Published by: xpeppers in Agile, Coding