Ventiquattro anni di un framework: cosa ha retto, cosa no, e perché

In questi mesi sto reingegnerizzando un framework che ho scritto nell'agosto del 2002. Si chiama MDL, e la versione nuova gira su .NET 10 — la vecchia era partita da .NET 1.0.

Nel frattempo ci abbiamo costruito sopra un gestionale amministrativo-contabile in esercizio da allora, il software con cui gestiamo il nostro helpdesk, il framework ASP.NET che ha retto la prima generazione dei nostri form web, e una quantità di web service e API verso l'esterno: banche, sistemi di protocollo, servizi di conservazione digitale, e in pratica chiunque avesse bisogno di dialogare con un ateneo.

Nel mezzo ci sono ventiquattro anni in cui il motore è stato rifatto pezzo per pezzo mentre le applicazioni costruite sopra continuavano a girare. Mi sembra un buon momento per guardare indietro e chiedermi quali decisioni abbiano retto, quali no, e soprattutto perché.

Agosto 2002

La situazione era questa: un database esistente, la necessità di costruirci sopra centinaia di form funzionanti nel giro di pochi mesi, e una squadra in cui ero l'unico con esperienza di programmazione alle spalle. Gli altri sarebbero diventati bravi, ma allora partivano da zero.

Non è una premessa drammatica, è semplicemente il vincolo da cui dovevo partire. E se lo si prende sul serio, la conclusione è obbligata: se chi costruisce i form non può scrivere codice, i form non devono contenere codice. Tutto quello che un form fa normalmente — leggere, scrivere, filtrare, validare, interrogare la logica applicativa — deve stare da un'altra parte e dedursi da qualcos'altro.

Quel qualcos'altro è, in parte, il DataSet: la struttura dei dati in memoria — tabelle, colonne, relazioni, stato delle righe — che nel .NET dell'epoca era il contenitore standard per i dati staccati dal database. Un form diventa allora un design più un DataSet, e il DataSet è il punto in cui tutto si incontra. In parte sono i metadati, che descrivono quello che il DataSet da solo non dice: regole, comportamenti, vincoli applicativi.

E in parte, soprattutto, è la scomposizione in componenti. MDL non è un blocco unico: è un insieme di pezzi con responsabilità distinte — chi legge i dati, chi li salva, chi parla con il database, chi costruisce le condizioni di filtro. Il motore mette insieme quei pezzi a partire dalla descrizione, e il form riceve un comportamento senza averlo scritto.

Abbozzai la prima versione durante le ferie di agosto. Con quella, i form iniziarono a leggere e scrivere dati e a interrogare la business logic senza che nessuno dovesse scriverne il codice.

Non era la prima volta

Sarebbe comodo raccontare che il vincolo produsse l'architettura, ma non è andata così: era la terza volta che costruivo la stessa cosa.

Il primo esperimento fu in Turbo Vision, la libreria Borland per le interfacce a caratteri. Lì esisteva un meccanismo per cui una dialog componeva un record pezzo per pezzo, scorrendo i propri controlli in ordine e chiedendo a ciascuno il suo contributo — e lo stesso all'inverso per ripopolarli. Quel meccanismo mi era rimasto impresso, perché conteneva già l'idea centrale: il form sa dire cosa contiene, senza che nessuno lo scriva da qualche parte.

Il secondo tentativo fu un framework in Borland C++ Builder, con lo stesso principio applicato a un'interfaccia grafica vera.

MDL è il terzo. Il salto è che in .NET ogni controllo porta con sé una proprietà Tag, un contenitore libero, buono per qualsiasi cosa. Ci ho messo dentro la mappatura: quale tabella, quale colonna, che ruolo ha quel controllo. Sopra ci ho costruito il meccanismo che legge i valori dai controlli verso il DataSet, li riscrive all'indietro, e da quegli stessi controlli estrae le condizioni di filtro per le ricerche.

È per questo che un form può essere "solo un design": la mappatura non sta in un file di configurazione, sta attaccata ai controlli stessi. Chi disegna il form la sta già scrivendo, senza rendersene conto e senza scrivere codice.

C'è poi una ragione più profonda per cui questa forma mi era familiare. Un framework che si costruisce il comportamento a partire da una descrizione è, concettualmente, un interprete: la descrizione è il sorgente, il motore è il runtime. A venticinque anni programmavo già da parecchi anni, e alle spalle avevo un compilatore Lisp, uno Pascal, una libreria grafica in assembler, e il BIOS dell'M24 disassemblato per capire come funzionasse. Chi ha scritto un compilatore, davanti al problema "far fare a una macchina quello che altrimenti dovrebbe scrivere una persona", riconosce subito di che si tratta.

