Dr. Carlo Pescio
Bugs Everywhere

Pubblicato su Computer Programming No. 73


Possiamo imparare molto dai nostri bug. A ben guardare, possiamo imparare molto anche dai bug degli altri...

Come si diventa un buon software engineer? Attraverso lo studio, l’applicazione, il lavoro. Ed anche sbagliando, ed imparando dai propri errori. Ancora meglio, imparando anche dagli errori altrui, ed evitando così di pagare di tasca nostra quello che è già stato pagato da altri.
Eppure parlare dei propri errori è difficile. È molto più soddisfacente raccontare dei propri successi, spiegare con dovizia di dettagli le nostre migliori soluzioni ai problemi più complicati, e nascondere a tutti gli inevitabili sbagli commessi da chiunque abbia un po’ di anni di esperienza alle spalle.
Questa puntata un po’ controcorrente di Principles&Techniques è dedicata ad alcuni errori real-world che ho commesso, che ho rischiato di commettere, o che ho riscontrato con maggiore frequenza nei vari progetti di cui mi sono occupato.
Inizialmente volevo discutere principalmente dei tradizionali bug a livello di codifica. D’altra parte, questo è anche il livello meglio supportato, sia per quanto riguarda gli strumenti che la conoscenza a disposizione del programmatore: ad esempio, il mio "C++ Manuale di Stile" è spesso utilizzato come guida per evitare i bug di codifica più frequenti.
I "bug" di cui vi parlerò non riguardano quindi il livello implementativo: ho cercato invece di isolare alcuni eventi significativi nelle altre fasi dello sviluppo del software, attività complessa all’interno della quale intervengono fattori tecnologici, umani, individuali, eccetera.
Come sempre, ho anche cercato di andare un po’ oltre e di discutere qualche tentativo di prevenzione, più o meno sistematica, degli errori incontrati.

