Stairway to Hell

E’ da un po’ che penso all’insieme di tecniche, regole, principi e strumenti che conosco, e sempre più spesso mi rendo conto che, oltre al contesto tecnologico della loro applicazione, c’è un’altra proprietà molto interessante secondo cui catalogarle, quello che ho preso a chiamare “ordine”, come se si trattasse dei pezzi di un’approssimazione di MacLaurin.

L’ordine rappresenta l’immediatezza degli effetti di una pratica di sviluppo (sia questo il termine generico) sul risultato finale. Lo si può definire per differenza come “la complessità oltre la quale il mancato utilizzo della pratica porta all’esplosione dei costi e alla morte del progetto entro l’ordine seguente”. Proprio come nelle approssimazioni polinomiali, più si ci allontana dall’origine dell’approssimazione più bisogna introdurre le componenti di ordine superiore.

Quindi il mancato utilizzo di una pratica di primo ordine porta alla morte del progetto (costi del tutto esplosi) mentre questo si avvicina a una complessità del secondo ordine.

Questo non vuol dire che tutti le pratiche che elencherò saranno quelle che reputo migliori per una certa complessità, sono solo quelle -strettamente necessarie per non morire-, alcune sono sostituite da altre che però appaiono in ordini più alti, ma la cui assenza, quando la complessità è bassa, non porta automaticamente alla morte.
Perchè le ordino quindi in questo modo, invece di concentrarmi semplicemente sulle “migliori” in assoluto? Non perchè io creda che le pratiche di ordine più basso siano più facili e veloci, e quindi adatte a sviluppi piccoli, in verità molte delle pratiche di ordine superiore hanno proprio la caratteristica di accelerare lo sviluppo e di aumentare il valore del prodotto a tutti i gradi di complessità. Proprio per questo sono critiche quando le linee sono tante. Il motivo effettivo di questa categorizzazione lo discuterò alla fine.

Per semplificare, come spesso si usa, conteggio la complessità con le linee di codice, in particolare linee di codice di un linguaggio moderno ad oggetti in un’applicazione in cui le linee di codice sono in qualche modo correlate a un numero crescente di features (insomma, non un mega algoritmo di ricerca, che fa molto bene una sola cosa, ma qualcosa di molto variegato nel range dei suoi compiti : una “storia” ogni mille linee). Ovviamente senza conteggiare le linee di librerie di terze parti, frameworks etc.

1° ordine : 100 linee, qualcosa di più di un hello world
2° ordine : 1.000 linee, la piccola componentuccola.
3° ordine : 10.000 linee, una applicazione di qualche utilità anche se presa da sola.
4° ordine : 100.000 linee, classica web application completa, multilayer.
5° ordine : 1.000.000 linee, grosso sistema, il classico “progettone” di una piccola società
6° ordine : 10.000.000+ linee, mega sistemone tuttologo integrato, mostro sviluppatosi in anni nella pancia di una grossa entità, con vari gruppi di sviluppo che si sono avvicendati nel tempo, magari appartenenti a un set di società terze.

Dunque, secondo la mia personale esperienza, catalogo le pratiche di sviluppo secondo il loro ordine :

1° ordine : 100
– Suddivisione delle procedure in sottoprocedure
l’alternativa è una singola enorme procedura, facendo così i costi esplodono quando superi le cento linee e la morte sopravviene intorno alle mille (secondo giorno di sviluppo).
– Nomi significativi
quando variabili e procedure hanno nomi oscuri bisogna sempre tenere a mente il loro significato, se sono davvero oscuri si inizia a far fatica sopra le cento e verso le mille nessuno ci capisce più niente.

2° ordine : 1K
– Strutture dati
se non si pensa in anticipo alle strutture dati, ad esempio, se non si usa una gerarchia di oggetti quando serve, si inizia ad impazzire sopra le mille linee e non riesci più a scrivere una sola procedura verso le diecimila, semplicemente perchè la struttura dei dati non rispecchia il problema.
– Uso di variabili d’istanza
se si procede tutto a metodi statici, per quanto vengano usate le pratiche del primo ordine, avvicinandosi alle 10K linee si inizia davvero a non capirci più nulla, si ha per le mani del codice puramente procedurale, con metodi con moltissimi parametri e un mucchio di variabili.