Quindi no, non fu il vincolo a inventare l'architettura. Il vincolo fornì l'occasione e l'urgenza a un'idea che avevo già in mano da anni — e la terza volta la misi in opera con la consapevolezza che le prime due mi erano costate.

Novembre 2002

Poi riprogettammo il database. E per novembre avevamo già centinaia — poi migliaia — di client installati.

Un pezzo di cui vado molto orgoglioso è come lo distribuivamo. Il LiveUpdate era un meccanismo costruito da noi per cui ogni client, a ogni avvio e poi periodicamente ogni ora, verificava se ci fossero componenti aggiornati e li scaricava. Non solo le DLL: anche gli script da eseguire sul database e i report.

A ogni riavvio, i client avevano l'ultima versione. Correzioni e form nuovi arrivavano in giornata.

In ventiquattro anni c'è stata una sola occasione in cui questo non è bastato: il salto fra due versioni maggiori di .NET, dove reinstallare era tecnicamente inevitabile. Succederà di nuovo ora con .NET 10. Ma anche quella volta non abbiamo dovuto mettere piede da nessun cliente: l'aggiornamento lo fecero gli utenti stessi, premendo un pulsante nel client che avevano già. Nessun intervento su misura, in ventitré anni.

ClickOnce, il meccanismo Microsoft per l'aggiornamento automatico delle applicazioni desktop, sarebbe arrivato tre anni dopo con .NET 2.0. Gli strumenti che hanno reso normale versionare le migrazioni di schema e distribuirle insieme al codice — Liquibase, Flyway — sono di parecchi anni dopo ancora. Nel 2002 quella roba ce la costruivi o non ce l'avevi.

Distribuire automaticamente script che modificano il database di un cliente è la cosa che fa venire i sudori freddi a chiunque l'abbia fatta almeno una volta. Con la contabilità di enti pubblici dall'altra parte, e senza nessuno degli strumenti di oggi, era una faccenda che richiedeva una certa disciplina.

Perché ha retto

La domanda interessante non è come sia nato, ma perché sia sopravvissuto. I framework interni muoiono quasi tutti: riscritti, abbandonati, sostituiti da quello alla moda, o semplicemente crollati sotto il peso dei casi particolari che nessuno aveva previsto. Le ragioni per cui questo non è successo sono alcune, e curiosamente nessuna di esse aveva la longevità come obiettivo.

1. Il codice stava tutto in un posto solo

L'obiettivo, in tutte e tre le versioni del framework, è sempre stato scrivere meno codice possibile. Non per pigrizia, e nemmeno per il tempo risparmiato scrivendolo — che è la parte trascurabile del conto. Per una ragione di qualità.

Il codice che si scrive una volta sola è anche quello che si manutiene di meno, perché è quello che viene eseguito sempre. Le funzionalità del core sono state stressate all'inverosimile fin dal primo giorno, da ogni form e da ogni utente contemporaneamente. Un difetto lì emerge in ore, si corregge in un punto solo, e la correzione arriva ovunque.

Il codice sparso nei form ha la proprietà opposta: ciascun pezzo viene esercitato di rado, quindi i difetti restano latenti per anni e si scoprono uno alla volta, sempre nel momento peggiore. E ogni correzione vale per un form solo.

Quindi più logica si centralizza, meno si manutengono le applicazioni — e questo conta soprattutto quando un progetto è giovane, cioè quando il rapporto fra righe scritte e difetti ancora da scoprire è al suo massimo.

La conseguenza sulla longevità è arrivata dopo, e gratis. Un form che non chiama nulla non può rompersi quando cambi ciò che sta sotto: il contratto fra applicazione e framework non è procedurale ma dichiarativo. Il form non dice al motore cosa fare, dice cosa contiene. E finché il motore sa ancora leggere quella descrizione, tutto continua a funzionare, per quanto il motore sia cambiato.

Nel caso normale un form funzionava senza una riga di codice. Nemmeno una. Il codice compariva solo quando si volevano bottoni con comportamenti particolari, o altre stranezze.

2. Restava una via d'uscita per i casi veri

Detto questo, sarebbe falso raccontare che nessun form contenesse codice. I form complicati esistono, e hanno logica propria — contabile, specifica, irriducibile ai metadati. Quelli li ho scritti quasi tutti io.