Risolvere il problema sbagliato
L’attività tipica del consulente è di trovare delle soluzioni ai problemi dei clienti. Per chi (come il sottoscritto) risulta "visibile" anche al di fuori della cerchia dei clienti, questo significa ritrovarsi con una quantità industriale di email a cui rispondere.
Molto spesso, chi richiede una soluzione non spiega il vero problema. Spiega invece un problema secondario, che ha incontrato cercando di mettere in pratica una sua soluzione "privilegiata" al vero problema.
Un esempio reale: alcuni anni fa ho progettato l’architettura di un sistema aperto di acquisizione e analisi dati, usato per controllare diverse apparecchiature in campo medico. L’API fornita a chi sviluppava nuovi moduli di controllo cercava, per quanto possibile, di disaccoppiare l’acquisizione (sincrona e interrupt-driven) dal trattamento (asincrono, per quanto soft real-time): un classico pattern, detto Half-Sync, Half-Async [SC96], cui ho già accennato in passato. Durante lo sviluppo di una nuova apparecchiatura, il responsabile del progetto mi chiese in toni un po’ disperati di aggiungere una ulteriore funzione all’API, per accedere "al più recente campione acquisito" (anziché al più vecchio campione non ancora trattato). Dal mio punto di vista si trattava di aggiungere poche righe al modulo di interfaccia tra driver e moduli di analisi, ed ero fortemente tentato di accontentarlo all’istante.
Comunque gli chiesi la ragione, scoprendo che voleva utilizzarlo come misura di sicurezza. La sua apparecchiatura richiedeva un modulo di pilotaggio molto oneroso dal punto di vista computazionale. Se il sistema veniva configurato con alcuni parametri limite, e poi sovraccaricato, poteva risultare pericoloso per il paziente anche nei pochi secondi necessari affinché la diagnostica di sovraccarico intervenisse a fermarlo.
Tuttavia la sua soluzione era solo un palliativo (che poi abbiamo comunque implementato come terza misura di sicurezza). Alla fine ho potuto convincerli che era necessaria una valvola (puramente meccanica) in più nel dispositivo, che in fase prototipale era stata esclusa per eccessiva fiducia nel software e nell’elettronica. Questo è un errore apparentemente più diffuso di quanto vorremmo: si veda ad esempio il caso del Therac-25, che purtroppo ha anche causato alcune vittime [LT93].
In quel caso, cercare il vero problema è stato fondamentale. Tuttavia situazioni analoghe (magari meno rischiose) sono molto diffuse, anche perché le cause del fenomeno sono tante.
Talvolta, chi chiede una soluzione non è neppure conscio del vero problema: può succedere che una soluzione diventi così cristallizzata da essere scambiata per il problema originale [Pes97].
In altri casi, la strada intrapresa sembra quella ovvia e naturale, ed il tempo necessario a spiegare completamente il problema originale sembra decisamente troppo, quindi ci si limita a richiedere una soluzione ad un sotto-problema secondario.
In altri casi ancora, il problema vero sembra troppo confuso ed astratto per poter essere spiegato, mentre il problema secondario è più facile da esprimere in termini concreti: questo non dovrebbe sorprendere, perché il problema secondario è spesso strettamente tecnologico, non di rado a livello di micro-design o di codifica, mentre il problema vero è spesso a livello applicativo, di dominio del problema.
La cura migliore che ho trovato, e che cerco sempre di applicare sul lavoro (tranne i casi di emergenza, o quelli in cui ho già acquisito le necessarie informazioni in fasi precedenti) è di risalire all’indietro di qualche livello, chiedendo semplicemente perché è necessario risolvere il problema presentato. Spesso un solo "perché" non basta, e bisogna risalire parecchio prima di identificare il vero problema. Questo è decisamente time-consuming, e sta ovviamente al nostro buon senso capire quando fermarsi.
In realtà, con l’esperienza si capisce ben presto che "i perché" sono una delle informazioni più importanti ad ogni livello (analisi, design, implementazione) e che purtroppo sono anche dolorosamente assenti dalla documentazione. Spesso la documentazione di design spiega a parole i diagrammi (cosa ovviamente del tutto inutile), mentre dovrebbe spiegare il razionale dietro le scelte di design operate. È molto comune trovare commenti che spiegano cosa fa il codice (informazione che possiamo ottenere da soli) anziché spiegare perché fa una certa cosa. Gli stessi project manager dicono cosa fare ai programmatori, ma spesso non spiegano loro perché (un errore molto grave, che porta direttamente a soluzioni win-lose anziché win-win). E così via.
Il miglior salto qualitativo nella documentazione sta proprio nell’evitare di spiegare quello che è già nel prodotto, e discutere invece perché il prodotto è fatto così. Più in generale, le soluzioni migliori si ottengono cercando perlomeno di abbozzare il problema vero, ed eventualmente di chiarire la soluzione intrapresa ed i problemi secondari. In questo modo possiamo ragionare a diversi livelli ed ottenere il miglior equilibrio possibile.