3° ordine : 10K
– Compilazione real-time (o con un click)
se il processo di compilazione è una fase a parte nel proprio processo di sviluppo, eseguita non continuamente, ogni compilazione rivelerà moltissimi errori di battitura e di svista, questi sono veloci da correggere, ma sarebbero stati ancora più veloci se fossero invece stati evidenziati dall’IDE, o dallo script di compilazione continua.
– Almeno un paio di tests di integrazione durante lo sviluppo
attendere l’ultimo momento per integrare il software significa non avere nessun feedback del comportamento di quanto si è scritto, se non quando è troppo tardi. Se il software sono 10.000 linee magari si avrà il tempo di debuggare in fretta e furia e si riuscirà a rilasciare qualcosa, ma verso le 100K questo sforzo frenetico è anche conosciuto come “allungarsi a dismisura dei tempi di sviluppo e conseguente morte del progetto”.
– Coinvolgimento del cliente
affidarsi a un elenco di features definite in partenza e mai approvate dal cliente se non alla fine di tutto, porta, da questo punto in poi, grane sempre maggiori, per il semplice fatto statistico che, più features ci sono, più ci sono ambiguità e dunque risultati che non rispettano l’idea del cliente. Da qui in poi le features richieste dal cliente sono almeno una dozzina.

4° ordine : 100K
– Design Patterns
un bel design, con tanti patterns, e un sacco di diagrammi UML è quello che ci vuole per superare con comodo le cinquencento classi.
– Programmare sull’interfaccia
se i tipi delle variabili, dei return e dei parametri non sono classi astratte, tutti i nostri metodi e le nostre classi saranno estremamente poco polimorfici. La perdita di polimorfismo porterà a duplicazione del codice.
– Pochi parametri per metodo
se le responsabilità delle classi sono sballate a tal punto da avere delle procedure piene di parametri si sono in verità utilizzati gli oggetti solo per raggruppare dei metodi, e si ci trova un sistema puramente procedurale ben organizzato. Il milione di linee probabilmente ucciderà tutto questo.
– SRP
avere delle classi che fanno troppo inizia a questo punto a portare all’esplosione combinatoria delle estensioni. Prima si notava meno, perchè il numero delle classi era basso. A questo punto, un’architettura cresciuta sui dati, invece che sui comportamenti, ha ammonticchiato tipicamente una dozzina di metodi significativi per ogni classe, e ogni nuova feature sicuramente ne porta almeno due a divergenza; una divergenza che non viene gestita con la composition deve essere gestita con l’ereditarietà, e quindi avremo sia mancanza di chiusura che duplicazione massiccia del codice.
– Don’t Repeat Yourself
la sincronizzazione di dati duplicati, sopra questo numero di linee, tende a far impazzire la complessità degli algoritmi e a moltiplicare il numero dei parametri dei metodi, per il semplice fatto che, per mantenere la coerenza dei dati, bisogna avere sotto mano, sempre, tutti i punti in cui si trovano, prima magari erano solo due, ma da qui in poi tendono a essere tre, quattro o più.
– Test unitari automatizzati
senza unit tests, quando le classi iniziano a essere più di 500, si perde traccia delle responsabilità di ognuna, la documentazione inizia a essere irrimediabilmente obsoleta e i tests ai morsetti non forniscono i dettagli necessari a mantenere coerente l’uso e l’estensione del codice esistente. “Guardare cosa fa il sistema” non è più sufficiente per capire dove mettere le mani. Il trasferimento di conoscenza, allo stesso modo, diventa troppo costoso se non è appoggiato da micro casi d’uso.
– Automatizzazione del deployment
il deployment automatizzato è prerequisito dell’integrazione continua, sopra questo ordine di grandezza del progetto si deve poter integrare tutto frequentemente, e quindi automaticamente. Incidentalmente il feedback end-to-end frequente copre i punti che sfuggono ai tests unitari.