La distinzione non è fra "con codice" e "senza codice": è che il framework si occupa delle cose normali, e il codice resta per quello che è davvero specifico. Un framework che pretende di eliminare del tutto il codice applicativo muore al primo requisito che non aveva previsto, e ne muoiono molti così. La via d'uscita va lasciata aperta, a patto che serva a un decimo dei casi e non a metà.

3. La compatibilità è stata progettata ogni volta

Questa non è stata gratis. Ogni modifica al framework ha richiesto un lavoro esplicito di progettazione perché le applicazioni esistenti non si accorgessero di nulla, con tutti gli accorgimenti necessari a garantire che nessun cliente si bloccasse mai.

La superficie stretta ha reso la retrocompatibilità possibile. Il lavoro l'ha resa reale. Servivano entrambe le cose, e la seconda è quella che non si vede e che nessuno ti riconosce.

La prova più forte: il cambio di mezzo

C'è però un dato che vale più dei ventiquattro anni di manutenzione.

MDL è nato per costruire form desktop. Sopra ci abbiamo poi costruito un framework ASP.NET che generava form web, e in seguito una quantità di web service e API per far dialogare gli atenei con banche, sistemi di protocollo e servizi di conservazione. In nessuno di quei casi c'era un form da disegnare, e in molti non c'era nemmeno un utente umano dall'altra parte.

Il merito non è dei metadati in sé: è della scomposizione in componenti. Le parti che leggono i dati, li salvano e parlano con il database non avevano mai avuto niente a che fare con l'interfaccia, e questo le ha rese riusabili così com'erano dietro un form web o dentro un web service. Se una descrizione dei dati concepita per il desktop regge poi il web e poi le API fra sistemi, vuol dire che i pezzi erano stati tagliati nel punto giusto — ed è una prova più convincente della longevità, perché durare a lungo può anche dipendere dall'inerzia, mentre cambiare mezzo no.
A distanza di anni ho riscritto la stessa scomposizione in JavaScript, con moduli separati per il dataset, le espressioni di filtro, il caricamento e il salvataggio. Non per nostalgia: perché quando i confini sono al posto giusto, restano al posto giusto anche cambiando linguaggio.

La prova in corso: il fornitore della griglia

Quest'ultima non è ancora una prova, è una scommessa che sto giocando adesso, e la racconto proprio perché l'esito non lo conosco.

Per vent'anni le liste di dati sono state costruite sulla griglia di Xceed. In MDL10 quel componente lascia il posto a DevExpress. È il tipo di sostituzione che di norma si paga cara, perché una griglia non è un dettaglio: è il modo in cui l'utente vede i dati, e ogni sua particolarità tende a infiltrarsi nel codice che la usa.

Qui non dovrebbe succedere, perché le applicazioni la griglia non l'hanno mai vista: era il framework a gestirla per intero, dietro classi che facevano da involucro — quelle con cui negli anni ho avvolto Xceed pezzo per pezzo. Quelle classi ora muoiono insieme al componente che avvolgevano, e nascono le nuove — ma il costo resta tutto dentro MDL.

Se le previsioni sono giuste, i form non se ne accorgeranno. È esattamente il lavoro per cui un framework esiste: assorbire le dipendenze esterne significa che quando una di quelle dipendenze scompare — e prima o poi scompaiono tutte — a pagare è una persona sola, e non trecento applicazioni.

Lo saprò con certezza fra qualche mese. Nel frattempo è un buon promemoria del fatto che un'architettura non si giudica mai definitivamente: ogni volta che cambia qualcosa sotto, torna in discussione.

Il limite: quello che il framework non può assorbire

E qui devo raccontare il rovescio, perché altrimenti questa storia sembrerebbe più lieta di quanto sia.

Nel passaggio a .NET 10 ci sono componenti standard della piattaforma che non esistono più. Il vecchio DataGrid, il controllo per le tabelle delle prime versioni di WinForms, fu soppiantato da DataGridView nel 2005 e sul .NET moderno non è mai stato portato. Discorso analogo per alcuni componenti dei menu.

Risultato: ogni maschera che li usa va toccata. Nessun metadato mi salva, nessuna indirezione: il controllo sta nel disegno del form, e il disegno è dell'applicazione.

Ed è la regola generale: il framework può proteggerti da ciò che possiede, non da ciò che vive nel form. La griglia Xceed la usava il framework, e infatti è sostituibile senza che nessuno se ne accorga. Il DataGrid lo ha messo lì chi ha disegnato la maschera, ed è parte di quel poco che avevamo lasciato alle applicazioni.

Detto questo, la fattura è molto più leggera di quanto potrebbe essere, e la ragione merita attenzione perché è la parte istruttiva della vicenda.