Eliminare i sintomi invece del problema
Questo è decisamente un errore classico. Sotto la pressione di una consegna imminente, emerge un bug insidioso; mentre cerchiamo di scoprirne la causa, troviamo un modo semplice per evitare che il bug abbia un effetto visibile. Ad esempio, se una funzione scritta da un altro programmatore restituisce un numero doppio rispetto a quello che ci aspettiamo, prendiamo il risultato e lo dividiamo per due. Sicuramente si fa prima così, anziché chiedere all’autore (altrettanto sotto pressione) e anziché mettersi d’impegno a capire il codice altrui.
Oltre ad essere un errore classico, è anche uno dei comportamenti meno professionali che possiamo assumere. Nascondere un bug, aggiustando il codice chiamante, ha senso soltanto quando la funzione chiamata è intoccabile (es. una API del sistema operativo) e insostituibile in tempi ragionevoli. Altrimenti significa fare un salto nella fede e sperare di aver "beccato" l’unico errore della routine e di averlo bypassato.
In questo caso devo dire che, dopo alcune scottature pre-professionali (chi dice che un po’ di smanettamento non serve...) ho imparato presto ad essere decisamente testardo. Un bug non è risolto sino a quando non si capisce esattamente cosa sta succedendo e perché. Solo a quel punto si può scegliere la soluzione più indicata, che sia una riscrittura totale o una patch locale.
Questo può sembrare eccessivo, e forse lo è nei casi di software amatoriale. Parlando però di sistemi mission-critical, la prospettiva cambia. Vediamo un altro caso reale: alcuni anni fa, una azienda svedese mi aveva commissionato un driver per una nuova scheda (utilizzata per l’acquisizione di dati fisiologici), fornendomi un prototipo. Lo sviluppo di prodotti per il campo medico è piuttosto meticoloso se confrontato con molto software commerciale, e prima di passare la scheda in produzione il driver ed il prototipo erano stati testati molto a fondo. Quando arrivano i primi campioni di produzione, sembra che il driver sia sbagliato - il campionamento avviene con un errore in frequenza del 10% circa. La scheda era in produzione, il rilascio vicino, e la pressione per applicare una soluzione locale (modificare la programmazione del timer, un lavoro di pochi minuti) decisamente molto forte. Altrettanto forte è stato il mio rifiuto. Solo dopo una giornata spesa tra debugger, assembler ed oscilloscopi è emerso il reale problema. In fase di produzione era stato montato un quarzo sbagliato sulla partita di schede. Una delle armoniche era vicina alla frequenza voluta, ed il quarzo oscillava su quella, più per caso che per altro. Risolvere il problema riprogrammando il timer avrebbe significato il rilascio sul mercato di una partita di prodotti difettosi, ritrovandosi peraltro con un software errato in future partite di prodotti conformi.

Non gestire i rischi
I progetti più rischiosi sono spesso quelli con il maggiore potenziale di guadagno. D’altra parte, il rischio è qualcosa che vorremmo sempre evitare. In un certo senso, tutto lo sforzo di impostare il processo di sviluppo in modo ripetibile è un tentativo di minimizzare i rischi.
Eppure, molte volte si ottiene la ripetibilità solo costruendo sistemi "troppo" simili; ovviamente, il progetto "rischioso" in questi casi è la costruzione di un meta-sistema [DeM95], o di un framework architetturale (vedere [Pes98] per un semplice esempio), che consenta poi di realizzare le varie istanze a tempo di record.
Altrettanto ovviamente, il progetto "rischioso" ha un potenziale di ritorno molto più alto, ma tenderà a non rispettare tutti i nostri bravi canoni di ripetibilità, e quindi sarà più difficile da pianificare, da tenere sotto controllo, ed ovviamente anche da realizzare.
Ciò non significa che si debba intraprendere il progetto a più alto potenziale ignorando i fattori di rischio: al contrario, ogni rischio significativo va identificato il prima possibile, e deve essere prevista una azione di recovery. Da quel che ho visto, la maggior parte degli errori nelle stime, ed anche la maggior parte dei progetti che non arrivano mai a conclusione, è dovuta proprio alla totale mancanza di gestione e prevenzione dei rischi, tipica di chi è abituato allo stile "speriamo che vada tutto bene".
Non posso negare che gestire e mitigare i rischi sia difficile; purtroppo l’ampia letteratura sul risk management è solitamente di poco aiuto, perché dedica uno spazio enorme alla classificazione dei rischi, alla fase di assessment, eccetera, e poi fornisce ben poche strategie concrete per affrontarli. Ricordo una guida del Software Engineering Institute, che dopo 80 pagine di tassonomia dei rischi si limitava a dire che, una volta identificati, questi dovevano essere risolti "attraverso una seduta di brainstorming"!
Una strategia che utilizzo ogni volta che le circostanze lo permettono è di identificare una soluzione il più possibile tecnologica. In fondo, la finalità ultima della tecnologia è di risolvere problemi non tecnologici (altrimenti sarebbe totalmente fine a se stessa). Identificare una soluzione sul lato tecnologico contribuisce anche a rimuovere l’enfasi dalla causa (che spesso è umana), evitando una serie di conseguenze deleterie sul lungo termine. E del resto, come sviluppatori spesso preferiamo lottare con il compilatore che con la burocrazia, la politica aziendale, e simili amenità.
Vediamo di nuovo un piccolo esempio concreto. Non molto tempo fa, ho preso parte al design di una applicazione con diversi "punti di contatto" con il mondo esterno. Le schede "note" da utilizzare erano tutte visibili come porte seriali, tramite driver opportuni. Una di queste schede (richiesta da uno dei principali clienti) ci era totalmente sconosciuta. Forse sarebbe stata visibile a sua volta come porta seriale, tramite apposito driver. Forse il driver avrebbe avuto una sua API. Forse ci avrebbero fornito un programma separato di gestione, da pilotare tramite pipe.
Tuttavia il progetto doveva partire il prima possibile (eravamo all’inizio dell’estate), ed arrivare a buon punto di completamento in autunno. Il cliente che "imponeva" la scheda non era in grado di fornire dettagli tecnici. Il produttore temporeggiava, facendo anche sospettare che i driver per il sistema operativo prescelto non fossero ancora pronti.
Le soluzioni erano molte, ed andavano dal negoziare un nuovo planning con il management, al tentativo di convincere il cliente ad abbandonare la scheda, alle ovvie pressioni sul produttore. Ovviamente potevamo anche ignorare il rischio e sperare che anche la nuova scheda fosse visibile come seriale. Ma la soluzione più semplice era evidentemente di sovra-ingegnerizzare (leggermente) il sistema, ignorando l’uniformità di interfaccia delle altre schede e costruendo un layer di astrazione ulteriore. Una soluzione totalmente tecnologica, semplice da gestire e tutto sommato potenzialmente utile in altri casi. Di fatto, il driver risultò disponibile solo mesi dopo, con una interfaccia proprietaria, ma questo fattore venne assorbito totalmente dal layer fisico che era stato impostato. Se avessimo seguito la strada semplice (sperare che anche la "scheda sconosciuta" fosse visibile come seriale), ignorando i rischi connessi, avremmo semplicemente mancato gli obiettivi.
Una nota di rigore: in alcuni ambienti, gestire i rischi alla luce del sole è praticamente impossibile, perché la cultura aziendale tende a sopprimere i progetti "rischiosi". Quindi il semplice fatto di cercare i problemi il prima possibile, e di renderli visibili per consentirne la gestione, è visto come un atteggiamento errato ("vogliamo soluzioni, non problemi") e mette a repentaglio sia il progetto che il tapino che si azzarda a tentare il risk management. In questi casi, la soluzione migliore che conosco (ma che alcuni trovano discutibile) è di gestire i rischi "sottocoperta" (nuovamente è più facile farlo attraverso soluzioni tecnologiche), evitando quindi di sollevarli apertamente. Chi conosce una strategia migliore (al di là del bucolico "tentare di raggiungere un punto di intesa") è seriamente invitato a farsi sentire...