5° ordine : 1M
– Liskov
con alcune migliaia di classi in gioco e probabilmente una configurazione estremamente dinamica e distribuita su più macchine, la non aderenza a Liskov inizia a costare molto, cosa usare, e dove, deve essere una caratteristica built-in nella definizione dei tipi del sistema, che deve assicurare la sostituibilità e non qualcosa di inaffidabile e da documentarsi caso per caso.
– Avoid singletons
quanto più il sistema è vasto tanto più si sente di avere bisogno a disposizione una quantità di informazioni, la tentazione di usare il singleton per raggiungere queste informazioni ha probabilmente portato a un centinaio di questi simpatici oggettini, conosco almeno un paio di sistemi che stanno esplodendo nei costi di manutenzione ed estensione perchè tutti questi singletons sono di fatto non-sostituibili e quindi non permettono la chiusura verso il cambiamento. Shotgun surgery anyone?
– Legge di Demeter
strisciante come è Demeter, la sua mancanza ci danneggiava già da tempo, ma da qui in poi i trenini di getters, con il loro bagagli di dipendenze efferenti, ci impediscono di seguire la pratica seguente.
– Evitare le ciclicità nelle unità di rilascio
riutilizzare singolarmente le classi bastava quando ne avevamo cento, ora che ne abbiamo qualche migliaio è il caso di riutilizzarle a grossi gruppi, per farlo dobbiamo essere in grado di definire dei gruppi, ma se ci sono due gruppi, A dipende da B e B dipende da A, non abbiamo due gruppi, ne abbiamo uno solo. Punto.
– Isolamento delle interfacce
il polimorfismo dato dalle astrazioni è una gran bella cosa, il passo seguente è riutilizzare queste astrazioni in diversi contesti, più un’interfaccia è carica di metodi, più definisce delle unità di riuso ampie, più sono ampie le unità di riuso, meno si trova un contesto dove riusarle… per chi non l’avesse ancora capito, questo post è la fiera delle banalità.
– Tests di acceptance automatizzati
con varie centinaia di features nel sistema il cliente non è in grado di controllarle “a mano”, la sua presenza e coinvolgimento non bastano più, deve essere messo in grado di definirle in modo univoco e controllabile automaticamente.
– Refactoring sistematico
personalmente ritengo che il limite tecnico dove il big design up front cessa di essere solo un metodo presuntuoso e inefficiente di definire l’architettura di un sistema e inizia invece a essere la diretta causa del fallimento di un progetto, il punto cioè, dove nessuno è più in grado, anche con i migliori sforzi, di far aderire l’implementazione a un design fatto a tavolino 10 mesi prima, sia localizzato da queste parti.
– Mocking e stubbing selvaggio
uno dei vantaggi di una copertura di tests unitari è che quando qualcosa si rompe, durante lo sviluppo di nuove features, il difetto viene immediatamente notato. Questo però non è più sufficiente quando si hanno alcune centinaia, se non migliaia, di tests, semplicemente per una questione di numeri : se prima rompevo qualcosa tre tests diventavano rossi, io li guardavo, e capivo in 40 secondi qual’era il problema, ora se rompo qualcosa i tests che diventano rossi sono trenta, e ci metto almeno 10 minuti a identificare qual’è effettivamente la classe che sta causando il fallimento. Se invece si mocka quasi tutto ciò che non è direttamente la classe sotto test, ci sarà, in ogni test, solo una classe che possa cambiare, il resto saranno tutti mocks e stubs quasi privi di pericoli di regressione, a questo punto il rapporto è uno ad uno, e quindi, quando io rompo qualcosa, si rompe solo un test, quello che effettivamente è responsabile di testare la classe dove c’è il problema.