Nei form era finito il tipo del controllo, non il suo uso. Tutta la logica che pilota la griglia — cosa mostrare, come popolarla, come leggere la selezione, come impaginare — era in MDL. Le maschere si limitavano a dichiarare "qui c'è una griglia". Quindi la sostituzione è meccanica: si cambia il tipo del componente, si aggiorna in MDL10 la parte che lo governa, e nel form non si tocca nient'altro.

La distinzione decisiva non è se qualcosa trapela nelle applicazioni, ma che cosa: una dichiarazione che trapela si sostituisce meccanicamente in mille posti; un comportamento che trapela va riprogettato mille volte. È la differenza fra un pomeriggio e un anno.

L'unico modo per non pagare nemmeno il pomeriggio sarebbe stato non lasciare che i form usassero direttamente i controlli della piattaforma: costruire uno strato di widget miei e far disegnare le maschere con quelli. Alcuni framework lo fanno. È una difesa enorme, che si paga ogni giorno per vent'anni contro un evento che capita una volta ogni vent'anni, e che in cambio ti obbliga a reinseguire da solo ogni miglioria introdotta dalla piattaforma.

Rifarei la stessa scelta senza esitare. Ma è giusto dire che una scelta c'era, e quanto è costata.

La prova del nove

A un certo punto, per ragioni che qui non contano, ci trovammo a dover rinominare tutte le tabelle e tutte le colonne del database.

E poiché eravamo comunque costretti a mettere le mani ovunque, ne approfittammo per fare quello che nel frattempo avevamo capito che si poteva fare meglio: ridistribuire campi fra le tabelle, normalizzare dove serviva, denormalizzare dove la normalizzazione stava solo complicando le cose. Unimmo l'utile al necessario.

Tutto questo a cuore aperto, su un sistema in esercizio, con migliaia di client che continuavano a ricevere aggiornamenti quotidiani. I clienti non si accorsero di nulla.

È stato possibile per una ragione sola: il legame fra i form e il database non stava nei form, stava nel DataSet. Il form si lega al DataSet; il DataSet descrive tabelle, colonne e relazioni; sotto, le componenti di lettura e scrittura traducono verso il database reale. Sposti il punto di traduzione e i form seguono, senza sapere che è successo qualcosa. In un'architettura convenzionale, con i nomi delle tabelle sparsi per il codice di centinaia di form, la stessa operazione sarebbe stata semplicemente impraticabile.

Quell'astrazione l'avevo costruita nel 2002 per una ragione di organico. Anni dopo ci ha permesso di sopravvivere a una situazione che con l'organico non c'entrava niente. È la cosa che ho imparato meglio in tutta questa storia: le buone astrazioni ripagano in valute che non avevi previsto. Non sai quale problema ti risolveranno, sai solo che ne risolveranno uno.

Cosa invece è invecchiato

Passo alla parte meno lusinghiera, che è anche la più utile. C'è una decisione che in MDL10 sto disfacendo, ed è quella che considero l'errore concettuale più profondo di tutta la storia.

Il descrittore di un'entità era mutabile, e si portava dentro la connessione al database, il dispatcher e il DataSet corrente. Avevo fuso in un solo oggetto due cose diverse: la descrizione di un'entità e la sessione di lavoro su quell'entità.

Finché il framework è stato un rich client monoutente, quella fusione non produceva alcun danno osservabile: c'era un solo consumatore, e il descrittore poteva tranquillamente tenersi in tasca la connessione. Anzi, faceva risparmiare codice.

Diventa un blocco nel momento in cui lo stesso oggetto deve servire più richieste insieme, o essere tenuto in cache e condiviso. In MDL10 la descrizione diventa immutabile e quindi cacheabile, mentre lo stato vivo — riga corrente, dataset, connessione — si sposta in un contesto separato, che nasce e muore con l'operazione.

Se dovessi indicare la regola generale che ne ho ricavato: quando un oggetto descrive qualcosa, non deve anche ricordarsi cosa ci stai facendo adesso. È una separazione che costa poco all'inizio e moltissimo dopo.

Un caso più piccolo, che cito perché è istruttivo al contrario. Per qualche mese, agli inizi, i client si collegavano al database attraverso un proxy costruito con il remoting di .NET — la tecnologia che all'epoca Microsoft raccomandava per la distribuzione. Fu abbandonato presto, e in MDL10 la sua ultima traccia sparisce a favore di uno strato driver esplicito. Non lo considero un errore di progetto: le scommesse sull'infrastruttura del fornitore invecchiano peggio delle astrazioni che ti scrivi da solo, e la differenza è che quelle puoi disfarle in pochi mesi, mentre un errore nelle tue astrazioni te lo porti vent'anni.