Trasformare un ritardo in un fallimento
I programmatori si lamentano spesso dell’eccessiva complessità delle applicazioni che vengono loro richieste. A dire il vero, anche gli utenti se ne lamentano, ma questo sarebbe un aspetto complementare molto lungo da analizzare. Di fatto, quasi ogni progetto nasce con un elenco di requisiti già sostanzioso, e strada facendo ne incorpora altri ancora, sino a quando si arriva all’inevitabile conclusione: non si riuscirà a consegnare in tempo.
A questo punto si aprono molte possibilità. Possiamo proseguire con il piano originale, trasformando il progetto in una marcia forzata [You96]. Possiamo aumentare le risorse (con tutti i rischi connessi alla legge di Brooks [Bro95]). Possiamo "tagliare" alcune feature. Eccetera.
In pratica, molte volte avviene una combinazione di quanto sopra. Eppure, non di rado, anche dopo una riprogrammazione dei tempi di consegna e degli obiettivi del progetto, si arriva nuovamente in ritardo.
Come consulente, ho avuto spesso il non semplice compito di rimediare, per quanto possibile, a situazioni "difficili" come quella descritta sopra, che penso non risulterà nuova a molti di voi. In uno dei miei primi grandi progetti, ricordo di aver accettato di preservare una funzionalità molto complicata e marginalmente utile, solo perché il project manager (che l’aveva ideata) ne era follemente innamorato. Scegliere le feature da tagliare non è semplice, ma la cosa peggiore è farsi guidare dalle emozioni.
Una tecnica molto semplice (ma anche efficace) per tagliare nel modo migliore è di fornire sin dall’inizio una doppia valutazione per ogni feature. Un semplice flag (semplice/difficile) per classificarne la prevista complessità realizzativa, ed un altro flag (fondamentale/opzionale) per classificarne la rilevanza per l’utente finale. In alcuni casi, è utile prevedere una terza categoria, ovvero la rilevanza per il marketing.
Se si supera lo schedule, si inizi tagliando tutte le feature con la combinazione (difficile, opzionale). Se non abbiamo tagliato molto, o abbiamo sbagliato tutta la gestione del progetto (implementando sino a quel punto solo le cose facili e/o opzionali) oppure, in tutta onestà, occorre rivedere la valutazione. Se invece abbiamo tagliato parecchio, ma non abbastanza per rientrare nei piani, possiamo iniziare a dare delle priorità agli elementi rimanenti; non di rado, durante questa seconda fase spariscono alcune altre feature.
Talvolta questa tecnica (nota come triage [You96]) semplicemente non funziona. Non funziona perché gli sviluppatori mettono ovunque "difficile" o perché (più frequentemente) il project manager mette ovunque "fondamentale". Purtroppo in questi casi la soluzione esula dal campo strettamente tecnico, e l’opzione migliore è spesso la ricerca di un terzo che aiuti a riportare un giusto equilibrio nelle valutazioni.
Vi è un altro errore collegato, che ho osservato frequentemente, ma che lo spirito di conservazione mi ha sempre impedito di commettere: mancando una milestone (o una deadline), promettere una consegna troppo ravvicinata, mettendosi nelle condizioni di mancare anche quella. Le ragioni sono piuttosto naturali: avendo "fallito", vogliamo riscattarci con un successo ottenuto in tempi brevi. Purtroppo, da un punto di vista statistico, è più probabile che si ottenga un ulteriore fallimento in tempi brevi. Molto meglio, come suggerito anche da [McC95], fissare un obiettivo sicuramente raggiungibile, in modo da ricostruire il morale di ogni persona coinvolta, anziché affossarlo del tutto.