6° ordine : 10M
– Test driven development
nella mia esperienza il rateo dei difetti quando il codice è sviluppato in TDD è circa un quarto di quello che ho quando sviluppo vecchia maniera e scrivo i tests dopo. Inoltre, strano a dirsi, ciò che ottengo col TDD richiede meno rifattorizzazione per mantenere la qualità del software, e a questi livelli il debito di design sappiamo avere degli interessi enormi. E’ un’ipotesi, perchè non ne ho esperienza diretta, ma sono convinto che l’approccio “feature-driven” sia l’unico che permetta di ottenere il design potente e eppure semplice che è necessario per non iniziare a morire lentamente una volta superate le 5000 features.
– Gestione automatizzata di tutti gli artefatti
non avere uno strumento che gestisce la complessità di decine di prodotti deployati in modo indipendente, ma sviluppati con una grande quantità di moduli comuni e continuamente riutilizzati (prerequisito per arrivare vivi fino a qui) trasforma lo sforzo di sviluppo in uno sforzo di gestione. Malevola, come sempre, torna la viscosità ambientale.

C’è dell’altro, ma non saprei mai elencare a memoria ogni singola tecnica che trovo vitale, devo trovarmi ad usarle o farmi dell’auto-brain-storming per qualche ora con un foglietto dove scrivere tutto, e non ne ho proprio voglia. Magari aggiungerò, con il loro ordine, altre tecniche, quando mi troverò ad usarle nei prossimi giorni, ma per adesso mi fermo qui.

Perchè questa categorizzazione dunque.

Il motivo è molto pratico : si impara solo sbagliando. Sfortunatamente molti errori tendono a essere evidenti solo da un certo punto in poi, esattamente come il debito di design non si fa sentire immediatamente, pratiche che si credevano eccellenti si dimostrano fallimentari solo a mesi o anni di distanza (superata una certa complessità) e si scopre quindi che erano sufficienti solo fino a un certo punto.

Appurato che l’educazione accademica fornisce solo a pochissimi una preparazione tecnica che includa anche solo alcune delle pratiche che ho elencato sopra, per non parlare di design OO, l’unico caso in cui queste verranno effettivamente apprese, è dopo essere stati testimoni (e fautori) della morte, o dell’agonia, di uno o più progetti software dell’ordine di grandezza necessario perchè una certa problematica risulti evidente.

Ma se l’industria locale supera raramente le 100K linee di codice per singolo progetto (e per questo ringrazio anche i frameworks) il numero di persone esposte alle dure lezioni dello sviluppo di sistemi complessi è estremamente ridotto. Detto questo, non mi stupisce che il programmatore in Italia non sia visto come il professionista che è nel mondo anglosassone. In effetti anche un neolaureato ha la preparazione per arrivare vivo alle 10K linee, peccato che difficilmente avrà mai la responsabilità di scriverne molte più di così, e quindi non crescerà, come invece crescono molti dei programmatori di US e UK.

Quindi questi “ordini” sono un’indicazione di quale sia il potenziale di crescita tecnica per un individuo che inizia a programmare nell’industria. Meno si affrontano ordini elevati, più è breve la carriera tecnica, perchè breve è il periodo di apprendimento e quindi, in finis, il valore della propria professione. Mentre sappiamo benissimo quali cose grandiose siano in grado di ottenere professionisti con 20 anni di carriera tecnica alle spalle. Per avere 20 anni di apprendimento (e dopo 20 anni averne ancora da imparare) bisogna però affrontare problemi di ordine molto più elevato di quelli che si vedono in giro (almeno, dalle mie parti), altrimenti ogni azienda nel settore avrà solo “ragazzi” e, non a caso, si terrà ben lontana da progetti di dimensioni elevate, perchè, chissà poi come, quelli tendono a esplodere in mano. E’ un circolo vizioso che produce un sacco di manovali, pochi prodotti di livello internazionale, quindi ben poche occasioni per essere obbligati ad apprendere cose che arricchiscano le professionalità e così via.

Leave a comment