La radice è sempre quella: MDL è nato come rich client. In un rich client, separare descrizione da sessione, logica da presentazione, tuo da altrui, non produce alcun beneficio visibile — solo lavoro in più. Nel 2005 chiunque mi avrebbe dato torto a farlo.

Il conto però non è arrivato tutto insieme ventiquattro anni dopo: è arrivato a rate, e la prima l'abbiamo pagata quando siamo andati sul web. Ogni volta che portavamo i metadati fuori dal contesto per cui erano nati — un form web, un web service senza interfaccia, un'API senza utente — ci trovavamo a dover aggirare le parti che si portavano dietro la connessione o davano per scontato che ci fosse una finestra. Funzionava, ma contro corrente.

Non le chiamerei errori di giudizio. Erano decisioni corrette rispetto ai vincoli reali, che sono sopravvissute alle proprie premesse. È una categoria diversa, e a mio parere è la categoria in cui ricade la maggior parte di quello che nel software si chiama "debito tecnico".

MDL10

La versione nuova non è un rifacimento: è la stessa idea, liberata dai vincoli del 2002. E in buona parte è la formalizzazione di qualcosa che di fatto stavamo già facendo da anni, ogni volta che portavamo i metadati su un mezzo diverso da quello per cui erano nati.

Tre progetti separati al posto di uno: un core senza alcuna dipendenza dall'interfaccia grafica, che contiene metadati, accesso ai dati, query, sicurezza ed espressioni; un progetto di soli helper puri; e la parte di interfaccia, dove sta tutto ciò che tocca Windows Forms. La regola che ho seguito è una sola e non ammette eccezioni: il core non deve dipendere dall'interfaccia utente. Ciò che è contaminato — finestre di dialogo, colori, griglie — o scende nel progetto di UI, o passa da un'interfaccia con tipi del core.

I metadati diventano immutabili, quindi cacheabili. Le istanze arrivano da una factory. I filtri sono espressioni componibili invece che stringhe. I comandi restituiscono il numero di righe interessate invece di una stringa d'errore — un idioma da .NET 1.0, di quando le eccezioni erano ancora guardate con sospetto.

Nulla di tutto questo era pensabile nel 2002, e non per ignoranza: semplicemente non ce n'era la ragione, e il tempo speso a farlo sarebbe stato tempo tolto ai form che servivano per novembre.

Il mestiere

Mi considero uno sviluppatore di framework. È un mestiere che non ha un nome adatto: la cosa più vicina l'ha scritta Fred Brooks parlando del toolsmith, del fabbricante di strumenti, di chi costruisce arnesi perché altri facciano il proprio lavoro. Non ha mai attecchito come qualifica professionale, e "framework author" o "platform architect" dicono cosa costruisci ma non la cosa che conta davvero — e cioè che il tuo utente è un altro programmatore.

Ha una conseguenza che ho impiegato anni a mettere a fuoco: questo lavoro è invisibile per costruzione. Non per sfortuna, non per cattiva comunicazione. Un framework riuscito rende facili le applicazioni costruite sopra, e il merito va alle applicazioni. Nessuno guarda un gestionale che funziona da vent'anni e si domanda che astrazioni ci siano sotto. Il successo di un framework ha la forma dell'assenza di problemi, e l'assenza non si nota.

La contropartita è che quando una scelta di vent'anni fa si rivela giusta, lo scopri davvero. Non per un'opinione o un benchmark: perché il sistema è ancora lì, e perché quella volta che avete dovuto rifare il database sotto ai piedi di migliaia di client, si è potuto fare. E quando invece qualcosa è sfuggito, lo scopri con altrettanta precisione: ti ritrovi a toccare centinaia di maschere per sostituire un controllo che non esiste più, e a misurare esattamente quanto ti costa ciò che avevi lasciato fuori dal framework.

Se dovessi condensare in una riga quello che questa storia mi ha insegnato, sarebbe questa: scegli le astrazioni pensando a chi dovrà cambiarle, non a chi dovrà usarle. Gli utilizzatori li accontenti comunque, prima o poi. Chi dovrà mettere le mani nel motore fra vent'anni sei tu, e non ti ricorderai perché avevi fatto così.

Commenti

Post popolari in questo blog

I teoremi di incompletezza di Gödel

Refactoring: improving modularization

Crittografia e Hashing