Confondere qualità con documentazione e testing
Negli ultimi anni la diffusione tra la aziende della certificazione ISO, e la conoscenza di modelli alternativi come il CMM (Capability Maturity Model) è andata via via crescendo. Di conseguenza sono nate molte iniziative di "Process Improvement" o più in generale di "Qualità Aziendale".
Molto spesso, queste iniziative si risolvono in una serie di documenti più o meno standard (e più o meno dettagliati) che vanno compilati nelle varie fasi di sviluppo. Altrettanto spesso, si migliora anche la fase di testing. In un numero significativo di casi, la "qualità" del prodotto risultante migliora in modo appena sensibile, solo a discapito del time-to-market, che invece aumenta in modo ancora più sensibile.
Prima che qualche fedele di Deming e Crosby si metta a tuonare che "la qualità è gratis", vorrei far notare tre cose. La prima è che, ammesso che sia gratis, lo è nell’intero ciclo di vita, ovvero considerando anche la manutenzione; il fatto che il time-to-market aumenti non è quindi una contraddizione (che poi sia gratis per l’azienda non vuol dire che sia gratis per le persone coinvolte, ma questo sarebbe un discorso lungo). La seconda è che gran parte dei modelli adottati per il software non tiene presente che il software è una attività intellettuale e non di produzione industriale; chi penserebbe a mettere il marchietto di qualità sui romanzi, sulle poesie, sui teoremi, sulle teorie di fisica delle particelle?. La terza è che il trend è (purtroppo) di migliorare la qualità del processo, sperando che questo si rifletta in una migliore qualità del prodotto.
Sto quindi affermando che lo sviluppo deve essere caotico, non documentato, "artistico" e così via? Chi ha seguito alcune puntate di Principles&Techniques può facilmente immaginare che questa non è la risposta. Ovviamente mi aspetto che lo sviluppo di ogni prodotto serio venga affrontato da un team preparato, che conosce bene l’importanza di analisi e design, che sa bene quanto sia utile documentare le decisioni prese e le loro motivazioni, e così via. Quello che invece mi sento di affermare in tutta onestà è che cercare di irreggimentare lo sviluppo, sperando di ottenere risultati migliori senza il contributo del talento individuale (ma anzi puntando spesso al deskilling) è spesso una strada perdente.
Naturalmente non sono il solo a pensarla in questi termini: esiste una intera scuola di pensiero, basata sul criterio di Best Practice, secondo la quale la qualità del prodotto si migliora intervenendo prima sulle persone che creano il prodotto, e poi sui problemi reali e contingenti del prodotto e del processo di sviluppo, anziché cercare una ripetibilità che serve più a far dormire sonni tranquilli al management che a migliorare i prodotti finali.
Vediamo ancora un esempio concreto. Tempo fa una azienda mia cliente decide di riprovare ad impostare un "sistema di qualità" (Ri-provare, perché il precedente tentativo era fallito). Il tentativo iniziale era basato sull’apporto della solita, grande società di consulenza che non vede l’ora di installare il super-CASE, il super-planner, il super-manualone pieno di form da compilare ogni volta che si preme un tasto. Il fallimento era dovuto a molte ragioni; ad esempio, gli analisti (tipicamente figure senior) non avevano alcuna intenzione di scrivere tonnellate di documentazione per adattarsi ai modelli forniti. Quindi scaricavano il compito sui progettisti, che le inquinavano con informazioni di design. Poi le passavano ai programmatori, sui quali ovviamente finivano per scaricare a loro volta la documentazione di design, visto che avevano perso tempo con quella di analisi. La documentazione di design finiva allora per essere una documentazione "del codice", vissuta come costosissima ridondanza da tutti. La conseguenza di questo ed altri problemi fu il rapido abbandono, dopo alcuni mesi di sofferenza, del costoso sistema di qualità impostato. La società di consulenza si rifiutava peraltro di tenere presente i paradigmi adottati dalle diverse divisioni, insistendo con un unico formato di documenti che mal si adattava, ad esempio, al paradigma ad oggetti.
Non pretendo che quanto sopra sia rappresentativo della media; diciamo che l’ho osservato con una certa frequenza, soprattutto in aziende dinamiche che non scrivono da trent’anni lo stesso tipo di software.
In ogni caso, decidendo di riprovare, l’azienda ha preferito seguire un approccio diverso, guidato dai problemi reali e contingenti. Per due mesi abbiamo chiesto ad ogni sviluppatore di tenere traccia, per ogni difetto rimosso, del "punto di iniezione" più probabile: analisi, design, codifica, debugging, manutenzione. Un overhead non nullo ma tutto sommato accettabile.
Alla fine abbiamo steso una piccola statistica, ed osservato che i bug era statati introdotti con frequenza maggiore (38%) in fase di codifica. Per i due mesi successivi, abbiamo chiesto di proseguire con la classificazione, aggiungendo per quelli di codifica una categorizzazione del bug, come "Memory Leak", "Return code ignorato", eccetera. Sapendo che il linguaggio di riferimento era il C++, credo non vi sorprenderà sapere che una percentuale rilevante (48%) era in realtà da ricondurre all’uso dei puntatori. Includendo sotto questo termine il più generale "gestione delle risorse" (di cui la memoria dinamica è solo una istanza) si raggiungeva il 59%. Quindi il 22% dei bug complessivi era da ricondurre alla gestione delle risorse.
L’intervento successivo è stato sia a livello di formazione che di sviluppo. Innanzitutto abbiamo impostato alcuni brevi seminari per allineare le conoscenze degli sviluppatori riguardo le numerose tecniche di gestione delle risorse in C++. Poi abbiamo impostato una famiglia di smart pointer per la gestione dei casi più rilevanti (possesso esclusivo, possesso condiviso, navigabilità senza possesso), ed una serie di template per gestire con relativa semplicità alcune situazioni molto comuni (risorse con funzioni di "open" e "close").
Due mesi dopo il numero di bug complessivi era sceso del 15%, ed i programmatori erano più produttivi di prima, perché l’azione intrapresa non serviva a rimuovere i bug a valle del processo di sviluppo, ma ad impedire che vi entrassero nel mezzo. La gestione delle risorse è divenuta parte delle "best practice" aziendali, e viene ora insegnata ad ogni nuovo programmatore.
L’attività di miglioramento dei prodotti è poi continuata, con risultati decisamente positivi, intervenendo anche su altri aspetti (usability testing, design for testability, design for extension, design review, mentoring dei neoassunti, eccetera), sempre guidati dalla statistica aggiornata. Curiosamente, non abbiamo ancora raggiunto il punto in cui la standardizzazione della documentazione (che ognuno è tenuto a fare, ma che non prevede un formato rigido, form standard da compilare, "momenti" prefissati, eccetera) sembra avere, in proiezione, un return-of-investment sufficiente.
Alcune note importanti. Innanzitutto, se l’obiettivo dell’azienda è di conquistare il "bollino" dell’ISO, attività mirate come quelle qui descritte servono a poco, perché sono tipicamente guidate dai problemi contingenti e non tendono alla ripetibilità extra-settoriale. Se nasce una nuova divisione di "prodotti per internet", questa potrà scegliere fin dall’inizio alcune delle Best Practice esistenti, ma dovrà nel tempo formarsi le proprie (la gestione delle risorse in Java, ad esempio, segue un modello totalmente differente). Inoltre, è indubbio che uno sforzo di miglioramento come quello qui descritto è molto più complesso da mettere in pratica rispetto all’impostazione-imposizione di un processo di sviluppo rigido e standard; non dovrebbe quindi sorprendere che sia le aziende produttrici che quelle di consulenza tendano ad optare per il secondo. Infine, anche il CMM prevede il miglioramento continuo spinto dai problemi contingenti. Tuttavia lo introduce solo a livello 5 (il massimo), richiedendo quindi (ad esempio) la ripetibilità come precondizione. Quanto questo sia sensato in un ambiente dove le rivoluzioni avvengono in pochi mesi è un giudizio che lascio ad ogni lettore.

