Non importa come lo si misuri, il software sta sempre più arrivando a dominare sistemi elettronici. Le applicazioni automotive da sole hanno attualmente molti milioni di righe di codice, come li avranno i dispositivi consumer. Eppure gran parte di questo software è stato sviluppato in un modo precario e indisciplinato. Gli utenti dei prodotti consumer sono rassegnati a doverli spegnere e riaccendere – riavviando il computer interno – in modo sistematico. Anche se ci sono alcuni segnali di miglioramento, le cose si stanno muovendo ancora troppo lentamente.
Ci sono molti strumenti disponibili, che spaziano dalla specifica dei requisiti iniziali e del system modelling ai test e al debug, e che possono aiutare a trasformare lo sviluppo software in una disciplina razionale di progettazione.
Questo articolo darà uno sguardo ai requisiti essenziali per dei progetti di successo e ai tool che assicurino la qualità del software. Ci sono quasi altrettante definizioni di buon codice quante le persone che commentano lo sviluppo del software, ma ciò che si sta cercando è un codice che sia privo di bug, facile e comprensibile, il che implica semplicità, e che sia facile da mantenere.
Per semplicità, gli esempi saranno riferiti a C e C++, non perché questi linguaggi siano migliori o peggiori di altri, ma perché sono quelli più ampiamente utilizzati. Gli argomenti potrebbero, nel complesso, fare riferimento a Java, Ada o anche altri linguaggi.
Alla base di questo approccio c’è il presupposto che innanzitutto è meglio non scrivere un codice che abbia dei bug. Anche con questo obbiettivo, si possono avere dei bug, quindi il passo successivo è quello di rimuoverli prima che il codice sia compilato. Infine c’è la necessità di valutare quanto bene si stia svolgendo il compito.
Un altro presupposto è che sia meglio risolvere i problemi di bug il prima possibile nel ciclo di sviluppo. È ampiamente evidente che i costi per fissare un bug crescono quasi esponenzialmente quanto più tardi vengono identificati nel ciclo di sviluppo (i costi di un bug trovato quando il prodotto è già sul campo possono, nei casi più estremi, essere causa del fallimento di un’azienda).
Standard di codifica
Un punto di partenza è quello di utilizzare standard di codifica. Non si è interessati alle “Style Guides” – set di regole che prevedono le convenzioni per i nomi e l’indentazione del codice – ma agli standard che aiutano a prevenire che il software faccia cose non desiderate. Questo è in particolare il caso del C e C++ dove la vastità degli standard e la flessibilità che offrono, portano con sé la possibilità che qualcosa che appare essere lecita possa avere conseguenze inaspettate o imprevedibili. Cosi come per ogni linguaggio, come per lo stesso inglese, una frase grammaticalmente lecita può essere ambigua o addirittura senza senso.
Esempi di comportamento indefinito in C includono: dereferenziare un puntatore nullo, divisioni per zero in una espressione, e una funzione che restituisca un collegamento a dati locali non statici. Una linea guida per la programmazione può essere tanto facile quanto una regola omnicomprensiva per evitare tutti i comportamenti indefiniti. Più utili sono le linee guida che richiedono che la proprietà del puntatore sia verificata prima che sia dereferenziato, o che il programma si accerti che il divisore non sia zero.
L’altra causa di bug sono semplici errori di codifica, oppure la formulazione di assunzioni che non sono poi valide riguardo il comportamento del codice oppure non prevedere tutte le conseguenze di una operazione. C e C++ offrono molte possibilità che ciò avvenga, ma dei buoni standard di codifica possono ridurre tutto ciò drasticamente.
I tool di analisi statica del codice che analizzano il software cercando errori di codifica sono utilizzati ormai da diversi anni come sistema per rimuovere i bug. Oggigiorno sono disponibili tool che contengono potenti motori per analizzare il comportamento del programma nel suo insieme. Possono identificare anche utilizzi pericolosi del linguaggio, codici eccessivamente complessi ed evidenziare problematiche che produrranno un codice poco migrabile o difficile da mantenere.
C’è ampia evidenza che i tool di analisi statica del codice sono nettamente migliori nello scovare bug di quanto non lo siano i compilatori. Uno studio condotto da PRQA ha mostrato come “…è dimostrato empiricamente che l’analisi statica è in grado di evidenziare una quantità di warning 25 volte superiore rispetto a tutti e quattro i compilatori [testati].”
http://www.programmingresearch.com/resources/white-papers/
Ambiguità dei compilatori
In effetti, gli stessi compilatori possono essere la causa di ciò che appare come un bug nel codice compilato, come le ambiguità nei linguaggi standard, oppure le opzioni per differenti implementazioni nello standard possono implicare che differenti compilatori possono compilare lo stesso codice ottenendo comportamenti differenti.
In C e C++ l’ordine di valutazione degli operatori tra i punti della sequenza non viene specificato, quindi possono essere valutati in qualsiasi ordine. Se la corretta operazione poggia su un particolare ordine di valutazione, allora si possono insinuare dei bug subdoli.
Per esempio:
x = f1() * f2() + f3();
Lo sviluppatore sta facendo affidamento sulle tre funzioni chiamate nell’ordine, f1, f2 e f3. Infatti l’ordine non è specificato – le funzioni possono essere chiamate in qualsiasi ordine. Si sa dalle regole di precedenza che la moltiplicazione verrà effettuata prima della somma – ma questa è effettuata su valori restituiti dalle funzioni.
Lo sviluppatore decide di correggere il codice:
x = (f1() * f2()) + f3();
Tuttavia, le parentesi non incidono sull’ordine di valutazione degli operatori, ma solo sulla precedenza. Il solo modo per raggiungere questo scopo è quello di chiamare le funzioni separatamente.
Un punto di sequenza è un posto in un programma in cui tutti gli effetti collaterali precedenti hanno avuto luogo. Un punto e virgola è un punto di sequenza e ce ne sono altri. Per esempio gli operatori logici AND e OR (| | e &&). Si consideri il seguente codice:
if ( (x == f1()) || (x == f2()) )
Diversamente dal precedente esempio si sa esattamente in che ordine le funzioni saranno chiamate: f1 sarà assolutamente chiamata per prima. Tuttavia, f2 potrebbe essere chiamata, oppure no. C e C++ hanno una particolare caratteristica chiamata ‘short circuit’ con gli operatori logici. Con un OR se la prima espressione è true, qualsiasi sia il secondo valore, il risultato generale è true – quindi lo standard C dice che non deve essere valutato. Perciò se il programma dipende dal fatto che entrambe f1 e f2 siano chiamate allora il programma potrebbe non funzionare correttamente – dipende dal valore di x e da quello restituito da f1.
Alcuni programmi possono essere scritti per trarre vantaggio da questo, per esempio:
if ( OpenDatabase() && WriteRecord(record) )
WriteRecord sarà chiamata solo se la funzione OpenDatabase ritorna true. Tuttavia si possono avere problemi facendo un semplice errore di battuta:
if ( OpenDatabase() & WriteRecord(record) )
In questo caso entrambe le funzioni saranno chiamate – ma non si sa in che ordine.
Gli strumenti di analisi del codice dovrebbero identificare queste aree di ambiguità, se gli standard di codifica non hanno ancora imposto che, per esempio, le funzioni debbano essere chiamate separatamente.
Nuove categorie di strumenti
Se aggiungete alle cap
acità di rilevamento dei bug di un analizzatore statico del codice anche la capacità di verificare la rispondenza agli standard di codifica, questo crea una nuova categoria di tool – quella dei Coding Standard Enforcement (CSE) tool. Lo stesso studio ha mostrato che un CSE potrebbe identificare molte più violazioni agli standard di codifica rispetto ai compilatori, in alcuni casi con un ordine di grandezza di due o tre volte.
L’analisi del codice è stato uno strumento per garantire un buon codice quasi fin dagli albori dell’informatica. Mentre l’analisi formale del codice, spesso obbligatoria per uno sviluppo safety-critical e con elevata affidabilità, può dimostrarsi un gran dispendio di tempo, persino un’analisi informale “run an eye over this” (“dagli solo un’occhiata”) può essere preziosa nel rilevare problemi con complicazioni più in profondità, come la struttura del programma. Lanciare un’analisi statica del codice e gli strumenti CSE prima della revisione del codice può far risparmiare tempo e permettere all’analisi di concentrarsi sugli obbiettivi generali, senza preoccuparsi delle problematiche minori.
Fig. 1 – QA•Verify, un nuovo strumento di PRQA, lavora con i tool code analysis/CSE e le principali versioni dei prodotti control/code repository per fornire l’accesso a dati di analisi storica, trend, statistiche, e valutazioni quantitative che riflettano la costante qualità del codice base
Finora ci si è occupati del codice nuovo, ma è raro che tutto il codice di un progetto sia nuovo; è sensato riutilizzare del codice legacy, codice di cui è stato verificato il funzionamento. In questo caso è quasi certo che il codice non sarà conforme agli standard di codifica e che facendolo esaminare da un tool CSE emergeranno molti possibili errori. Poiché “sistemarli” comporterebbe sia a un dispendio di tempo che la potenziale aggiunta di bug a un codice che sta funzionando adeguatamente, spesso è meglio lasciare le cose come stanno. Ma potrebbe essere necessario aggiungere codice, effettuare cambiamenti per manutenzione, o aggiungere funzionalità. Una possibilità è quella di confrontare le problematiche di programmazione prima e dopo i cambiamenti e le aggiunte, evidenziando solo le nuove problematiche. PRQA ha sviluppato degli strumenti di analisi che fanno esattamente questo.
Per una buona prassi di progettazione è importante conoscere i trend alla base. Il codice sta migliorando nel tempo? Ci sono particolari problematiche ricorrenti? Queste sono valutazioni quantitative difficili da raccogliere e analizzare, ma molte informazioni sono disponibili nei prodotti version control/code repository. Gli strumenti code repository e version control memorizzano diverse versioni del codice, permettendo cosi agli sviluppatori il roll back alle versioni precedenti, se necessario, oppure di utilizzare una versione precedente del codice come punto di partenza per una differente implementazione. Normalmente uno sviluppatore rileverà il codice dallo store, ci lavorerà e in seguito lo riconsegnerà. È buona prassi subordinare la riaccettazione del codice al superamento di una analisi statica del codice stesso.
QA•Verify, un nuovo strumento di PRQA, lavora con i tool code analysis/CSE e le principali versioni dei prodotti control/code repository per fornire l’accesso a dati di analisi storica, trend, statistiche, e valutazioni quantitative che riflettano la costante qualità del codice base.
Le tecniche e i tool qui analizzati da sole non garantiscono progetti di successo. Tuttavia, specialmente quando sono messe in campo come parte di un ben definito processo di sviluppo, con tool appropriati, a partire dalle specifiche iniziali fino ai test finali e persino al life cycle management del prodotto, possono offrire la sicurezza che il codice sia di elevata qualità.