Conclusioni
Anche in questa puntata, ho trattato solo un ristretto sottoinsieme dei molti argomenti possibili. Spero comunque di aver fornito qualche spunto di riflessione interessante. Gli interessati potranno trovare qualche argomentazione ulteriore in [Pes96], disponibile attraverso la mia home page.
Le strategie e le tattiche che ho descritto nascono principalmente dall’esperienza diretta, e non nascondo che possano esservi percorsi alternativi più efficaci. Se avete una vostra "storia di guerra" che dimostra l’importanza o la superiorità di un diverso approccio, mandatemi qualche riga via mail. Le terrò sicuramente presenti per una futura puntata di Principles&Techniques.

Reader's Map
Molti visitatori che hanno letto
questo articolo hanno letto anche:

Bibliografia
[Bro95] Frederick Brooks, "The Mythical Man-Month", Addison-Wesley, 1995.
[DeM95] Tom DeMarco, "Why Does Software Cost So Much?", Dorset House, 1995.
[LT93] Nancy Leveson, Clark S. Turner, "An Investigation of the Therac-25 Accidents", IEEE Computer Vol. 26, No. 7, July 1993.
[McC95] Jim McCarthy, "Dynamics of Software Development", Microsoft Press, 1995.
[Pes96] Carlo Pescio, "Introduzione della Tecnologia ad Oggetti in Azienda: Gestione e Prevenzione dei Rischi", Proceedings della Conferenza "Tecnologia ad Oggetti per l'Industria", dicembre 1996.
[Pes97] Carlo Pescio, "When Past Solutions Cause Future Problems", IEEE Software, September/October 1997.
[Pes98] Carlo Pescio, "Architettura di Sistemi Reattivi", Computer Programming No. 68, 69, 70.
[SC96] Douglas C. Schmidt, Charles D. Cranor: "Half-Sync/Half-Async: An Architectural Pattern for Efficient and Well-Structured Concurrent I/O", in Pattern Languages of Program Design Vol. 2, Addison-Wesley, 1996.
[You96] Edward Yourdon, "Death March", Prentice-Hall, 1996.