Politecnico di Milano
Anno Accademico 2010/2011
Ingegneria del Software 2
Corso della Prof.ssa Elisabetta Di Nitto
Stefano Invernizzi
Facoltà di Ingegneria dell’Informazione
Corso di Laurea Magistrale in Ingegneria Informatica
Ingegneria del Software 2
Indice
Indice
Capitolo 1: Introduzione ____________________________________________________________________________ 5
L’ingegneria del software: definizioni __________________________________________________________________________ 5
L’ingegneria del software e le altre discipline ingegneristiche _______________________________________________________ 5
Ingegneria del software e programmazione _____________________________________________________________________ 7
Cenni alla storia dell’ingegneria del software ____________________________________________________________________ 7
Aspetti fondamentali dell’ingegneria del software ________________________________________________________________ 8
Capitolo 2: Prodotto e processo SW __________________________________________________________________ 9
Il processo ed il prodotto ____________________________________________________________________________________ 9
Le qualità del software ______________________________________________________________________________________ 9
Le qualità del processo _____________________________________________________________________________________ 11
Capitolo 3: Cicli di vita del software _________________________________________________________________ 12
Cicli di vita del software ____________________________________________________________________________________ 12
Il modello a cascata _______________________________________________________________________________________ 12
Prototipazione ___________________________________________________________________________________________ 15
Rilascio incrementale (incremental delivery) ____________________________________________________________________ 16
Il modello a spirale ________________________________________________________________________________________ 16
Sviluppo basato sui componenti (component-based development) __________________________________________________ 17
Rapid Application Development (RAD) ________________________________________________________________________ 18
Extreme Programming (XP) _________________________________________________________________________________ 19
Rational Unified Process (RUP)_______________________________________________________________________________ 21
Sincronizzazione e stabilizzazione (Sync-and-stabilize) ____________________________________________________________ 22
Lo sviluppo open source ____________________________________________________________________________________ 24
Capitolo 4: Il Capacity Maturity Model _______________________________________________________________ 25
Introduzione al Capacity Maturity Model (CMM) ________________________________________________________________ 25
Valutazione della maturità di un’organizzazione ________________________________________________________________ 26
Standard ISO 9000 ________________________________________________________________________________________ 30
Capitolo 5: Ingegneria dei requisiti __________________________________________________________________ 31
L’ingegneria dei requisiti ___________________________________________________________________________________ 31
I System Context Diagrams _________________________________________________________________________________ 34
Gli obiettivi (goal) e le asserzioni _____________________________________________________________________________ 35
Il documento SRS _________________________________________________________________________________________ 36
Osservazioni _____________________________________________________________________________________________ 40
Come derivare i requisiti dagli obiettivi? _______________________________________________________________________ 40
La gestione del processo____________________________________________________________________________________ 42
3
Indice
Ingegneria del Software 2
Capitolo 6: UML _________________________________________________________________________________ 44
Il concetto di modello ______________________________________________________________________________________ 44
I modelli dei sistemi software ________________________________________________________________________________ 45
UML: concetti generali _____________________________________________________________________________________ 45
Come utilizzare UML? ______________________________________________________________________________________ 46
I class diagram ___________________________________________________________________________________________ 46
Object diagram ___________________________________________________________________________________________ 52
Use case diagram _________________________________________________________________________________________ 52
Sequence diagram ________________________________________________________________________________________ 58
Activity diagram __________________________________________________________________________________________ 60
Component diagram _______________________________________________________________________________________ 64
Statechart _______________________________________________________________________________________________ 66
Utilizzare l’UML per modellare i requisiti _______________________________________________________________________ 69
Capitolo 7: Specifica ______________________________________________________________________________ 71
Il concetto di specifica _____________________________________________________________________________________ 71
Introduzione ad Alloy ______________________________________________________________________________________ 72
La sintassi di Alloy _________________________________________________________________________________________ 75
Un esempio di utilizzo di Alloy _______________________________________________________________________________ 81
Osservazioni conclusive su Alloy______________________________________________________________________________ 82
Capitolo 8: Software design ________________________________________________________________________ 83
Software design & software architecture ______________________________________________________________________ 83
Il processo di design _______________________________________________________________________________________ 85
Gli stili architetturali _______________________________________________________________________________________ 86
I design pattern ___________________________________________________________________________________________ 96
I pattern architetturali ____________________________________________________________________________________ 101
Capitolo 9: Verifica e validazione __________________________________________________________________ 102
Introduzione e terminologia ________________________________________________________________________________ 102
Tempi e modalità della fase di verifica e validazione ____________________________________________________________ 103
Il processo di verifica e validazione __________________________________________________________________________ 104
Approcci alla verifica e alla validazione _______________________________________________________________________ 105
Il testing: introduzione ____________________________________________________________________________________ 108
Confronto tra testing strutturale e testing funzionale ___________________________________________________________ 110
Osservazioni varie sulla fase di test __________________________________________________________________________ 115
4
Ingegneria del Software 2
Capitolo 1: Introduzione
Capitolo 1: Introduzione
L’ingegneria del software: definizioni
Possiamo dare diverse definizioni del termine Ingegneria del Software:
Definizione 1
L’ingegneria del software è il settore dell’informatica che si occupa della creazione di sistemi software talmente grandi
o complessi da dover essere realizzati da una o più squadre di ingegneri. Tali progetti hanno una vita lunga e vengono
rilasciati in versioni diverse tra loro, e sono inoltre soggetti a modifiche che hanno lo scopo di correggere errori in essi
presenti o di migliorare o estendere le funzionalità del software stesso.
Definizione 2
L’ingegneria del software è l’approccio sistematico alle diverse fasi di vita del software, che includono lo sviluppo, la
fase di operation (messa in esecuzione del software), la manutenzione, il deployment (installazione del software), la
fase di training di coloro che utilizzeranno il software ed il ritiro del software stesso (tale fase richiede la messa in atto
di operazioni che consentano di lasciare in uno stato consistente l’ambiente in cui il software operava).
Definizione 3
L’ingegneria del software è una disciplina metodologica e manageriale che ha a che fare con la produzione sistematica
e la manutenzione di prodotti software, in modo che siano sviluppati e mantenuti in modo controllato e anticipato.
Definizione 4
L’ingegneria del software è una disciplina che ricerca soluzioni ai problemi che siano efficaci dal punto di vista dei
costi, applicando la conoscenza scientifica alla realizzazione di prodotti software al servizio dell’uomo.
Questa definizione mette in evidenza una serie di concetti base legati in generale all’ingegneria: l’intenzione di
risolvere dei problemi pratici, la ricerca di soluzioni vantaggiose dal punto di vista economico, l’uso della conoscenza
scientifica, l’obiettivo di “costruire qualcosa”, l’intento di realizzare qualcosa che sia utile per l’uomo.
L’ingegneria del software e le altre discipline ingegneristiche
Compiti ingegneristici
I compiti di un ingegnere possono essere suddivisi in due categorie:

Compiti di routine (programmazione di routine)
I compiti di routine includono la risoluzione di problemi famigliari, che può avvenire utilizzando delle soluzioni
precedentemente realizzate e adottate. La programmazione di routine ha perciò a che fare con problemi noti e di
conseguenza ha tempi e costi facilmente prevedibili.

Compiti innovativi (design innovativo)
I compiti innovativi di un ingegnere riguardano invece lo sviluppo di soluzioni nuove in risposta a problemi che
non sono famigliari o noti.
Nell’ingegneria del software molte attività riguardano la categoria del design innovativo: nell’ingegneria del software
infatti non viene catturata e gestita l’organizzazione di ciò che è già noto.
Tutte le discipline ingegneristiche mature prevedono che le conoscenze di design vengano conservate, organizzate e
condivise. L’ingegneria del software si pone l’obiettivo di fare questo anche nel campo software, che è un’area
ingegneristica ancora non completamente matura.
5
Capitolo 1: Introduzione
Ingegneria del Software 2
Evoluzione delle pratiche dell’ingegneria del software
La figura seguente ritrae in maniera schematica l’evoluzione prevista nell’ingegneria del software, a partire da
soluzioni ad-hoc (di design innovativo), fino ad arrivare alle pratiche ottimizzate, diventate ormai di routine:
Soluzioni ad-hoc
Nuovi problemi
Euristiche
Modelli e teorie
Codifica di procedure
sistematiche
Miglioramento delle
pratiche di routine
Figura 1: evoluzione delle pratiche dell’ingegneria del software
Differenze rispetto alle discipline ingegneristiche tradizionali
La precedente figura mette in evidenza la presenza di cicli, assenti invece nell’evoluzione delle pratiche adottate dalle
altre discipline ingegneristiche (come ad esempio l’ingegneria civile). Tale caratteristica è dovuta al fatto che il
contesto nel quale opera l’Ingegneria del software è sempre diverso.
Si osserva inoltre che l’ingegneria del software viene ancora oggi praticata ed insegnata in maniera non sistematica e
che risulta ancora meno stabile ed organizzata rispetto alle discipline ingegneristiche tradizionali: al momento, non
esistono degli standard e delle specifiche per il design del software.
Possiamo perciò affermare che, se da un lato è vero che l’ingegneria del software è considerata a tutti gli effetti una
disciplina ingegneristica ed ha molti punti in comune con le altre discipline di tale categoria, è vero anche che ci sono
molti punti di divergenza, che possiamo mettere in evidenza con un banale esempio: mentre i ponti vengono
normalmente costruiti in tempo, entro i range di budget e “perfettamente funzionanti” (cioè non cadono), spesso il
software viene costruito fuori dai tempi e dai budget prefissati, e può presentare difetti più o meno gravi al momento
del rilascio. Ciò è dovuto proprio alle differenze che andiamo ora ad elencare:
1.
2.
6
Le ingegnerie tradizionali prevedono una fase di design estremamente dettagliata, nella quale si valutano diverse
alternative mediante l’uso di modelli; dopo la scelta di un certo design, tale scelta viene congelata e la flessibilità
nel cambiamento di specifiche è pressoché nulla; viceversa, nell’ingegneria del software si ha spesso a che fare
con applicazioni che riguardano i processi di business, i quali richiedono continue evoluzioni, perciò non è
possibile congelare le specifiche iniziali.
Discipline come l’ingegneria civile hanno ormai circa 3 millenni di vita, mentre l’ingegneria del software è una
disciplina molto recente. Di conseguenza, le altre discipline possono contare su una larga base di conoscenze,
sulla base delle quali sono state costruite nel tempo delle teorie e delle metodologie, grazie anche all’analisi dei
fallimenti che storicamente si sono verificati; l’ingegneria del software non può invece contare su tutto questo.
Ingegneria del Software 2
Capitolo 1: Introduzione
Ingegneria del software e programmazione
Le competenze che un ingegnere del software deve possedere sono ben diverse da quelle tipiche di un buon
programmatore: un programmatore sviluppa in maniera completa un programma, lavorando in maniera individuale su
un certo insieme di specifiche note. Un ingegnere del software deve invece essere in grado di identificare i requisiti e,
a partire da questi, sviluppare le specifiche, molto spesso lavorando all’interno di un team. In tal modo riesce a
progettare un componente che funzionerà solo se combinato con altri componenti. Inoltre, lo sviluppo e la
manutenzione del componente vengono affidati ad altre persone, e il componente potrà essere utilizzato all’interno di
più sistemi diversi tra loro. Possiamo perciò fare le seguenti affermazioni:
1.
2.
3.
Siccome il software deve interagire con l’ambiente esterno, l’ingegnere del software deve essere in grado di
capire e analizzare tale ambiente. Dall’ambiente esterno verranno tratti i requisiti del software che verrà
realizzato.
Dai requisiti ottenuti, occorre essere in grado di derivare una serie di specifiche dell’applicazione.
La conoscenza del dominio riveste un ruolo fondamentale nello sviluppo: se un ingegnere del software svolge la
propria attività di design basandosi su assunzioni sbagliate riguardanti il dominio applicativo, potrebbero sorgere
dei veri e propri disastri.
Da questo consegue che le competenze dell’ingegnere del software sono le seguenti:
1.
2.
3.
Competenze tecniche
Competenze di gestione del progetto
Competenze cognitive
4.
5.
6.
Capacità di organizzazione dell’impresa
Capacità di interazione con altre culture
Conoscenze di dominio
Cenni alla storia dell’ingegneria del software
L’arte della programmazione
Inizialmente il software veniva considerato come un’arte (“l’arte della programmazione”): gli sviluppatori realizzavano
in maniera individuale delle applicazioni, solitamente di dimensioni piuttosto ridotte, utilizzando linguaggi di basso
livello e dovendo rispettare vincoli piuttosto stringenti di memoria e di prestazioni dei calcolatori. Tali applicazioni
avevano per utenti gli stessi sviluppatori che le realizzavano, venivano usate soprattutto per la risoluzione di problemi
matematici ed avevano un tempo di vita piuttosto breve: l’esempio tipico di programmi di questo tipo è rappresentato
dall’applicazione che un ricercatore poteva sviluppare ad-hoc per la risoluzione di un certo problema; nel momento in
cui si fosse presentato un nuovo problema, il ricercatore avrebbe provveduto a scrivere una nuova applicazione,
buttando via quella precedentemente realizzata.
La programmazione come “artigianato”
Quando l’informatica ha trovato applicazione anche nella gestione delle informazioni, soprattutto con la nascita dei
sistemi informativi aziendali, le esigenze del nuovo software sono cambiate fortemente: da questo momento in poi, il
software ha avuto utenti ben distinti dagli sviluppatori, e sono nate delle vere e proprie software house.
Parallelamente, sono stati compiuti importanti passi in avanti da un punto di vista tecnologico, con la nascita di
linguaggi ad alto livello e il miglioramento delle caratteristiche fisiche dei calcolatori.
Tuttavia, i primi progetti software di grandi dimensioni si rivelarono dei veri e propri insuccessi, a causa di problemi
nella gestione del denaro e del tempo, errori nelle interazioni umane e gravi difetti nelle specifiche del software
stesso. In alcune situazioni estreme, tali problemi portarono anche alla morte di diverse persone, come accadde ad
esempio nel progetto Therac-25: un’azienda sanitaria aveva richiesto la realizzazione di un software per la gestione di
una macchina di radioterapia, ma un fraintendimento tra coloro che realizzarono la macchina hardware e coloro che si
occuparono di sviluppare il software portò alla morte di almeno 4 persone, vittime di una forte sovraesposizione alle
radiazioni che avrebbero dovuto curarle dal cancro. Nello specifico, l’incomprensione era dovuta al fatto che nel
progettare il software si ipotizzò che tutta una serie di controlli fossero svolti direttamente dall’hardware, che invece li
aveva demandati all’applicazione.
7
Capitolo 1: Introduzione
Ingegneria del Software 2
L’ingegnerizzazione del processo di sviluppo del software
Tali fallimenti resero evidente la necessità di ingegnerizzare il processo di creazione del software, tramite:
1.
2.
3.
4.
5.
La realizzazione di metodi e standard per lo sviluppo del software
La messa in atto di attività di pianificazione e di management
L’automazione del processo
L’introduzione di qualità oggettive e verificabili
La divisione del software in componenti
Si è passati ad una vera e propria industria. Il termine software engineering fu ufficialmente coniato nell’ottobre 1968.
È da sottolineare che l’ingegneria del software si occupa solamente delle sviluppo di sistemi di grandi dimensioni e con
caratteristiche critiche (cioè nei quali un malfunzionamento genera dei rischi per la vita umana o delle perdite,
soprattutto di carattere economico). L’utilizzo dell’approccio ingegneristico dovrebbe quindi migliorare la qualità dello
sviluppo di questo tipo di applicazioni.
Ad oggi il settore del software ha assunto una grande importanza: nel 1996 il software diventò il terzo settore
industriale dopo quello automobilistico e quello dell’elettronica; nonostante ciò, occorre ricordare che dopo i primi
anni 2000 ci fu un certo ridimensionamento del settore.
Uno dei compiti fondamentali dell’ingegneria del software è quello di comprendere e di gestire i cicli di vita del
software (software lifecycle), ovvero il flusso di fasi che porta dal problema al prodotto finale, comprendendo anche
tutte le attività che ne consentono l’evoluzione, fino al momento del ritiro. È bene però sottolineare che non tutti i
progetti software arrivano ad essere completati: circa il 30% dei progetti vengono annullati prima di essere portati a
termine. Inoltre, più del 53% dei progetti software ha costi che sfiorano il doppio rispetto alle stime iniziali.
L’ingegneria del software cerca quindi di limitare questi fallimenti e questi inconvenienti nello sviluppo del prodotto
software stesso.
Aspetti fondamentali dell’ingegneria del software
Gli aspetti fondamentali dell’ingegneria del software sono:
1.
2.
3.
4.
5.
6.
7.
8
Rigore e formalità
Separazione degli ambiti di interesse
Modularità
Astrazione
Anticipazione dei cambiamenti
Generalità
Incrementalità
Ingegneria del Software 2
Capitolo 2: Prodotto e processo SW
Capitolo 2: Prodotto e processo SW
Il processo ed il prodotto
Il concetto di prodotto ed il concetto di processo
L’obiettivo di un ingegnere del software è quello di sviluppare dei prodotti software, cioè delle applicazioni (il
prodotto è perciò, come nel linguaggio quotidiano, il “risultato finale”). La modalità attraverso la quale otteniamo tale
prodotto prende il nome di processo. L’importanza del processo è fondamentale, così come quella del prodotto;
possiamo inoltre affermare che sia il processo che il prodotto hanno una serie di qualità e, come si può facilmente
comprendere:
1.
2.
Le qualità del prodotto dipendono strettamente da quelle del processo: se il processo è confusionario o casuale,
molto probabilmente il prodotto ottenuto sarà di bassa qualità.
Le qualità interne del software (ovvero quelle che riguardano la sua struttura) influenzano fortemente le qualità
esterne (quelle legate al suo funzionamento).
Differenze tra i prodotti tradizionali ed il software
Il prodotto software, come possiamo facilmente comprendere, presenta forti differenze rispetto alle tipologie
tradizionali di prodotti:
1.
2.
3.
Si tratta di un prodotto intangibile, perciò difficile da valutare e da descrivere;
È malleabile, ovvero si evolve continuamente nel tempo;
Lo sviluppo del prodotto software è human intensive, ovvero richiede un’attività creativa dell’uomo durante la
sua realizzazione. Gli altri prodotti prevedono invece un’attività creativa solo in un momento iniziale, mentre la
creazione finale del prodotto avviene solitamente in maniera automatizzata.
Le qualità del software
Le qualità principali del software
Le principali qualità del software sono:
1.
2.
3.
4.
5.
Correttezza
Affidabilità
Robustezza
Prestazioni
Scalabilità
6.
7.
8.
9.
10.
Usabilità
Manutenibilità
Riusabilità
Portabilità
Interoperabilità
Vediamo ora di analizzarle una dopo l’altra.
Correttezza (correctness)
Il software deve essere corretto, ovvero deve funzionare secondo le aspettative. Più formalmente, il software è
corretto se soddisfa le specifiche.
Se le specifiche sono indicate in maniera formale, la correttezza può essere definita in maniera formale e può essere
provata come un teorema (utilizzando strumenti noti come theorem-prover) oppure, al contrario, si può dimostrare
che il software non è corretto mediante l’individuazione di controesempi. Questo è possibile perché i programmi sono
degli oggetti formali: il loro significato è definito in modo formale e rigoroso, il compilatore trasforma quindi in
maniera univoca il programma sorgente, e non si ha alcuna ambiguità nella definizione del programma stesso.
In ogni caso, l’approccio migliore è quello che prevede di cercare di realizzare un software corretto a priori, attraverso
l’adozione di un processo adeguato e l’uso di strumenti adeguati (es.: linguaggi ad alto livello). Per fare ciò è però
necessario ancora una volta che le specifiche vengano definite in maniera formale.
9
Capitolo 2: Prodotto e processo SW
Ingegneria del Software 2
La correttezza presenta però alcuni importanti limiti:
1.
2.
Tutto ciò che è stato finora detto vale a patto che le specifiche siano esatte e complete: è infatti possibile che le
specifiche non catturino tutti i requisiti dell’utente, o che siano fondate su requisiti errati, e perciò un software
corretto (cioè che soddisfa tutte le specifche) potrebbe anche non soddisfare l’utente.
Inoltre, la correttezza è una qualità “assoluta”: non esiste un grado di correttezza, ma è possibile solamente dire
se un software è corretto oppure no.
Affidabilità (reliablity)
In maniera informale, un software è affidabile quando l’utente si può fidare del software stesso, ovvero quest’ultimo
non creerà danni all’utente che lo usa.
Da un punto di vista matematico, l’affidabilità può essere definita come la probabilità di assenza di comportamenti
errati in un certo periodo di tempo.
Se le specifiche sono corrette (cioè rispecchiano tutti i requisiti attesi dagli utenti), tutto il software corretto è anche
affidabile, ma non vale il viceversa: un software affidabile potrebbe anche non essere corretto). Si ricordi però che le
specifiche possono anche essere sbagliate. Proprio in virtù del fatto che l’affidabilità è una richiesta “più debole”
rispetto alla correttezza, spesso si dà una maggiore priorità all’affidabilità del software rispetto alla sua correttezza.
Robustezza (robustness)
Il software si dice robusto se il suo comportamento è ragionevole anche in presenza di circostanze impreviste. Questo
significa che il software deve comportarsi in maniera ragionevole anche di fronte a guasti dell’hardware, input
scorretti, o situazioni analoghe.
Prestazioni (performance)
Il software ha delle buone prestazioni se usa in maniera efficiente le risorse, come ad esempio la memoria, il
processore e le interfacce di comunicazione.
Le prestazioni possono essere valutate in maniera oggettiva, attraverso strumenti diversi, come l’analisi di
complessità, e la valutazione delle performance per mezzo di modelli o simulazioni. Le performance possono
influenzare l’usabilità e la scalabilità e possono dipendere da aspetti tecnologici.
Scalabilità (scalability)
La scalabilità del software è la sua capacità di soddisfare la crescita di richieste degradando le prestazioni in maniera
ragionevole, senza incorrere in situazioni instabili o di crash. Generalmente, si considera ragionevole il degrado di
prestazioni quando esso avviene in maniera lineare.
Naturalmente, scalabilità e performance sono correlate.
Usabilita (usability)
Un software viene definito usabile quando gli utenti che ci si aspetta dovranno utilizzarlo considerano semplice
l’utilizzo del software stesso. I sistemi usabili sono detti talvolta anche ergonomici o user-friendly.
Per valutare l’usabilità del software è quindi fondamentale definire in maniera accurata quelli che saranno gli utenti
che ci si aspetta che useranno l’applicazione.
Nonostante questo, la valutazione dell’usabilità rimane piuttosto soggettiva e difficile da valutare. Una modalità tipica
mediante la quale si può valutare l’usabilità consiste nel far provare il sistema o il prototipo del sistema stesso ad un
certo insieme di utenti appartenenti al gruppo degli expected-users.
L’usabilità dipende principalmente dall’interfaccia con l’utente (testuale o grafica).
10
Ingegneria del Software 2
Capitolo 2: Prodotto e processo SW
Manutenibilità (mainainability)
La manutenibilità è la capacità del software di poter essere trasformato, mediante la correzione di errori, lo sviluppo
di evoluzioni del software stesso, e la realizzazione di cambiamenti in generale.
Affinché il software abbia la qualità di manutenibilità, è necessario che sia dotato di un’architettura adatta e che il
codice risulti facilmente comprensibile.
Riusabilità (reusability)
La riusabilità del software consiste nella possibilità di riutilizzare delle porzioni del software in contesti diversi. Per
certi versi è quindi simile alla manutenibilità, ma in questo caso si fa riferimento ai componenti: un software
mantenibile è generalmente anche di facile riuso.
Portabilità (portability)
La portabilità del software è la capacità del software stesso di adattarsi a diversi ambienti operativi. Per fare ciò è
talvolta necessario modificare il software stesso, e per tale ragione si tratta di un’altra qualità piuttosto simile alla
manutenibilità.
Interoperabilità (interoperability)
L’interoperabilità è la capacità di coesistenza e di cooperazione del progetto software con le altre applicazioni.
Le qualità del processo
Le qualità principali del processo
Le principali qualità del processo sono:
1.
2.
Produttività
Tempestività
Analizziamole ora nel dettaglio.
Produttività (productivity)
La produttività del processo è generalmente calcolata come il rapporto tra le unità di prodotti ottenute e lo sforzo
affrontato per ottenerle. Nel caso dell’ingegneria del software, le unità di sforzo sono generalmente considerate come
i mesi-persona necessari per la realizzazione del prodotto, mentre le unità di prodotto ottenute possono essere
valutate come le linee di codice scritte, oppure le funzionalità sviluppate.
Tempestività (timeliness)
La tempestività è l’abilità del processo di rispondere ai cambiamenti delle richieste in una maniera puntuale e rapida.
Tale caratteristica è importante perché le esigenze del cliente crescono in continuazione, e perciò il processo deve
essere in grado di adattarsi ad esse, tendendo a raggiungere le richieste stesse.
Figura 2: crescita delle richieste degli utenti e delle funzionalità implementate dal sistema
11
Capitolo 3: Cicli di vita del software
Ingegneria del Software 2
Capitolo 3: Cicli di vita del software
Cicli di vita del software
Modelli di cicli di vita del software
L'espressione ciclo di vita del software si riferisce al modo in cui una metodologia di sviluppo o un modello di processo
scompongono l'attività di realizzazione di prodotti software in sottoattività fra loro coordinate, il cui risultato finale è il
prodotto stesso e tutta la documentazione a esso associata. Nel corso degli anni sono stati sviluppati diversi modelli di
ciclo di vita del software.
Il modello code&fix
I primi sviluppatori solitamente non adottavano alcun modello di riferimento, ma procedevano semplicemente
secondo l’approccio code&fix, che prevede semplicemente di codificare in maniera più o meno istintiva, provvedendo
poi a correggere il codice in caso di problemi.
Naturalmente però quando si ha un committente diverso dall’utilizzatore l’approccio code&fix non può essere
considerato sufficiente.
Il modello a cascata
Obiettivi del modello a cascata
Il modello tradizionale di ciclo di vita del software è il waterfall model o modello a cascata, nel quale vengono
identificate tutte le fasi e le attività del processo di sviluppo del software e nel quale si impone un avanzamento
lineare da una fase a quella successiva. Il modello non prevede quindi alcun tipo di ciclo: il ritorno ad una fase
precedente è considerato dannoso, e si crede invece che sia opportuno pianificare e controllare tutto sin dall’inizio.
Un’altra importante caratteristica di questo modello è la standardizzazione di tutti i prodotti risultanti da ciascuna
delle singole fasi del processo. Si può perciò affermare che l’obiettivo principale di questo modello era quello di
considerare il software come una qualsiasi altra attività di produzione industriale.
Schema del modello a cascata
Le versioni esistenti del modello a cascata sono in realtà diverse. Di seguito è riportato lo schema relativo ad una di
queste:
Studio di
fattibilità
Analisi dei requisiti
e specifica
Design
Codifica e
unit test
Integrazione e
test di sistema
Fasi basse
Fasi alte
Figura 3: ciclo di vita del software secondo il modello a cascata
Analizziamo singolarmente le singole fasi di tale ciclo di vita del software.
12
Deployment
Manutenzione
Ingegneria del Software 2
Capitolo 3: Cicli di vita del software
Studio di fattibilità (feasibility study)
Lo studio di fattibilità comprende le seguenti attività:
1.
2.
Analisi del rapporto costo / benefici.
Determinare se è il caso di avviare il progetto oppure no (considerando ad esempio la possibilità di acquistare il
prodotto finale già realizzato da altri), valutando le diverse alternative possibili e individuando le risorse
necessarie.
Al termine della fase viene prodotto il documento dello studio di fattibilità, che contiene una descrizione dei problemi
preliminari, un insieme di scenari che descrivano le possibili soluzioni, i costi e la pianificazione nelle diverse
alternative individuate.
Analisi dei requisiti e specifica
Durante la fase di analisi dei requisiti e specifica si analizza il dominio nel quale l’applicazione dovrà operare,
identificando così tutti i requisiti che lo caratterizzano. Da tali requisiti si derivano poi la specifiche del software,
mediante l’interazione con l’utente. Ciò richiede naturalmente una piena comprensione delle proprietà del dominio in
analisi.
Il documento che viene prodotto al termine della fase è noto come RASD (Requirements Analysis and Specification
Document), nel quale è necessario specificare chi utilizzerà il sistema (who), il motivo per il quale il sistema viene
sviluppato e per il quale l’utente dovrebbe usarlo (why), cosa il sistema fornirà (what), dove il sistema verrà usato
(where) e quando e per quanto tempo potrà essere usato (when).
È necessario che il RASD risulti preciso, completo e consistente. Tale documento potrebbe includere anche un
manuale preliminare per l’utente e una pianificazione dei test per il sistema.
Design
Nella fase di design viene definita l’architettura del software, individuando quali sono i componenti (moduli) che lo
costituiscono, quali sono le relazioni tra i componenti e quali interazioni avvengono tra essi.
L’obiettivo è quello di consentire uno sviluppo concorrente, separando le responsabilità: in questo modo i singoli
componenti verranno realizzati indipendentemente l’uno dagli altri.
Il documento prodotto al termine della fase è il documento di design.
Codifica e unit test
In questa fase ogni singolo modulo viene implementato utilizzando il linguaggio di programmazione scelto. Ogni
modulo viene testato isolatamente da colui che ha sviluppato il modulo stesso (si parla per questo di unit test, ovvero
test dell’unità). Ogni modulo includerà inoltre la propria documentazione.
Integrazione e test di sistema
I moduli vengono integrati in un sistema (o in sottosistemi), e si procede con l’attività di test del sistema (o dei
sottosistemi). Questa fase e la precedente possono essere integrate in uno schema di implementazione incrementale.
Il test del sistema generale, necessario per verificare le proprietà di insieme, viene talvolta diviso in due fasi: alfa test e
beta test.
Deployment
La fase di deployment ha l’obiettivo di distribuire l’applicazione e gestire le diverse installazioni e configurazioni dal
lato client.
13
Capitolo 3: Cicli di vita del software
Ingegneria del Software 2
Manutenzione
Tutti i cambiamenti che seguono la messa in funzione del sistema appartengono alla fase di manutenzione. Il termine
è in realtà piuttosto improprio: in realtà infatti il software non si usura, e se si verificano dei malfunzionamenti, i
problemi che ne sono alla base erano esistenti sin dall’inizio.
Questa fase è particolarmente critica, in quanto si stima che oltre il 50% dei costi totali per lo sviluppo di un software
riguardino la manutenzione (secondo alcuni studi tale cifra raggiunge addirittura l’80%). I tipi di manutenzione sono:

Manutenzione correttiva
Consiste nel riparare i difetti che vengono individuati all’interno del software. È circa il 20% del totale e i suoi
costi sono a carico di chi sviluppa il software.
Si noti che in generale gli errori che riguardano le prime fasi del processo di sviluppo vengono individuati molto
tardi, e che il costo di rimozione degli errori aumenta man mano che passa il tempo (eliminare errori da sistemi
maturi ha costi molto superiori rispetto all’eliminazione di errori da sistemi nuovi).
Inoltre, molto spesso la rimozione di errori comporta l’introduzione di nuovi errori, perciò i sistemi di grandi
dimensioni tendono a stabilizzare il loro numero di errori attorno ad un certo numero che rimane più o meno
costante, dopo un certo periodo di tempo dal loro rilascio. Si stima che il software che viene rilasciato contenga
mediamente circa il 10% degli errori che sono stati individuati durante la fase di testing. La fase di testing infatti
copre tipicamente solo il 50% circa del codice.

Manutenzione evolutiva
La manutenzione adattativa, perfettiva e preventiva possono essere considerate insieme come categorie di
manutenzione di tipo evolutivo. L’evoluzione, può essere necessaria per cambiamenti del contesto nel quale
l’applicazione opera, per cambiamenti dei requisiti (nuove richieste degli utenti, che emergono solo dopo
l’introduzione del sistema), per errori nelle specifiche o perché alcuni requisiti non erano noti inizialmente.
Per gestire la manutenzione di tipo evolutivo è necessario innanzitutto tenere conto dei cambiamenti facilmente
prevedibili, sin dalle fasi iniziali di sviluppo del software. Inoltre, il software deve essere realizzato con
un’architettura che faciliti i cambiamenti futuri, in modo economico e affidabile: questo è uno dei principali
obiettivi dell’ingegneria del software. Il costo della manutenzione evolutiva è a carico del committente.
a)
Manutenzione adattativa: consiste nell’adattare il software ai cambiamenti dell’ambiente (l’hardware, i
sistemi operativi, le regole di business, le leggi, …). Un esempio è stato il dover adattare le applicazioni
all’introduzione dell’euro. È circa il 20% del totale.
b) Manutenzione perfettiva: consiste nel soddisfare requisiti nuovi o modificati da parte degli utenti. È circa il
50% del totale.
c)
Manutenzione preventiva: consiste nel mettere in atto azioni che mirino ad aumentare la manutenibilità del
software. È circa il 5% del totale.
Talvolta la distinzione tra manutenzione evolutiva e correttiva potrebbe non essere netta ed evidente, perché le
specifiche sono spesso ambigue e incomplete. Questo causa frequenti problemi, perché le specifiche sono parte dei
contratti tra i clienti e gli sviluppatori. Congelare troppo presto le specifiche potrebbe rivelarsi così un problema,
perché probabilmente le specifiche risulteranno inizialmente sbagliate.
I cambiamenti del software, secondo le buone pratiche dell’ingegneria, devono sempre essere prima messi in atto
modificando il design, e solo in seguito devono riguardare l’implementazione. I cambiamenti devono inoltre essere
applicati in modo consistente in tutti i documenti prodotti nello sviluppo del software.
Siccome i cambiamenti non sono quasi mai anticipati e pianificati, potrebbero causare seri problemi.
14
Ingegneria del Software 2
Capitolo 3: Cicli di vita del software
Osservazioni conclusive sul modello a cascata
Il modello a cascata è talvolta dannoso, perché richiede che il dominio venga compreso completamente sin dall’inizio
e che i requisiti siano noti e stabili. Tutto ciò però accade solo raramente, perciò i ricicli non possono essere eliminati,
come invece vorrebbe il modello a cascata.
Il problema principale del modello a cascata è che si tratta sostanzialmente di un modello a scatola nera, che non
fornisce trasparenza: la trasparenza è però necessaria, perché consente di controllare la presenza di errori in anticipo,
apportando eventualmente le modifiche necessario, grazie a dei feedback ottenuti durante il processo stesso. La
trasparenza facilita perciò la flessibilità.
Al termine di ogni fase si dovrebbe quindi eseguire un’operazione di validazione e verifica:

Validazione
La validazione consiste nel verificare che, dal punto di vista dell’utente, il prodotto che si sta realizzando è
effettivamente quello desiderato. La validazione è perciò indipendente dalle specifiche, che potrebbero non
rappresentare in maniera corretta o completa le esigenze dell’utente.

Verifica
La verifica consiste invece nel verificare che si sita procedendo nel modo giusto nel processo di sviluppo del
software: si verifica cioè la correttezza (ovvero, come già discusso, l’aderenza alle specifiche).
Per rispondere a queste esigenze, sono stati sviluppati modelli di cicli di vita del software leggermente diversi, che
consentano di avere dei feedback e di procedere in maniera incrementale. Due esempi sono la prototipazione e il
rilascio incrementale, che si basano ancora sul modello a cascata.
Prototipazione
Concetto di prototipo
Un prototipo è un modello approssimativo di un’applicazione, utilizzato per avere un feedback o per provare alcuni
concetti (ovvero, per effettuare delle verifiche di fattibilità di alcune idee). Il prototipo può anche essere solo un
disegno grafico: non deve necessariamente essere di tipo software.
I prototipi vengono usati nelle fasi preliminari di sviluppo del software. Cosa prototipare dipende da quelli che sono gli
aspetti critici dell’applicazione (ad esempio, l’interfaccia grafica, nel caso in cui sia particolarmente importante
l’usabilità). Se il prototipo viene realizzato durante la fase di design, solitamente viene usato per verificare che le idee
progettuali funzionino; se invece viene realizzato in una delle fasi precedenti, generalmente lo scopo del prototipo è
quello di avere un feedback dall’utente.
Approccio throw-away e approccio evolutivo
Gli approcci possibili secondo i quali la prototipazione può essere messa in atto sono fondamentalmente due:

Approccio throw-away
Prevede che il prototipo venga buttato via e non utilizzato per lo sviluppo vero e proprio del sistema software.

Approccio evolutivo
Prevede che il prototipo venga poi trasformato fino a diventare il sistema finale.
Naturalmente, entrambi gli approcci possono essere validi: di solito si usa l’approccio throw-away se il prototipo è
stato realizzato solo per mostrare qualcosa all’utente, me si usa l’approccio evolutivo se il prototipo riguarda aspetti di
progettazione.
15
Capitolo 3: Cicli di vita del software
Ingegneria del Software 2
Rilascio incrementale (incremental delivery)
Il rilascio incrementale: idee di base
Il rilascio incrementale prevede che il processo di sviluppo del software venga spezzettato in sottoprocessi, destinati
ciascuno ad una diversa funzionalità: l’idea di base è quella che in questo modo si fornisce anticipatamente un certo
insieme di funzionalità, e perciò si avrà anticipatamente un feedback da parte dell’utente, riguardante tale
sottoinsieme di funzionalità.
Il rilascio incrementale viene effettuato partendo dai sottoinsiemi di funzionalità critiche, sui quali è particolarmente
importante avere dei feedback da parte degli utenti.
Analisi
Rilascio del primo incremento
Design
Codifica
Test
Analisi
Design
Codifica
Test
Analisi
Design
Codifica
Rilascio del secondo incremento
Test
Rilascio del terzo incremento
Figura 4: rilascio incrementale
L’approccio incrementale è ancor più importante per i nuovi tipi di prodotti. Per tale ragione, vengono rese disponibili
delle versioni beta, che possano essere provate dagli utenti. In questo un grande aiuto proviene da Internet, che è un
grande mezzo per mettere in mostra e distribuire i diversi incrementi del software.
Il modello a spirale
Introduzione al modello a spirale
Il modello a spirale è un modello ideato da Barry Boehm, che di fatto rappresenta una generalizzazione dell’approccio
incrementale. Possiamo anzi affermare che il modello a spirale è un meta-modello, perché consente di descrivere
anche gli altri modelli di tipo evolutivo.
Il modello a spirale pone una forte enfasi sull’analisi dei requisiti e dei rischi. Esso prevede una continua iterazione tra
diverse task region:
1.
2.
3.
4.
5.
6.
Comunicazione con il cliente
Pianificazione
Analisi dei rischi
Ingegnerizzazione (strutturazione)
Costruzione e rilascio
Valutazione del cliente
Ogni task region è ulteriormente scomposta in vari compiti (task); il numero dei task ed i loro obiettivi vengono
definiti in base alle caratteristiche del progetto. Ogni task region inoltre deve essere di per sé un ciclo di vita.
Il modello a spirale considera centrale la natura evolutiva intrinseca del software. Il modello deve essere però
“specializzato” per uno specifico software che di desidera realizzare. Ad oggi, il modello a spirale non ha ancora
trovato un ampio utilizzo.
16
Ingegneria del Software 2
Capitolo 3: Cicli di vita del software
La figura seguente mostra il modello a spirale.
Figura 5: modello a spirale
Sviluppo basato sui componenti (component-based development)
Lo sviluppo basato sui componenti può in realtà essere considerato un’applicazione del modello a spirare e
dell’approccio incrementale. Il termine “componente” in questo contesto viene usato con il significato di “unità
riutilizzabile”.
In sostanza, ogni iterazione della spirale consente il rilascio di un nuovo componente, mediante l’esecuzione dei
seguenti tasks (che sono quindi quelli che costituiscono ogni iterazione della spirale stessa):
1.
2.
3.
4.
Identificazione dei componenti necessari
Ricerca dei componenti necessari all’interno di librerie, repository, catologhi, …
Possibilmente, sviluppo di nuovi componenti
Rilascio di un nuovo incremento del prodotto
Lo scopo di questo approccio è quello di limitare lo sforzo per lo sviluppo di nuovi componenti, riutilizzando il più
possibile i componenti già esistenti.
17
Capitolo 3: Cicli di vita del software
Ingegneria del Software 2
Rapid Application Development (RAD)
Concetti di base
Il termine RAD è un termine utilizzato per indicare una metodologia di sviluppo del software che prevede un utilizzo
minimo della pianificazione, al fine di ottenere rapidamente la prototipazione del prodotto finale. La pianificazione è
quindi alternata alla fase di implementazione e grazie alla mancanza di una fase intensiva e lunga di progettazione, si
arriva alla scrittura del software in maniera più rapida ed è più semplice modificare i requisiti in corso d’opera.
Tale approccio prevede che si usino insieme le tecniche dello sviluppo incrementale e della prototipazione. Il modello
RAD è usato soprattutto per sviluppare sistemi informativi, le cui caratteristiche consentono un approccio con scarsa
pianificazione, anche grazie al fatto che gli obiettivi del progetto sono ben noti a priori.
Passi fondamentali
I passi principali previsti dall’approccio RAD sono i seguenti:

Business modeling
Il flusso di informazioni tra le diverse funzioni di business viene modellato in modo da identificare le persone, le
informazioni e i processi che caratterizzano il business in esame.

Data modeling
In questa fase si crea un modello nel quale vengono categorizzate e raffinate le definizione delle persone e delle
informazioni di interesse. Si definiscono inoltre le relazioni tra persone e informazioni di interesse per il business
oggetto di studio.

Process modeling
Si creano delle descrizioni dei processi che consentono di aggiungere, modificare, cancellare o recuperare i dati.

Application generation
Utilizzando linguaggi di generazione di quarta generazione (4GL, fourth generation languages), il modulo viene
costruito. Di fatto la generazione dell’applicazione avviene in maniera automatica o semiautomatica.

Testing
I componenti appena creati necessitano di cicli di testing dettagliati; è inoltre necessario eseguire dei controlli di
qualità anche sui componenti riutilizzati.

Turnover
Questa è la fase più importante del modello RAD. Nella fase di turnover, ci si assicura che il cliente sia
pienamente soddisfatto dalle operazioni che il sistema gli consente di eseguire.
Vantaggi e svantaggi
I vantaggi principali sono:
1.
2.
I risultati sono visibili entro breve tempo;
È possibile scomporre il sistema in sottosistemi che possono essere sviluppati entro i termini di tempo prefissati.
Per contro, questa metodologia non è applicabile in situazioni nelle quali i requisiti non sono ben chiari a priori.
18
Ingegneria del Software 2
Capitolo 3: Cicli di vita del software
Extreme Programming (XP)
Concetti di base
Per rispondere alle continue esigenze di rendere più flessibile il processo di sviluppo, sono stati introdotti i cosiddetti
agile methods, tra i quali uno dei più importanti è il modello extreme programming. Il modello XP consente uno
sviluppo incrementale che fornisce dei continui feedback da parte degli utenti. Ad intervalli di poche settimane si ha
infatti un confronto con il committente, durante il quale viene stabilito lo step successivo da eseguire.
Visti gli obiettivi che si prefigge, l’extreme programming sta oggi attirando su di sé forte attenzione: la flessibilità viene
infatti portata a livelli estremi. La premessa sulla quale si basa l’extreme programming è che risulta impossibile
stabilire in maniera esatta le specifiche del software in modo anticipato. L’approccio viene così paragonato a quello
della guida di un’auto: si ha la necessità di continui aggiustamenti.
La distinzione tra il design e l’implementazione risulta naturalmente molto labile, e a volte non esiste del tutto. I
principi base dell’XP model sono 4:
1.
2.
3.
4.
Comunicazione: occorre agevolare la comunicazione all’interno del progetto, nella giusta misura.
Semplicità: bisogna sempre cercare di individuare la soluzione più semplice, purché funzioni.
Feedback: è necessario ottenere continui feedback dai clienti, ma anche attraverso test interni.
Coraggio: bisogna gettare via il lavoro precedente, quando questo si rende necessario.
Come mettere in atto il modello XP?
Le attività fondamentali che si hanno all’interno del modello XP sono la codifica, il testing, l’ascolto ed il design.
Le pratiche fondamentali di cui si fa uso nel modello XP sono:

Pratiche di pianificazione
La pianificazione viene gestita nel modo seguente: ad ogni iterazione, si determina lo scope della release
successiva, si definisce il piano di lavoro e, non appena la realtà rende superato il precedente piano, si procede
ad una nuova attività di pianificazione.
Gli utenti definiscono gli obiettivi che desiderano vengano raggiunti, e per comunicarli utilizzano delle storie. Gli
sviluppatori quindi stimano lo sforzo necessario per ogni storia, e congiuntamente con gli utenti definiscono le
priorità.

Rilasci
I rilasci avvengono a breve distanza di tempo, mettendo a disposizione, per esempio, solo degli insiemi limitati di
funzioni.

Uso di metafore
Il sistema viene spesso caratterizzato per mezzo di metafore, o comunque con un certo livello di astrazione, in
modo tale da definire una visione del sistema e di creare un vocabolario di termini da usare (ad esempio, si può
caratterizzare il sistema mediante frasi come “il calcolo delle pensioni avviene in modo simile a quanto accade
con un foglio di calcolo”).

Design semplificato
In ogni dato istante, si eseguono tutti i test, senza duplicare la logica. Ogni intenzione importante viene
dichiarata. Si definisce il minor numero possibile di classi e metodi.

Refactoring
Quando si implementa una certa caratteristica, si devono sempre considerare i possibili cambiamenti e
miglioramenti.
19
Capitolo 3: Cicli di vita del software
Ingegneria del Software 2

Testing
Si eseguono testing continuamente e in maniera automatica, spesso utilizzando tools automatici.

Programmazione a coppie
Il codice viene scritto solitamente da gruppi di persone molto piccoli (solitamente un team è composto da 2 sole
persone), costituiti da sviluppatori molto esperti che lavorano allo stesso terminale (in modo da tenere alta
l’attenzione).

Proprietà colletiva del codice
Chiunque veda la possibilità di apportare miglioramenti, può modificare il codice. Per fare ciò, è preferibile
adottare un’organizzazione del codice semplice e rispettare le convenzioni di programmazione più diffuse.

Integrazione continua
L’integrazione avviene in continuazione, senza prevedere un’unica fase di integrazione finale dei vari
componenti.

40 ore di lavoro settimanali
Ai programmatori vengono richieste 40 ore di programmazione settimanali, perché il lavoro deve essere
considerato piacere e creatività, e non uno stress.

Partecipazione del cliente
Il cliente deve partecipare allo sviluppo del progetto, e perciò deve partecipare alle discussioni insieme al team di
sviluppo.

Standard di codifica
Si devono rispettare alcuni standard di codifica: si devono seguire le convenzioni più comuni riguardanti la
scrittura del codice, e si deve sempre evitare di duplicare il codice.
Vantaggi dell’XP
Secondo i sostenitori dell’XP, il vantaggio fondamentale di questo approccio è dato da una drastica riduzione dei costi
legati ai cambiamenti. Tuttavia, è bene notare che tale dato non è ancora del tutto provato, perché il modello XP è
molto recente.
Costo dei
cambiamenti
Approccio
tradizionale
Costo dei
cambiamenti
Approccio XP
Istante in cui il cambiamento avviene
(dall’avvio del progetto)
Istante in cui il cambiamento avviene
(dall’avvio del progetto)
Figura 6: confronto tra il costo dei cambiamenti con l’approccio tradizionale e con l’approccio XP
Valutazioni preliminari
Alcune delle pratiche introdotte dal modello XP sembrano essere particolarmente efficaci; tuttavia, la loro efficacia
dipende fortemente dal tipo di progetto che si deve sviluppare, e risulta elevata soprattutto se il progetto è di
dimensioni ridotte, presenta dei requisiti molto volatili, ed è orientato all’utente finale.
20
Ingegneria del Software 2
Capitolo 3: Cicli di vita del software
Rational Unified Process (RUP)
Cos’è RUP?
Il RUP è un framework a supporto del processo iterativo di sviluppo del software. Il termine Rational che compare
nell’acronimo è il nome dell’azienda che ha dato vista a questo strumento, oggi diventata parte di IBM.
L’approccio può essere considerato un’evoluzione di una precedente metodologia per lo sviluppo di software
orientato agli oggetti, nota con Objectory method.
RUP è allo stesso tempo un framework e un prodotto commercializzato da Rational:


Si tratta di un framework, o processo generico, che può essere personalizzato in base allo specifico sistema
software che si deve realizzare, all’organizzazione, al dominio dell’applicazione, al livello di competenza e alle
dimensioni del progetto.
Si tratta di un prodotto che offre una serie di strumenti per automatizzare le varie fasi previste da questo
approccio per lo sviluppo del software.
RUP è in sostanza un insieme di linee guida e di strumenti di supporto per la progettazione del software. In
particolare, le linee guide prevedono l’utilizzo di UML come linguaggio di descrizione, si basano sulle best practices e
focalizzano la loro attenzione sull’architettura del sistema, imponendo la creazione di modelli in ogni fase (si parla
perciò di approccio model-driven).
Gli strumenti utilizzati come supporto per la progettazione del software sono noti con l’acronimo CASE (ComputerAided Software Engineering) e si tratta di applicazioni che automatizzano le fasi di analisi, design e programmazione
del progetto software.
Caratteristiche fondamentali
1. L’approccio di RUP è di tipo iterativo ed incrementale, in modo tale da gestire la complessità e da consentire
l’evoluzione dei requisiti.
2. Inoltre, la metodologia RUP pone l’enfasi sulla modellizzazione anziché sull’uso del linguaggio informale: in questo
modo viene aumentata la leggibilità e la modificabilità ed è possibile sfruttare il supporto di strumenti automatici.
3. Il modello è incentrato sull’architettura del software, che viene definita sin dalle primissime fasi del progetto. Si
cerca inoltre di scomporre il sistema per consentire lo sviluppo parallelo, il riuso e la manutenibilità del sistema.
Fasi dell’approccio RUP
1. Inception: si identificano i casi di business e l’ambito del progetto. In sostanza quindi si inizia a pensare allo
sviluppo del nuovo sistema software. Si definisce al termine di questa fase un documento di vision, contenente le
caratteristiche fondamentali del sistema.
2. Elaboration: si effettua la pianificazione del progetto, definendo tutte le sue caratteristiche ed il design
architetturale.
3. Construction: si sviluppa il sistema.
4. Transizione: si mette in esecuzione il sistema.
Vision
Inception
Funzionalità
iniziali
Architettura
di base
Elaboration
Construction
Rilascio del
prodotto
Transition
Figura 7: fasi della metodologa RUP
21
Capitolo 3: Cicli di vita del software
Ingegneria del Software 2
Iterazione
Oltre alle 4 fasi analizzate, vengono definiti anche diversi workflow (requisiti, analisi, design, implementazione e
testing). Le 4 fasi vengono eseguite nell’ordine precedentemente descritto, ma ciascuna può essere iterata più volte
consecutivamente. In ogni iterazione, i diversi workflow hanno delle rilevanze diverse, che sono messe in evidenza
dalla figura seguente.
Figura 8: workflow e iterazioni all’interno del modello RUP
Sincronizzazione e stabilizzazione (Sync-and-stabilize)
L’approccio sync-and-stabilize
L’approccio sync-and-stabilize è stato adottato da Microsoft ed è noto anche come daily build. Esso è stato concepito
per supportare lo sviluppo di prodotti caratterizzati da una forte complessità, ai quali lavorano team di dimensioni
medie o grandi, con uno sviluppo basato sui componenti, orientati all’introduzione di innovazione durante il corso del
progetto stesso.
I principi di base sono:
1.
2.
3.
Lo sviluppo in parallelo, portato avanti da un piccolo numero di team di sviluppo.
Sincronizzazioni molto frequenti dei risultati prodotti dai diversi team.
Stabilizzazione periodica del prodotto.
Il termine sync-and-stabilize fa riferimento al fatto che alla fine di ogni giornata, ogni lavoratore inserisce i propri
aggiornamenti in un respository, dando così il via ad una serie di test (sincronizzazione); inoltre, periodicamente si
identificano le parti più stabili del sistema, le quali verranno messe in commercio (stabilizzazione).
22
Ingegneria del Software 2
Capitolo 3: Cicli di vita del software
Fasi principali
Le fasi principali dell’approccio sync-and-stabilize sono:

Pianificazione
Si definisce la visione del progetto, si effettua la pianificazione delle attività del progetto e si identificano le sue
caratteristiche fondamentali. Si producono i seguenti output:
a)
Il vision statement, sviluppato dal product manager e contenente gli obiettivi del progetto, ma anche una
lista di caratteristiche che il prodotto finale avrà, con le relative priorità. Questo documento guiderà poi
l’intero progetto.
b) Lo specification document, sviluppato dal program manager e dagli sviluppatori, contenente le specifiche
funzionali e tutte le caratteristiche richieste, ma anche la definizione del design architetturale.
Naturalmente, il set di caratteristiche evolverà poi durante il corso del progetto.
c)
Un documento di pianificazione delle fasi successive, nel quale vengono identificati i sottoprogetti (di solito 3
o 4), da sviluppare in maniera sequenziale e dedicati ciascuno ad un diverso insieme di caratteristiche. In
questo documento vengono anche definiti i tempi per la realizzazione dei vari sottoprogetti, indicando
anche dei buffer time, cioè degli intervalli tra una fase e la successiva, che servono per tenere conto di
eventuali problemi. Tipicamente il primo sottoprogetto è quello che si occupa di realizzare le funzionalità più
importanti e critiche.
Tipicamente, un team ha un program manager, da 3 a 8 sviluppatori ed un tester per ogni sviluppatore.

Sviluppo
I sottoprogetti precedentemente identificati vengono portati avanti uno dopo l’altro. Per ciascuno, si hanno le
fasi seguenti:
a)
b)
c)
d)
Sviluppo delle funzionalità
Integrazione
Testing
Correzione degli errori (bugs)
Durante lo sviluppo, il progetto viene compilato a frequenza giornaliere a settimanale. In questo modo si
possono identificare preventivamente gli errori e i problemi di integrazione, si può monitorare il grado di
completamento del progetto e si possono rendere evidenti gli obiettivi raggiunti.

Stabilizzazione
Si effettuano dei test interni ed esterni e si prepara il rilascio del progetto (ovvero del software da distribuire e
della relativa documentazione), che viene poi messo in funzione (deployment). Questa fase è coordinata dal
project manager.
Vantaggi e svantaggi
In questo approccio i documenti chiave sono il documento di vision e di design architetturale, che vengono scritti nelle
fasi iniziali dello sviluppo. Tuttavia, è difficile anticipare l’analisi delle funzionalità da implementare sin dall’inizio del
progetto. Per questo motivo, non sono previsti documenti di design dettagliato e contenenti le specifiche dettagliate
del software.
La costruzione giornaliera del software offre alcuni vantaggi importanti: si ha sempre una versione aggiornata del
software, i test vengono sviluppati parallelamente all’attività di sviluppo e si ha una visione concreta del progetto e del
suo livello di completamento.
23
Capitolo 3: Cicli di vita del software
Ingegneria del Software 2
Lo sviluppo open source
Gli obiettivi dell’open source
Lo sviluppo open source del software ha caratteristiche molto diverse dallo sviluppo “tradizionale”: esso coinvolge
infatti tantissime persone fortemente distribuite a livello mondiale, che non si conoscono e collaborano tra loro
volontariamente solo per lo sviluppo del software, senza alcun guadagno economico di tipo diretto.
L’obiettivo fondamentale dello sviluppo open source è quello di produrre del software gratuitamente disponibile per
chiunque lo desideri, fornendo direttamente il codice sorgente dell’applicazione.
Trattandosi di uno sviluppo fortemente distribuito, la comunicazione si basa su strumenti legati ad Internet (come ad
esempio le e-mail, i gruppo di discussione, le newslist, …).
In ogni caso, è bene evidenziare che nel software open source esiste sempre un designer principale (o comunque un
certo gruppo con un numero limitato di persone). Tale persona o gruppo di persone si occupa di definire il nucleo
fondamentale dell’applicazione (ovvero la sua architettura) e valuta gli sviluppi messi a disposizione dagli altri
programmatori.
24
Ingegneria del Software 2
Capitolo 4: Il Capacity Maturity Model
Capitolo 4: Il Capacity Maturity Model
Introduzione al Capacity Maturity Model (CMM)
Che cos’è il CMM?
Il Capability Maturity Model (CMM), oggi noto come CMMI è un approccio introdotto al fine di valutare la bontà ad
alto livello del processo di sviluppo. In sostanza quindi si tratta di un metodo per valutare la qualità dell’organizzazione
che produce software, in modo tale da individuare le aree che necessitano di interventi di miglioramento.
Storicamente, questo approccio è stato introdotto nel 1986, quando il Dipartimento della Difesa americano e la NASA
hanno attribuito al SEI (Software Engineering Institute) il compito di effettuare le valutazioni di qualità di aziende che
producono software; il SEI si è così preoccupato di definire dapprima quali sono i parametri per poter definire
“immatura” una certa organizzazione software e quindi, sulla base di questo, è stato possibile definire quali sono le
organizzazioni mature. Il motivo principale per il quale tale approccio è stato introdotto è che si intendeva individuare
una serie di criteri in base ai quali scegliere i fornitori di software più “capaci”, in modo da rivolgersi ad essi per le
iniziative strategiche di difesa.
Cenni alla storia del CMM
1. Nel 1986, nasce l’approccio CMM.
2. Nel 1987 viene definita la struttura generale del modello e viene rilasciato il primo questionario.
3. Tra il 1987 e il 1991 il modello viene sperimentato e validato.
4. Nel 1991 nasce il CMM v.1.0, che era basato sulla conoscenza del processo di produzione del software acquisita
negli anni precedenti e sul feedback degli utenti.
5. Fino al 1993 è stata portata avanti una nuova attività di revisione.
6. Nel 1993 è nata la v.1.1 del CMM.
7. Tra il 1996 e il 1998 è stato costruito il modello CMM v.2.0.
8. Oggi esistono diversi modelli:
a) Il modello CMMI (Capability Maturity Model Integration), che si occupa del miglioramento del processo. In
particolare, esistono diversi modelli CMMI: il modello SW-CMM (Software CMM, che riguarda il processo di
creazione del software), il modello SE-CMM (System Engineering CMM, che riguarda il processo di
ingegnerizzazione dei sistemi) ed il modello IPD-CMM (Integrated Product Development CMM, che riguarda il
processo di creazione di sviluppo di prodotti integrati).
b) Il P-CMMI (People Capability Maturity Model Integration), che si occupa di giudicare il livello di maturità
dell’azienda nella gestione delle proprie risorse umane.
c) Il SA-CMM (Software Acquisition Capability Maturity Model), che giudica il livello di maturità nel processo di
acquisizione del software da parte di un’organizzazione.
25
Capitolo 4: Il Capacity Maturity Model
Ingegneria del Software 2
Valutazione della maturità di un’organizzazione
Il concetto di immaturità
Come accennato, per prima cosa il CMM ha dovuto individuare le caratteristiche tipiche delle organizzazioni
immature; tali caratteristiche sono:
1.
2.
3.
4.
L’improvvisazione nel processo di sviluppo;
Il carattere reattivo dei manager, i quali si occupano di risolvere i problemi man mano che si presentano;
L’incapacità di valutare correttamente i tempi ed i costi del progetto, che porta l’azienda a non rispettare le
scadenze, i budget e i livelli di qualità prefissati;
L’impossibilità di prevedere e giudicare la qualità di un prodotto.
Il concetto di maturità: caratteristiche delle organizzazioni mature
1. La capacità di gestire processi di sviluppo e manutenzione è parte del patrimonio aziendale, e non è delegata ai
singoli;
2. Si segue un modello di processo accuratamente definito e noto a tutti;
3. Il modello di processo viene continuamente migliorato, attraverso un’attività di manutenzione;
4. I manager sono in grado di verificare la qualità dei prodotti e il livello di soddisfazione dei clienti;
5. Si effettuano stime realistiche, sulla base dei dati storici.
Alcune definizioni

Software process capability
Descrive che cosa ci si può aspettare da un dato processo software.

Software process performance
Rappresenta i risultati effettivi ottenuti da un dato processo software.

Software process maturity
Indica il livello a cui un processo viene definito, gestito, misurato, controllato ed attuato. Se il processo software
è maturo, possiamo prevederne le capability e possiamo incrementare nel tempo le performance.
I livelli di maturità
Il SEI ha definito 5 diversi livelli di maturità per un’organizzazione che produce software:
Continuo miglioramento
del processo
Processo
prevedibile
Livello 5:
Ottimizzato
Livello 4:
Gestito
Livello 3:
Definito
Definizione di standard,
Processo consistente
Processo
metodico
Livello 2:
Ripetibile
Livello 1:
Iniziale
Figura 9: i 5 livelli di maturità definiti dal SEI all’interno del CMM
26
Ingegneria del Software 2
Capitolo 4: Il Capacity Maturity Model
Vediamo ora di analizzare nel dettaglio ciascuno di tali livelli:

Livello iniziale (processo improssivato)
A questo livello, l’organizzazione è giudicata completamente immatura.
Il processo viene definito ad-hoc per ogni singolo progetto ed è caotico. Pochi processi sono stati definiti ed i
successi dipendono esclusivamente dalle capacità e dagli sforzi personali.
Non esiste un “ambiente” stabile, non vengono praticate le “buone tecniche” di gestione dei progetti. Al
profilarsi di una “crisi” (approssimarsi della scadenza, superamento del budget), le procedure pianificate vengono
abbandonate e tutte le attività di controllo di qualità vengono eliminate.
Di conseguenza, le software process capability di una organizzazione a livello 1 non sono predicibili, non è
possibile stimare con precisione la durata ed il costo dei progetti, le funzionalità che verranno implementate e la
qualità che si potrà raggiungere nel prodotto.

Livello ripetibile (processo creato sulla base dei precedenti con caratteristiche simili)
Al livello ripetibile esiste un minimo di supporto alla gestione del processo, che consente in alcuni casi di ripetere
i successi aziendali (a patto che il contesto applicativo non sia troppo diverso da quello precedentemente
affrontato). Sono quindi presenti le tecniche base di project management: costi, tempi e funzionalità vengono
tenuti sotto controllo. La pianificazione di un nuovo progetto viene condotta sulla base dell’esperienza
accumulata in progetti simili.
Le tecniche di management consentono di stabilire degli obiettivi ragionevoli sulla base del confronto con
esperienze precedenti. I manager controllano l’avanzamento di costi, tempi e funzionalità: conflitti con gli
obiettivi posti vengono identificati ed affrontati. Vengono inoltre attuate le procedure atte a garantire la
consistenza ed integrità di tutti i semi-lavorati.

Livello definito (processo standard dell’organizzazione predefinito e derivazione dei singoli processi da esso)
Se un’organizzazione si trova al livello definito, allora all’interno dell’organizzazione esiste un processo
documentato e standardizzato che definisce sia le attività tecniche che quelle di gestione in modo organico
(organization’s standard software process). La responsabilità di gestire il processo standard è esplicitamente
affidata ad alcuni individui.
Inoltre, tutti i progetti di sviluppo e manutenzione seguono un processo derivato secondo regole prestabilite da
quello standard (project’s defined software process). Tale processo definisce ad esempio i criteri di
completamento, gli standard e le procedure di lavoro e le tecniche di verifica da adottare.
In un’organizzazione a livello definito, il management ha una buona visibilità del processo per tutti i progetti ed
esiste un programma di training che copre tutti i ruoli presenti nell’organizzazione.

Livello gestito (processo definito e valutato attraverso opportune misure)
L’organizzazione è al livello gestito se si occupa di raccogliere delle misure relative alla qualità del processo e dei
prodotti, in modo da valutarli e controllarli quantitativamente.
Le misure vengono raccolte, mantenute ed analizzate a livello aziendale (software process database). Inoltre è
possibile stabilire degli obiettivi quantitativi sulla qualità del processo e dei prodotti. I margini di variabilità dei
risultati ottenibili per un progetto sono ridotti ed è possibile predire la qualità dei prodotti, e realizzare prodotti
di alta qualità.
27
Capitolo 4: Il Capacity Maturity Model

Ingegneria del Software 2
Livello ottimizzante (miglioramento del processo basato sulle misure di valutazione della sua qualità)
In un’organizzazione a livello ottimizzante, i riscontri quantitativi dati dall’attuazione del processo e dalla
realizzazione di progetti pilota forniscono indicazioni su come migliorare continuamente il processo standard,
attraverso l’identificazione dei punti di forza e delle carenze del processo, in modo poi da prevenire i difetti,
anziché eliminarli: i difetti riscontrati vengono analizzati in modo da poterne identificare (e rimuovere) le cause
all’interno del processo di sviluppo.
Possiamo perciò affermare che le organizzazioni a livello 5 tentano continuamente di migliorare il proprio
processo, in modo incrementale: si migliora il processo esistente introducendo in modo controllato delle
innovazioni, dei nuovi metodi e delle nuove tecnologie.
Da quanto finora detto risulta abbastanza evidente che i livelli di maturità inferiori costituiscono delle basi
indispensabili su cui fondare i livelli superiori, di conseguenza è opportuno che un’organizzazione raggiunga un certo
livello di maturità in maniera graduale, attraversandoli tutti a partire dal più basso. Del resto, sarebbe inutile, ad
esempio, tentare di misurare un processo (livello 4) se non è ancora stato neppure definito il processo stesso.
Perché valutare la maturit{ di un’organizzazione?
I motivi per i quali la valutazione della maturità di un’organizzazione è utile sono diversi:
1.
2.
3.
Software Capability Evaluation (valutazione delle capacità software)
Si può valutare attraverso il CMM la maturità di un fornitore, in modo tale da sfruttare i risultate di tale analisi per
poter scegliere opportunamente a quale fornitore rivolgersi.
Inoltre, si può utilizzare il CMM per monitorare delle attività già in corso.
Software Process Assessment (pianificazione del processo software)
Si determina lo stato attuale del processo software di un’organizzazione, in modo da identificarne le carenze più
importanti. In questo modo si potrà poi ottenere il supporto organizzativo al miglioramento del processo.
Interim profile (valutazione interna dei progressi)
Il CMM è un modo non dispendioso per valutare concisamente i progressi ottenuti dal processo di miglioramento.
Viene effettuato ad intervalli regolari fra due fasi successive di pianificazione del processo, in modo da poter
valutare i risultati delle modifiche apportate al processo del software stesso.
Adottare il CMMI comporta inoltre degli importanti benefici in termini di costi, tempi, qualità e soddisfazione dei
clienti, nonché un incremento del cosiddetto ROI (Return on Investment).
28
Ingegneria del Software 2
Capitolo 4: Il Capacity Maturity Model
Le fasi di un assessment
L’assessment del processo software avviene secondo le fasi seguenti.
1.
2.
3.
4.
5.
6.
La prima fase consiste nell’identificare un team che si occupi di tale operazione. Il team deve aver ricevuto una
formazione sui principi fondamentali del CMM e deve essere costituito da professionisti con conoscenze ed
esperienze nell’ingegneria del software e nel management.
Dopodiché, il team selezionato si occuperà di effettuare dei questionari di maturità, secondo il modello imposto
dal CMM.
In tal modo sarà possibile passare alla fase successiva, che consiste nell’analisi dei dati così raccolti è
nell’identificazione delle aree chiave del processo, ovvero di quelle aree che devono ancora essere esplorate
perché potrebbero essere migliorate.
A questo punto, il team di valutazione è in grado di effettuare una visita sul luogo dell’organizzazione in analisi. Il
team condurrà così delle interviste sul luogo e si occuperà di analizzare la documentazione dell’organizzazione
riguardante il processo del software. In tale fase, il team sarà guidato dalle valutazioni precedentemente
effettuate.
Alla fine di questa fase, viene prodotto un documento nel quale vengono elencati i punti forti e le debolezze del
processo software dell’azienda.
Infine, il team si occupa di preparare un key process area profile, nel quale vengono evidenziate le aree nelle quali
l’organizzazione è in grado di ottenere gli obiettivi prefissati e quelle in cui invece non è in grado di raggiungerli. È
possibile che un’area, pur avendo ancora delle debolezze, sia in grado di raggiungere gli obiettivi prefissati.
Figura 10: le fasi di un assessment
29
Capitolo 4: Il Capacity Maturity Model
Ingegneria del Software 2
Standard ISO 9000
Cosa sono gli standard ISO 9000?
Gli standard ISO 9000 sono delle normative e linee guida sviluppate dall’ISO (International Organization for
Standardization) che definiscono i requisiti per l'implementazione, in una organizzazione, di un sistema di gestione
della qualità, al fine di condurre i processi aziendali, migliorare l'efficacia e l'efficienza nella realizzazione del prodotto
e nell'erogazione del servizio, ottenere ed incrementare la soddisfazione del cliente.
Si tratta perciò di norme che descrivono i requisiti di un sistema di qualità e definiscono gli attributi minimi che deve
avere una organizzazione per poter soddisfare determinati impegni contrattuali.
Le norme ISO 9000 riguardano prodotti di ogni tipo. Gli standard fondamentali in vigore dal 2000 sono 3:

ISO 9000:2000 Fundamentals and vocabulary (Fondamenti e vocabolario)
Si tratta di una norma che descrive il vocabolario ed i principi essenziali dei sistemi di gestione per la qualità e
della loro organizzazione.

ISO 9001:2000 Requirements (Requisiti)
È una norma che definisce i requisiti di un sistema di gestione per la qualità per una organizzazione.

ISO 9004:2000 Guidelines for performance improvements (L’approccio della gestione per la qualità)
Non si tratta di una norma, ma di una linea guida per favorire in una organizzazione il conseguimento del
successo durevole per mezzo della gestione per la qualità.
La certificazione
Le aziende che seguono le norme della serie ISO 9000 possono ottenere dall’ISO la relativa certificazione, che di
conseguenza rappresenta un attestato della qualità del processo adottato dall’organizzazione. Per ottenere tale
certificazione, l’organizzazione deve:
1.
2.
Definire, documentare e mantenere le procedure ed istruzioni operative.
Verificare che le procedure e le istruzioni definite vengano seguite, conservandone le prove.
I requisiti da soddisfare
I requisiti che l’organizzazione deve soddisfare per ottenere la certificazione ISO 9000 sono di diversi tipi:
1.
2.
3.
4.
5.
Systematic requirements (requisiti sistematici)
Ad esempio, bisogna realizzare dei documenti che riguardano il sistema di controllo della qualità.
Management requirements (requisiti di gestione)
Ad esempio, occorre definire delle politiche interne, bisogna soddisfare il cliente, …
Resource requirements (requisiti sulle risorse)
Ad esempio, è necessario che il personale dell’organizzazione sia di qualità.
Realization requirements (requisiti di realizzazione)
Ad esempio, bisogna controllare la pianificazione della realizzazione del prodotto.
Remedial requirements (requisiti delle misure di rimedio)
Ad esempio, bisogna monitorare la qualità.
Confontro tra ISO 9000 e CMM
La normativa ISO 9000 e l’approccio CMM riguardano entrambi il controllo di qualità di un processo, ma hanno
differenze significative: la più importante è che la certificazione ISO 9000 si limita a imporre dei requisiti minimi di
accettabilità, mentre il CMM impone un miglioramento continuo del processo (di conseguenza, l’ISO 9000 ha
granularità meno fine).
I due approcci sono però sinergici: si può ad esempio utilizzare il CMM come linea guida per raggiungere gli obiettivi
imposti per ottenere la certificazione ISO 9000.
30
Ingegneria del Software 2
Capitolo 5: Ingegneria dei requisiti
Capitolo 5: Ingegneria dei requisiti
L’ingegneria dei requisiti
Definizione
L’ingegneria dei requisiti è un insieme di attività che hanno a che fare con l’identificazione e la comunicazione dello
scopo e dei requisiti di un sistema software
Di conseguenza, possiamo affermare che l’ingegneria dei requisiti fa da ponte tra le necessità dei committenti e degli
utenti all’interno del mondo reale e le capacità ed opportunità messe a disposizione dalle tecnologie software.
Esigenze del
mondo reale
Requirement Engineering (RE)
Tecnologie
software
Figura 11: L’ingegneria dei requisiti come ponte tra mondo reale e tecnologie software
L’ingegneria dei requisiti, in altri termini, si occupa di studiare le relazioni tra il mondo reale e il mondo software. Per
meglio capire questo concetto, indicheremo in seguito con il termine macchina la porzione di sistema che deve essere
sviluppata e mondo (o ambiente) quella parte del mondo reale che ha a che fare con il sistema da sviluppare.
Naturalmente, lo scopo della macchina è sempre strettamente legato al mondo reale. Se definiamo l’insieme dei
fenomeni del mondo (ovvero l’insieme di tutti quegli eventi che accadono nell’ambiente) e l’insieme dei fenomeni del
sistema (cioè l’insieme di tutti quei fenomeni che accadono all’interno della macchina), risulta quindi ovvio che
l’intersezione tra tali insiemi non può essere vuota.
Fenomeni
del mondo
Fenomeni
condivisi
Fenomeni della
macchina
Figura 12: Fenomeni del mondo, fenomeni della macchina e fenomeni condivisi
In altri termini, devono esistere dei fenomeni condivisi, ovvero dei fenomeni che siano o controllati dal mondo e
osservati dalla macchina, oppure controllati dalla macchina ed osservati dal mondo.
L’ingegneria dei requisiti deve essere in grado di determinare quali sono i fenomeni condivisi, che rappresentano
l’interfaccia tra il mondo e la macchina: questo infatti significa individuare le caratteristiche che il sistema deve avere
per poter fornire un beneficio al mondo esterno. I modelli utilizzati per rappresentare i requisiti fanno solitamente
riferimento al mondo esterno.
31
Capitolo 5: Ingegneria dei requisiti
Ingegneria del Software 2
L’importanza dell’ingegneria dei requisiti
L’ingegneria dei requisiti ha un ruolo molto importante, per diverse ragioni:
1.
2.
3.
I requisiti troppo poveri e scarsamente definiti sono molto diffusi, ma è necessario ingegnerizzarli e rivederli e
correggerli continuamente.
L’ingegneria dei requisiti è una fase complessa e critica.
Individuare errori che riguardano i requisiti del sistema quando si è ormai giunti alle fasi avanzate del processo
software causa costi molto elevati (specialmente se ciò accade dopo il rilascio) e significativi rallentamenti: da un
punto di vista economico, il costo può essere anche 200 volte superiore rispetto a quello che sarebbe necessario
affrontare se gli errori nei requisiti venissero immediatamente identificati. L’ingegneria dei requisiti può fornire
un supporto per limitare questi fenomeni.
Nel corso degli anni, diverse indagini hanno evidenziato come la definizione inaccurata, erronea, incompleta o tardiva
dei requisiti sia una delle cause fondamentali che conducono un progetto ad essere realizzato con gravi mancanze, o
addirittura che portano alla sua cancellazione.
Altre indagini hanno evidenziato come gli errori nella specifica e nella gestione dei requisiti siano la causa principale
dei problemi del software.
Si può perciò affermare che l’attività di identificazione e analisi dei requisiti determina con un peso circa pari al 40%
l’esito di successo o insuccesso di un sistema software.
Complessit{ dell’ingegneria dei requisiti
I fattori che rendono complessa l’ingegneria dei requisiti sono molteplici:
1.
2.
3.
4.
5.
32
Complessità e vastezza dell’ambiente
Spesso i sistemi nei quali il software deve essere inserito risultano essere molto complessi e richiedono
l’interazione del software con altri componenti software, ma anche con persone, computer, dispositivi hardware
(come ad esempio sensori), …
Diversi sistemi da analizzare
I sistemi da analizzare sono in realtà più d’uno: occorre tenere conto del sistema così come è prima che si realizzi
il nuovo sistema software (system-as-is; SAI) e il sistema che si dovrà realizzare (system-to-be, S2B), tenendo
conto delle diverse alternative possibili e della sua futura evoluzione.
Diversi livelli di astrazione
Gli obiettivi possono essere definiti a diversi livelli di astrazione.
Diversi ambiti di cui tener conto
I requisiti possono essere di tipo funzionale, di qualità, di sviluppo, … e di conseguenza è possibile che le qualità
desiderata vadano in conflitto tra loro. Ciò rende necessaria l’introduzione di diversi livelli di priorità tra i requisiti.
Persone diverse con cui interagire
Durante la fase di analisi e specifica dei requisiti è necessario interagire con persone molto diverse tra di loro
(clienti, sviluppatori, utenti, esperti del dominio dell’applicazione, …). Tali categorie di persone hanno ovviamente
dei background molto diversi tra loro, e questo conduce ad una elevata difficoltà nell’individuazione di un modo
di comunicare che possa essere compreso da tutti.
Ingegneria del Software 2
Capitolo 5: Ingegneria dei requisiti
Le dimensioni dell’ingegneria dei requisiti
L’ingegneria dei requisiti ha 3 dimensioni: why, what e who.

Dimensione Why?
La dimensione why? Consiste nello svolgere le seguenti attività:
a. Capire lo stato attuale del sistema (SAI) e come deve essere il nuovo sistema (S2B).
b. Analizzare le diverse alternative possibili per soddisfare gli obiettivi del sistema che si deve realizzare.
c. Ottenere un guadagno dalla comprensione del dominio applicativo e dalle possibilità messe a disposizione
dalle nuove tecnologie.
La dimensione why coinvolge diverse “parti” (categorie di individui).

Dimensione What?
La dimensione what? mette in relazione i servizi funzionali che il system-to-be deve fornire con gli obiettivi
identificati nel corso della why dimension.

Dimensione Who?
La dimensione who? si occupa di assegnare le responsabilità per il raggiungimento degli obiettivi prefissati.
Le attivit{ dell’ingegneria dei requisiti
L’ingegneria dei requisiti si occupa di svolgere le seguenti attività:
1.
2.
3.
4.
5.
Raccolta delle informazioni
Attraverso interviste al committente e agli utenti, ma anche attraverso l’analisi dei sistemi già esistenti e talvolta
anche attraverso l’interazione con tecnici dell’aspetto di dominio dell’applicazione, si individuano gli obiettivi del
progetto, il contesto, l’ambiente e si raccolgono le conoscenze di dominio e i requisiti.
Modellizzazione e analisi
L’ingegneria dei requisiti si occupa di descrivere le informazioni raccolte con un approccio di modellazione
sufficientemente preciso. I modelli così prodotti vengono poi analizzati, cercando di rilevare eventuali incoerenze.
I modelli hanno un ruolo centrale, perché sono la sintesi delle informazioni raccolte e la base per la
comunicazione dei requisiti con le varie personalità coinvolte.
Comunicazione dei requisiti
Il modello ottenuto deve poi essere comunicato a tutte le parti coinvolte, utilizzando dei linguaggi opportuni e
diversi a seconda degli interlocutori. La comunicazione avviene per mezzo di prototipi del sistema, documenti
come l’SRS, …
Negoziazione e raggiungimento di un consenso sui requisiti
Occorre gestire i rischi ed i conflitti tra i vari requisiti. Per tale motivo, bisogna riuscire a capire quali requisiti sono
quelli su cui focalizzarsi e quali invece possono essere trascurati. Ciò accade mediante una negoziazione tra i
progettisti e i committenti.
Gestione ed evoluzione dei requisiti
I requisiti devono essere gestiti per tutta la fase di sviluppo, in modo tale che se ne possa conservare la
tracciabilità. Occorre inoltre gestire i cambiamenti dei requisiti, controllando l’impatto che ognuno di questi
cambiamenti potrà avere.
33
Capitolo 5: Ingegneria dei requisiti
Ingegneria del Software 2
I System Context Diagrams
Cosa sono i system context diagrams
I system context diagrams sono dei modelli che possono essere utilizzati per mostrare le parti più importanti del
mondo e le loro interfacce con la macchina.
Elementi del context diagram

Agenti
Gli agenti sono delle entità attive, ovvero in grado di controllare dei fenomeni del mondo. Un agente può essere
umano, di tipo software, oppure può essere un dispositivo hardware.
Gli agenti vengono rappresentati mediante un rettangolo all’interno del quale è indicato il nome dell’agente
stesso:
Agente
Figura 13: Rappresentazione di un agente all’interno di un system context diagram

La macchina
La macchina (ovvero il S2B) viene rappresentata in maniera molto simile all’agente, ma con i bordi laterali doppi:
Macchina
Figura 14: Rappresentazione della macchina in un system context diagram

I fenomeni
I fenomeni sono rappresentati mediante delle frecce che partono da un agente (o dalla macchina) ed arrivano ad
un altro agente (o alla macchina). L’agente da cui la freccia ha origine è colui che controlla il fenomeno (cioè colui
che lo provoca), mentre l’agente sul quale la freccia arriva è l’agente che lo monitora. Sulla freccia è inoltre
indicato il nome del fenomeno.
Agente A
Fenomeno
Agente B
Figura 15: Rappresentazione di un fenomeno all’interno di un system context diagram
Di conseguenza, per ogni agente è definito un insieme di fenomeni da esso controllati e un insieme di fenomeni
da esso monitorati.
Esempio di context diagram
Figura 16: Un esempio di system context diagram
34
Ingegneria del Software 2
Capitolo 5: Ingegneria dei requisiti
Gli obiettivi (goal) e le asserzioni
Le asserzioni
Per definire i requisiti del sistema, occorre per prima cosa definire delle asserzioni, che possono essere di diverso tipo:

Asserzioni descrittive o indicative
Le asserzioni legate a leggi naturali o vincoli fisici. Ad esempio:

Asserzioni prescrittive
Le asserzioni prescrittive affermano delle proprietà desiderabili che possono essere vere oppure no, a seconda di
come il sistema funziona. Esprimono quindi dei desideri e devono essere messe in atto dai componenti del
sistema. Ad esempio:
Goal, proprietà di dominio e requisiti
Vediamo di introdurre alcuni concetti fondamentali. Gli esempi utilizzati sono relativi ad un sistema di gestione delle
ambulanze.

Goal
Gli obiettivi (goal) sono delle asserzioni prescrittive formulate in termini di fenomeni del mondo.
Esempio:

Proprietà di dominio
Le proprietà di dominio sono delle asserzioni descrittive che si assume siano vere nel mondo.
Esempio:

Per ogni chiamata urgente che segnala un incidente, un’ambulanza deve arrivare sul luogo
dell’incidente entro 14 minuti.
La posizione delle ambulanze è nota con precisione mediante il sistema GPS.
Requisiti
I requisiti sono delle asserzioni prescrittive formulate in termini di fenomeni condivisi.
Esempio:
Quando viene codificata una chiamata che segnala un nuovo incidente l’ADS (Automated
Dispatching Software, ovvero il sistema) deve mobilitare l’ambulanza disponibile più vicina
l’incidente, sulla base delle informazioni messe a disposizione dal GPS delle ambulanze.
Si noti che l’ingegneria dei requisiti è ancora relativamente giovane, e perciò non esiste ancora una terminologia
universalmente adottata. Ad esempio, alcuni autori usano il termine requirement (requisito) anziché il termine goal ed
il termine specification (specifica) al posto di requirement.
Altri autori invece usano i termini requisito di sistema (per goal) e requisito software (al posto del nostro requisito).
35
Capitolo 5: Ingegneria dei requisiti
Ingegneria del Software 2
Provare la correttezza
Per provare la correttezza di requisiti, goal e proprietà di dominio bisogna:
1.
2.
3.
Verificare che l’insieme D delle proprietà di dominio rappresenti proprietà e assunzioni valide nel mondo in
analisi.
Verificare che l’insieme degli obiettivi G catturi tutte le esigenze degli stakeholders.
Assicurarsi che l’insieme dei requisiti R soddisfi tutti gli obiettivi di G all’interno del dominio le cui proprietà sono
definite da D. In altri termini, occorre verificare che dalla verità di tutte le formule di R e di D segua la verità di
tutte le formule logiche di G:
Il confine tra il mondo e la macchina solitamente non è ben definito all’inizio dello sviluppo del progetto: l’ingegneria
dei requisiti ha lo scopo di identificare i goal reali del progetto e di esplorare le varie alternative per soddisfare tali
obiettivi, attraverso diverse coppie (R, D) oppure attraverso interfacce alternative tra il mondo e la macchina. Per ogni
alternativa si deve poi valutare il rischio, allo scopo di scegliere poi quella più appropriata.
Il documento SRS
Che cos’è il documento SRS?
Il documento SRS (Software Requirements Specifications) è un documento all’interno del quale vengono catturati tutti
i requisiti del sistema che si vuole realizzare.
L’SRS viene realizzato per diversi motivi:
1.
2.
3.
4.
5.
Ha lo scopo di aiutare a capire e comunicare i requisiti del sistema, spiegando sia il dominio applicativo, sia il
sistema che si vuole realizzare.
Spesso ha un valore contrattuale ed è legato ad aspetti legali (ciò che è scritto nell’SRS rappresenta un impegno
formale e vincolante da parte di chi desidera fornire il software).
Rappresenta la base per la pianificazione del progetto e per la stima della sua durata e dei suoi costi.
Rappresenta la base per effettuare le attività di test, verifica e validazione. L’SRS dovrebbe infatti contenere
informazioni sufficienti per verificare se il sistema finale effettivamente soddisfa i requisiti contenuti nell’SRS
stesso.
È una base per il controllo dei cambiamenti dei requisiti e, di conseguenza, del software.
A chi si rivolge l’SRS?
Il documento SRS è rivolto ad un insieme piuttosto vasto di figure diverse, con caratteristiche molto distanti tra loro:
1.
2.
3.
4.
5.
36
Clienti e utenti, interessati soprattutto agli obiettivi che riguardano la validazione del sistema e alla descrizione di
alto livello delle funzionalità che il sistema fornirà.
Analisti del sistema e dei requisiti, che devono scrivere le specifiche di altri sistemi correlati a quello descritto
nell’SRS stesso.
Sviluppatori e programmatori, che devono fare in modo che l’implementazione del software soddisfi i requisiti.
Tester, che dovranno verificare se i requisiti sono soddisfatti dall’implementazione del software.
I manager del progetto, che devono analizzare, misurare e controllare il processo di sviluppo.
Ingegneria del Software 2
Capitolo 5: Ingegneria dei requisiti
Chi scrive l’SRS?
L’SRS potrebbe essere scritto da categorie diverse di persone:

Chi richiede il sistema software
In questo caso, l’SRS deve essere abbastanza generale per raccogliere un buon numero di offerte di
implementazione, ma non troppo, così da escludere le offerte non ragionevoli. L’SRS è inoltre una vera e propria
richiesta di proposte di implementazione, che verranno poi fornite dagli sviluppatori.

Chi fornirà il sistema software
L’SRS è in tal caso una proposta di implementazione di un sistema, abbastanza specifica per dimostrarne la
fattibilità, ma anche abbastanza generale, in modo che l’organizzazione che deve fornire il software e che sta
scrivendo l’SRS non si impegni in modo eccessivo, imponendo requisiti difficili da implementare.

Uno sviluppatore selezionato
In questa situazione, l’SRS riflette il modo in cui lo sviluppatore ha compreso le esigenze del cliente e costituisce
la base per la valutazione delle prestazioni contrattuali.

Una persona indipendente ed esterna
Ovvero, una parte terza rispetto alle due parti interessate dal contratto.
Lo standard dell’IEEE suggerisce che l’SRS venga scritto congiuntamente da chi richiede il sistema e chi intende
fornirlo.
I contenuti dell’SRS
Il documento SRS dovrebbe contenere le seguenti informazioni:

Funzionalità
L’elenco delle funzionalità che il software dovrà supportare (ciò che il software farà).

Interfacce esterne
Il modo attraverso il quale il software interagisce con le persone, con l’hardware del sistema, ma anche con
componenti hardware e software esterni.

Performance
La velocità del software, la disponibilità, i tempi di risposta, il tempo di ripristino delle diverse funzionalità dopo
un fallimento, …

Attributi
Occorre definire la portabilità, la correttezza, la manutenibilità, la sicurezza, … del software.

Vincoli di design sull’implementazione
Bisogna elencare gli standard da utilizzare, la lingua dell’implementazione, le politiche per l’integrità del
database, i limiti delle risorse, l’ambiente operativo, … .
37
Capitolo 5: Ingegneria dei requisiti
Ingegneria del Software 2
La struttura dell’SRS secondo lo standard IEEE
L’IEEE ha definito una struttura standard per il documento SRS, che prevede le seguenti sezioni:
1.
Introduction
1.1. Purpose
1.2. Scope
identificazione del prodotto e del dominio applicativo
1.3. Definitions, acronyms, abbreviations
1.4. Reference documents
1.5. Overview
descrizione dei contenuti e della struttura dell’SRS
2. Overall Description
2.1. Product perspective
descrizione di tutte le interface esterne e dei vincoli hardware
2.2. Product functions
riassunto delle funzionalità più importanti
2.3. User characteristics
2.4. Constraints
qualsiasi vincolo che limiti le opzioni a disposizione degli sviluppatori
2.5. Assumptions and Dependencies
3. Specific Requirements
3.1. External Interface Requirements
3.1.1. User Interfaces
3.1.2. Hardware Interfaces
3.1.3. Software Interfaces
3.1.4. Communication Interfaces
3.2. Functional Requirements
può essere organizzata per modalità, tipologia di utenti, caratteristica, …
3.2.1. User Class 1
3.2.1.1. Functional Requirement 1.1
…
3.2.2. User Class 2
3.2.2.1. Functional Requirement 1.1
…
...
3.3. Performance Requirements
3.4. Design Constraints
3.4.1. Standards compliance
3.4.2. Hardware limitations
…
3.5. Software System Attributes
3.5.1. Reliability
3.5.2. Availability
3.5.3. Security
3.5.4. Maintainability
3.5.5. Portability
3.6. Other Requirements
Appendices
Index
38
Ingegneria del Software 2
Capitolo 5: Ingegneria dei requisiti
Qualità di un SRS
Vediamo ora quali sono le caratteristiche di un buon SRS:

Completezza
Il documento SRS deve essere completo:
a)
Per quanto riguarda gli obiettivi (goal). Bisogna infatti individuare tutti gli obiettivi rilevanti, compresi quelli
che riguardano la qualità. Bisogna inoltre individuare tutte le proprietà di dominio, e tutte in maniera
corretta.
Altrimenti, anche se la condizione:
è soddisfatta, i requisiti non saranno dei buoni requisiti.
b) Per quanto riguarda gli input: il comportamento del software deve essere definito per ogni possibile input.
c) Deve inoltre avere la cosiddetta completezza strutturale (non ci devono essere caratteristiche ancora da
discutere).

Pertinenza
1. Ogni requisito o assunzione di dominio deve essere necessario per il raggiungimento di un certo obiettivo.
2. Ogni obiettivo è davvero necessario per gli stakeholder.
3. Nell’SRS non vengono specificate informazioni che non riguardano i requisiti (come ad esempio informazioni
relative al design).

Consistenza
Non devono esserci contraddizioni tra i vari obiettivi, proprietà di dominio e/o requisiti.

Mancanza di ambiguità
1. Il dizionario non deve essere ambiguo: ogni termine deve essere definito e utilizzato coerentemente.
2. Le asserzioni non devono lasciar spazio ad interpretazioni diverse tra loro.
3. Ogni requisito deve essere verificabile mediante un opportuno processo.
4. La separazione tra le responsabilità del S2B e quelle dell’ambiente deve essere chiaramente definita.

Fattibilità
Gli obiettivi e i requisiti definiti all’interno dell’SRS devono essere realizzabili all’interno dei limiti di budget e di
tempo che sono stati imposti alla realizzazione del progetto.

Comprensibilità
Il documento SRS deve essere comprensibile da tutte le parti interessate.

Tracciabilità
1. Occorre indicare da dove provengono gli obiettivi, i requisiti e le assunzioni;
2. Si deve evidenziare quali sono i requisiti e le assunzioni che stanno al di sotto degli obiettivi;
3. È necessario facilitare i riferimenti ai requisiti nella documentazione futura.

Buona struttura del documento
Il documento deve avere una struttura che ne faciliti la comprensione. Ogni elemento deve essere perciò definite
prima di essere utilizzato. Inoltre, è opportuno ad esempio evidenziare i collegamenti tra obiettivi, requisiti e
assunzioni.

Modificabilità
Il documento SRS deve essere facilmente adattato, esteso o contratto attraverso delle modifiche locali. L’impatto
che consegue dalla modifica di un singolo elemento dovrebbe essere facilmente valutabile.
39
Capitolo 5: Ingegneria dei requisiti
Ingegneria del Software 2
Possibili errori all’interno di un SRS
Gli errori nei quali si può incappare durante la scrittura di un SRS sono strettamente correlati alle qualità elencate:
1.
2.
3.
4.
Incompletezza (requisiti o obiettivi mancanti, comportamento non definito per alcuni input).
Requisiti inadeguati.
Contraddizioni.
Ambiguità (termini ambigui o non definiti, ambiguità espressiva nella descrizione di obiettivi, requisiti e
assunzioni, scrittura di requisiti non verificabili, ambiguità nella divisione delle responsabilità).
5. Obiettivi e requisiti non raggiungibili.
6. Presenza di “rumori” (presenza di elementi che non hanno a che fare con i requisiti, o di elementi inutili, inseriti
solo per conformità rispetto allo standard del documento SRS, oppure presenza di elementi ridondanti).
7. Specifica di elementi che hanno a che fare con il design e con l’implementazione.
8. Mancanza di chiarezza (terminologia inventata inutilmente, presenza di frasi scritte male, …)
9. Scarsa strutturazione (riferimenti ad elementi definiti in seguito, elementi definiti all’ultimo momento tra
parentesi, riferimenti incrociati difficili da seguire).
10. Mancanza di tracciabilità.
Osservazioni
1.
2.
3.
4.
5.
La specifica dei requisiti sarà necessariamente imperfetta: l’ingegneria dei requisiti si occupa di fornire dei modelli
che rappresentano il mondo in maniera approssimativa, e perciò conterranno degli elementi poco accurati e delle
consistenze, e ometteranno delle informazioni. Attraverso l’analisi, il rischio che questi problemi comportino dei
seri rischi deve essere il più possibile ridotto.
Perfezionare una specifica potrebbe essere non conveniente dal punto di vista economico: l’analisi dei requisiti ha
un costo e, a seconda del progetto in analisi, il rapporto tra costi e benefici è diverso. Tuttavia, questo non deve
essere usato come scusa per non investire nell’ingegneria dei requisiti, perché abbiamo visto che ciò potrebbe
comportare il fallimento del progetto.
Lo sviluppo del software non è un processo sequenziale, perciò l’analisi dei requisiti continua attraverso tutto il
processo di sviluppo. Inoltre, non sempre è necessario scrivere anticipatamente tutti i requisiti.
Riscrivere i requisiti in ogni fase dello sviluppo potrebbe essere utile. Tuttavia, l’individuazione di alcuni requisiti in
ritardo potrebbe portare alla necessità di ripensare il design, e di conseguenza potrebbe significare maggiori costi
in termini di tempo, sforamento dei tempi previsti, o anche la cancellazione del progetto.
I requisiti non dovrebbero mai essere considerati fissi. Il loro cambiamento è inevitabile, e perciò deve essere
pianificato. È inoltre opportuno individuare un modo per inserire periodicamente i cambiamenti.
Come derivare i requisiti dagli obiettivi?
Vediamo ora mediante un semplice esempio come è possibile ricavare i requisiti dagli obiettivi. L’esempio fa
riferimento ad un tornello per il controllo dell’ingresso ad uno zoo.
Passo 1: descrizione di natura optativa degli obiettivi
Per prima cosa, si fornisce una descrizione di natura optativa degli obiettivi. In particolare, in questo caso si tratterà di
obiettivi di safety (che mirano cioè a fare in modo che non accada nulla di male all’interno del sistema):
Goal 1: In ogni istante di tempo, il numero di persone entrate attraverso il tornello non deve superare il numero di
monete depositate.
Goal 2: Il sistema non deve impedire l’accesso a chi inserisce nel tornello la moneta necessaria per entrare.
40
Ingegneria del Software 2
Capitolo 5: Ingegneria dei requisiti
Passo 2: analisi del dominio
A questo punto, occorre identificare gli eventi rilevanti rispetto agli obiettivi posti. Nell’esempio che stiamo
analizzando, tali eventi saranno:
1.
2.
3.
4.
5.
L’evento push, che si verifica quando viene fatto ruotare il tornello.
L’evento lock, ovvero l’emissione del segnale che blocca il tornello.
L’evento unlock, cioè l’emissione del segnale che sblocca il tornello.
L’evento enter, che si verifica quando l’utente oltrepassa il tornello.
L’evento coin, che corrisponde all’inserimento di una monetina.
Dopodiché, è necessario stabilire quali di questi eventi sono condivisi tra la macchina e il mondo, e definire se sono
controllati dalla macchina oppure dal mondo.


Gli eventi push, enter e coin sono controllati dal mondo e osservati dalla macchina.
Gli eventi lock e unlock sono controllati dalla macchina e osservati dal mondo.
L’unico fenomeno non condiviso tra la macchina e l’ambiente è l’evento enter, perché la macchina non è in grado di
osservarlo.
Passo 3: asserzioni o proprietà di dominio
Il terzo passaggio è rappresentato dall’individuazione delle asserzioni o proprietà di dominio, che caratterizzano il
mondo reale.
D1:
Gli eventi push ed enter avvengono alternativamente, ed il primo a verificarsi è l’evento push.
D2:
L’evento push è sempre seguito dall’evento enter.
D3:
L’utente può generare l’evento push solo se la macchina non è in stato locked.
Passo 4: formulazione dei requisiti
A questo punto dobbiamo formulare i requisiti, partendo dagli obiettivi e dalle asserzioni di dominio, ricordando che
l’obiettivo ultimo è quello di ottenere un insieme di requisiti R tale che:
Ad esempio, nella situazione che stiamo analizzando, da Da D1 e G1 otteniamo:
In ogni momento t, se osserviamo prima dell’istante un numero
numero di eventi di coin, allora avremo
di eventi di push e un
.
Da cui ricaviamo i seguenti requisiti:
R1:
Gli eventi lock e unlock devono essere alternati e il primo a verificarsi deve essere l’evento lock.
R2:
Se il tornello è locked e si ha
(il numero di eventi di push e il numero di eventi di coin verificatisi fino a
quel momento sono tra loro uguali), allora il tornello non può diventare unlocked.
R3:
Se il tornello è unlocked e si ha
, la macchina deve essere portata in stato locked.
Il secondo goal verrà invece trasformato nel seguente requisito:
R4:
Se la macchina si trova in stato unlocked e
, la macchina non passa allo stato locked. Se invece la
macchina è in stato locked e
, allora la macchina deve passare allo stato unlocked.
Si può infine verificare che i requisiti, insieme alle proprietà di dominio assicurano che valgano i goal prefissati.
41
Capitolo 5: Ingegneria dei requisiti
Ingegneria del Software 2
La gestione del processo
Introduzione
Dopo la trattazione teorica che abbiamo finora portato avanti, passiamo ad analizzare le modalità attraverso le quali il
processo viene gestito nella pratica. Le attività che vengono svolte sono di due tipologie diverse:

Attività di requirements elicitation (ovvero raccolta dei requisiti)
Si tratta di tutte quelle attività che portano all’identificazione dei requisiti, e che comprendono:
a) L’identificazione degli attori.
b) L’identificazione degli scenari (cioè delle modalità di interazione tra le persone e il sistema nelle diverse
circostanze).
c) Identificazione dei casi d’uso (che avviene sulla base degli scenari e che rappresenta un’astrazione
corrispondente ad un certo insieme di scenari).

Attività di analysis (ovvero di analisi)
Questa seconda categoria di attività comprende tutte quelle azioni che portano ad ottenere una specifica.
Raccolta dei requisiti

Identificazione degli attori
Identificare gli attori significa individuare:
a)
b)
c)
d)
Quali gruppi di utenti verranno supportati dal sistema nel loro lavoro?
Quali gruppi di utenti riceveranno dei benefici (anche indiretti) dal sistema?
Quali gruppi di utenti interagiranno direttamente con il sistema?
Quali gruppi di utenti interagiranno con le funzionalità secondarie del sistema (come quelle di manutenzione
ed amministrazione)?
e) Quali elementi hardware e software interagiranno con il sistema?

Identificazione degli scenari
Uno scenario è una descrizione a parole di ciò che una persona fa e di come si deve comportare per utilizzare un
sistema informatico o un’applicazione. Si tratta in altri termini di una descrizione concreta e informale di una
singola funzionalità usata da un singolo attore.
Un esempio di scenario è il seguente:
Nome:
Lo studente Rossi si iscrive all’esame di I.S.2 con il professore Di Nitto.
Attori:
Rossi, Di Nitto.
Event flow: Rossi apre l’interfaccia, …
Si noti quindi che lo scenario descrive una singola istanza del tipo di azione in analisi, e non tutte le possibili
situazioni che si possono verificare, e si focalizza su uno specifico attore, non su tutte le categorie di attori, e
neppure su tutti gli attori di una specifica categoria.
Gli scenari possono essere di vario tipo. I principali sono:
a) Scenario As-Is
Si tratta di una descrizione del sistema così come si trova prima che si realizzi il nuovo sistema (descrive cioè
la situazione corrente).
b) Scenario visionario
Descrive ciò che accadrà quando il sistema verrà utilizzato e fa capire al committente come ci si aspetta che
il sistema funzionerà.
42
Ingegneria del Software 2
Capitolo 5: Ingegneria dei requisiti
Per identificare gli scenari si adottano delle euristiche, che prevedono che ci si pongano le seguenti domande:
a)
b)
c)
d)
Quali sono i tasks principali del sistema?
Quali dati verranno creati, visualizzati, memorizzati, modificati o rimossi dagli attori all’interno del sistema?
Quali sono i cambiamenti e gli eventi dei quali gli attori devono essere informati?
Di che tipo di scenario si tratta?
Per raccogliere tali dati, si possono utilizzare dei questionari da sottoporre agli utenti, oppure si può
semplicemente scegliere di osservare il mondo e/o di intervistare gli utenti del sistema.

Identificazione degli use-case
Una volta ottenuti gli scenari, occorre cercare di mettere in atto un meccanismo di astrazione che consenta di
ottenere i casi d’uso, ciascuno dei quali specifica tutte le situazioni che possono verificarsi nel tentativo di
accedere ad una certa funzionalità del sistema. Per ogni sistema si avrà un certo numero di use case, che
rappresentano le funzionalità principali e gli attori che tali funzionalità vedono o si aspettano di vedere.
Di conseguenza, le informazioni fondamentali che sono contenute in use case sono:
a)
b)
c)
d)
e)
f)
g)
Il nome dello use case.
L’elenco degli attori partecipanti.
La condizione di ingresso.
Il flusso degli eventi che caratterizzano lo use case.
La condizione d’uscita.
Le eccezioni e la loro gestione.
I requisiti speciali (es.: proprietà non funzionali, ma temporali, altri vincoli, …).
Si noti che nella definizione degli use-case è opportuno seguire alcune semplici regole:
1.
2.
3.
4.
5.
6.
7.
Ogni use case viene avviato da un attore esterno.
Ogni use case è identificato da un nome, che è rappresentato preferibilmente da un verbo in forma attiva.
Gli attori devono avere un nome che ne identifichi la tipologia.
I confini del sistema devono essere chiari e ben definiti.
Si devono distinguere i passi che devono essere compiuti dall’attore da quelli del sistema.
Devono essere chiare le relazioni causali tra i vari passi.
Uno use-case deve essere descritto in un massimo di 2 o 3 pagine.
In generale, nella definizione dello use case è opportuno iniziare dalla definizione del nome, passare poi
all’identificazione degli attori e solo in seguito occuparsi della descrizione del flusso degli eventi, utilizzando il
linguaggio naturale.
Analisi
Dopo l’attività di raccolta dei requisiti, è necessaria l’attività di analisi, che consiste nella formalizzazione delle
informazioni raccolte mediante dei diagrammi UML.
1.
2.
3.
4.
Gli use case vengono formalizzati mediante degli use-case diagrams.
Le entità fondamentali vengono formalizzate mediante dei class diagrams.
Le relazioni tra gli use case e gli oggetti reali vengono mappate mediante dei sequence diagrams,
I comportamenti che dipendono da uno stato vengono rappresentati tramite degli statecharts.
Nel capitolo successivo introdurremo in maniera formale la notazione UML.
43
Capitolo 6: UML
Ingegneria del Software 2
Capitolo 6: UML
Il concetto di modello
Per prima cosa, riprendiamo brevemente il concetto di modello, che è uno dei concetti fondamentali dell’ingegneria e
che è già stato utilizzato in passato nel corso della nostra trattazione.
Definizione
Un modello è una rappresentazione di qualcosa, mediante un certo mezzo. Il modello cattura gli aspetti importanti di
ciò che esso stesso rappresenta e semplifica o omette gli altri aspetti. Il modello è dunque la proiezione di qualcosa in
qualcos’altro, realizzato partendo dall’osservazione della realtà, individuando gli elementi di interesse e proiettando
solo tali elementi all’interno di una rappresentazione della realtà di partenza.
Caratteristiche del modello
In un modello vengono associati agli elementi della realtà delle opportune astrazioni del modello . Mediante una
funzione d’interpretazione si può passare dagli oggetti reali alle relative astrazioni. Alle relazioni tra elementi della
realtà corrispondono delle relazioni
tra le relative astrazioni.
Se il modello è ben costruito, il seguente diagramma sarà commutativo:
Figura 17: Diagramma che rappresenta un buon modello
Scopo del modello
I modelli vengono utilizzati perché consentono di astrarre dai dettagli della realtà, in modo tale da poter ottenere
conclusioni complesse nella realtà mediante dei semplici passi all’interno del modello.
I modelli consentono inoltre di ottenere una comprensione del passato o del presente e di effettuare delle previsioni
riguardanti il futuro.
Linguaggio di modellazione
Un linguaggio di modellazione è un linguaggio artificiale che viene utilizzato per esprimere informazioni o conoscenze
o per rappresentare sistemi secondo una struttura che è definita mediante un insieme consistente di regole.
Il linguaggio di modellazione è caratterizzato da una particolare notazione e da una certa semantica, che attribuisce un
significato alle “frasi” di tale linguaggio (ovvero ai modelli espressi nel linguaggio stesso).
Semantica e notazione
La semantica cattura il significato di un modello. Essa viene utilizzata per generare il codice a partire dal modello
stesso, ma anche per valutare il modello, per controllarne la validità, … .
La notazione è invece l’insieme di regole che permettono di scrivere il modello, consentendo cioè di organizzare le
informazioni semantiche in modo che possano essere visualizzate ed editate.
44
Ingegneria del Software 2
Capitolo 6: UML
Aspetti critici derivanti dall’uso dei modelli
Nell’utilizzare i modelli si deve però tener conto anche di alcuni possibili problemi:
1.
2.
3.
4.
5.
L’astrazione può a volte scontrarsi con la necessità di esprimere molti dettagli; bisogna perciò scegliere con molta
cura, durante la stesura del modello, quali sono gli aspetti che devono essere ignorati e quali no.
Il modello può descrivere sia ciò che il sistema deve fare (modello per la specifica), sia come lo deve fare (modello
per l’implementazione). Bisogna scegliere con criterio quale tipo di modello usare.
I modelli sono delle descrizioni, e non delle istanze (che vengono invece usate come esempi).
Le diverse viste di uno stesso modello devono essere coerenti.
Talvolta diverse interpretazioni di un modello sono possibili. In questo caso bisogna intervenire per specificare
qual è l’interpretazione corretta.
I modelli dei sistemi software
A questo punto possiamo vedere come il concetto di modello possa essere utilizzato per i nostri scopi.
Modello di un sistema software
Un modello di un sistema software è una rappresentazione del sistema, realizzata secondo uno specifico punto di
vista, espressa in un opportuno linguaggio di modellazione e più semplice da gestire rispetto al sistema reale (almeno
per quanto riguarda uno specifico scopo).
Perché utilizzare dei modelli software?
Nell’ingegneria del software, i modelli vengono utilizzati per una serie di scopi diversi:
1.
2.
3.
4.
5.
Per catturare e indicare in maniera precisa i requisiti e le conoscenze di dominio.
Sono un supporto alla fase di design (progettazione dell’architettura) del sistema software.
Danno una visione semplificata di sistemi complessi.
Permettono di valutare e simulare il comportamento di un sistema.
Vengono usati per generare una serie di diverse configurazioni possibili di un sistema.
UML: concetti generali
Che cos’è UML?
L’UML (acronimo di Unified Modelling Language) è un linguaggio di modellazione grafico a carattere generale (non è
destinato solo all’Ingegneria del software), utilizzato per la specifica, la progettazione, la visualizzazione e la
documentazione di sistemi discreti (soprattutto di tipo software).
UML è stato progettato in modo da essere supportato da tool software appositi. Esso ha una struttura statica che
consente però di rappresentare un comportamento dinamico.
Le diverse viste supportate da UML
UML comprende 13 diverse tipologie di diagrammi, che possono essere classificati in:

Diagrammi (o viste) strutturali
Si tratta di diagrammi che descrivono la struttura del sistema. Appartengono a questa categoria i Class diagrams,
gli Object diagrams, i Component diagrams, … .

Diagrammi (o viste) comportamentali
Si tratta di diagrammi che descrivono il comportamento del sistema. Appartengono a questa categoria gli State
diagrams, gli Activity diagrams, gli Use case diagrams, ... .

Diagrammi (o viste) interattivi
Descrivono come si interagisce col sistema. Ad esempio, i Sequence diagrams appartengono a questa categoria.
Si tratta in realtà di particolari diagrammi comportamentali.
45
Capitolo 6: UML
Ingegneria del Software 2
La tassonomia dei diagrammi UML è rappresentata dalla seguente figura:
Figura 18: Tassonomia dei diagrammi UML
Come utilizzare UML?
UML può essere utilizzato fondamentalmente in 2 modi diversi, anche se è opportuno evidenziare che la distinzioneè
spesso molto confusa:
UML come strumento per realizzare abbozzi (sketch)
In questo caso, la caratteristica fondamentale che viene sfruttata è quella della selettività: si realizzano dei diagrammi
UML che rappresentano solo delle porzioni del codice che si andrà a realizzare, al fine di comunicare, spesso in modo
informale e dinamico, le diverse idee e alternative a disposizione. Questo è l’uso di UML che viene più
frequentemente fatto.
UML come strumento per realizzare modelli dettagliati (blueprint)
In altri casi, i diagrammi UML vengono realizzati cercando di ottenere la massima completezza, al fine di realizzare un
modello che fornisca una descrizione dettagliata comprendente tutte le decisioni che sono state prese. In alcuni casi si
sceglie di utilizzare UML in questo modo solo per particolari aree dell’intero progetto. Se si adotta questo approccio, la
programmazione verrà poi ridotta ad una procedura quasi meccanica.
I class diagram
Che cosa sono i class diagram?
I diagrammi delle classi (o class diagram) sono i diagrammi base di UML. Esso sono strettamente legati ai concetti della
programmazione orientata agli oggetti, e vengono usati per rappresentare in maniera le classi che costituiscono il
software, con i relativi attributi ed operazioni; tali classi verranno poi istanziate in oggetti. Il loro obiettivo è quello di
fornire una rappresentazione statica del sistema. I diagrammi delle classi possono anche essere usati per
rappresentare dei concetti non di tipo software, e comunque non hanno legami con un particolare linguaggio di
programmazione.
Il diagramma delle classi specifica, mediante le associazioni, i vincoli che legano tra loro le classi. Si osserva inoltre che
può essere definito a diversi livelli (analisi, disegno di dettaglio).
46
Ingegneria del Software 2
Capitolo 6: UML
Rappresentazione delle classi
La classe
Una classe viene rappresentata all’interno di un class diagram mediante un rettangolo, che è a sua volta suddiviso in
tre parti, come mostrato in figura:
Figura 19: Rappresentazione di una classe all’interno di un class diagram
Il nome permette di identificare l’elemento del quale stiamo parlando, gli attributi consistono nell’elenco degli
elementi che ci consentono di definire i singoli oggetti appartenenti a tale classe e i metodi descrivono il loro
comportamento. La rappresentazione della classe può anche essere accompagnata da un commento, riportato a lato,
all’interno di un riquadro che rappresenta in maniera stilizzata un foglietto. Ad esempio, potremo avere:
Figura 20: Esempio di classe rappresentata mediante la notazione dei class diagram
Accanto al nome della classe possiamo anche indicare in parentesi graffe le sue proprietà (ad esempio, possiamo
indicare abstract se si tratta di una classe astratta).
Gli attributi
Come mostrato nel precedente esempio, gli attributi vengono elencati con un particolare formato, che è il seguente:
<Visibilità> <Nome Attributo>: <Tipo Attributo> <molteplicità> = <Valore di default> {<Stringa di proprietà>}
Dove il campo <Visibilità> può assumere i valori indicati in tabella, con il significato riportato a lato:
Notazione
–
+
#
~
Significato
Indica che l’attributo ha visibilità private.
Indica che l’attributo ha visibilità public.
Indica che l’attributo ha visibilità protected.
Indica che l’attributo ha visibilità friendly.
Tabella 1: Notazione adottata per indicare la visibilità all’interno dei class diagram UML
La stringa di proprietà può ad esempio assumere valori come changeable, froze, addonly, … . La molteplicità può
invece assumere dei valori del tipo [0..1], per indicare che la molteplicità è compresa tra 0 e 1, [1], per indicare che la
molteplicità è uno (se lo si omette, si considera sottinteso che si abbia [1]), oppure [*], che significa che la molteplicità
è 0, 1, o un qualsiasi numero maggiore di 1.
Il significato dei restanti campi è invece molto intuitivo. Naturalmente, se non si ha alcun valore di default, la parte ad
esso relativa viene semplicemente omessa.
È invece opportuno osservare che tramite la notazione UML è anche possibile mettere in evidenza quando un
attributo è costante: in tal caso infatti l’attributo è sottolineato.
47
Capitolo 6: UML
Ingegneria del Software 2
I metodi (o operazioni)
Il formato attraverso il quale vengono specificati i metodi è invece il seguente:
<Visibilità> <Nome Metodo>(<Lista di Parametri>): <Tipo di ritorno> {Stringa di proprietà}
Dove il campo <Visibilità> può assumere gli stessi valori (e con lo stesso significato) che abbiamo già elencato quando
abbiamo descritto la notazione per gli attributi. Gli altri campi hanno invece un significato intuitivo. È però bene
descrivere in maniera dettagliata il formato attraverso il quale viene specificata la lista di parametri: i parametri sono
separati da virgola, e per ciascuno di essi scriveremo:
<Direzione> <Nome Parametro> : <Tipo Parametro>
Il campo <Direzione> è facoltativo, e può assumere i valori in, out oppure inout (con ovvio significato). Quando tale
campo è omesso, si sottintende che il parametro sia semplicemente un parametro in input.
Le relazioni
Attraverso i class diagram possono anche essere messe in evidenza le relazioni tra le classi. Tali relazioni sono di due
tipi: generalizzazione e associazione. Particolari tipi di associazione sono poi la composizione e l’aggregazione.
Generalizzazione
Rappresentazione della generalizzazione
Il concetto UML di generalizzazione corrisponde a quello di ereditarietà, tipico dei linguaggi di programmazione ad
oggetti. La generalizzazione si rappresenta in UML come mostrato nell’esempio seguente:
Figura 21: Esempio di generalizzazione
Dove si indica che Triciclo eredita da Bicicletta e che Bicicletta eredita da VeicoloTerrestre e da VeicoloAPedali.
Sappiamo che quest’ultima situazione in Java non può esistere, perché non si ha ereditarietà multipla; occorre tuttavia
ricordare che UML non è legato a Java e che in altri linguaggi di programmazione è possibile realizzare l’ereditarietà
multipla.
Nelle classi che ereditano da un’altra, naturalmente, verranno indicati solamente metodi ed attributi aggiuntivi e non
quelli ereditati dalla superclasse (in quanto sono impliciti).
Rappresentazione della interfacce
Le interfacce in UML possono essere rappresentate in due diversi modi. Il primo modo prevede semplicemente che si
usi la stessa rappresentazione adottata per le classi, ma escludendo la parte relativa agli attributi e aggiungendo la
scritta <<interface>> appena prima del nome, come nel seguente esempio:
Figura 22: Esempio di rappresentazione di interfacce mediante una delle possibili notazioni UML
48
Ingegneria del Software 2
Capitolo 6: UML
Come mostrato nell’esempio, l’implementazione di una classe è indicata così come la generalizzazione (ereditarietà),
ma con linea tratteggiata. Analogamente, le associazioni che coinvolgono l’interfaccia (come quella tra
FiguraGeometrica e List) sono indicate con una linea tratteggiata.
Esiste poi una seconda rappresentazione semplificata, che è la seguente, e che è stata introdotta più tardi rispetto alla
prima, con l’obiettivo di sostituirla:
Figura 23: Esempio di rappresentazione di interfacce mediante una delle possibili notazioni UML
Tale rappresentazione però è meno chiara, perché non indica esplicitamente i metodi definiti nell’interfaccia.
Associazioni
Le associazioni sono delle relazioni tra classi, di qualsiasi tipo purché non di generalizzazione. Ad esempio, possiamo
pensare ad una classe Persona e ad una classe Azienda: è possibile che una persona sia messa in relazione all’azienda
perché lavora per essa, perciò avremo un’associazione tra Persona e Azienda.
Come rappresentare le associazioni
Le associazioni vengono rappresentate mediante delle linee che congiungono le classi interessate dall’associazione
stessa. Su tale linea viene inoltre indicata una parola per descrivere la tipologia di associazione (solitamente un verbo:
nel precedente esempio, potremmo usare “lavorare”).
Inoltre, si può opzionalmente indicare una descrizione dei ruoli svolti dalle singole classi all’interno dell’associazione,
mediante un nome (come mostrato nella figura seguente, dove tali termini sono “dipendenti” e “azienda”).
Gli estremi della classe prevedono che si indichi una cardinalità (o molteplicità): su ogni estremo troveremo
un’indicazione del tipo: 1 (esattamente 1), oppure 0..1 (zero o 1), oppure 1..*, (uno o più), 0..* (zero, uno o più di
uno), n (dove n è un numero qualsiasi, indica esattamente n), n1-n2 (dove n1 e n2 sono numeri qualsiasi, purché n2 >
n1: indica minimo n1, massimo n2). La figura seguente mostra un esempio.
Figura 24: Esempio di utilizzo della relazione di associazioni tra classi in un UML class diagram
Nel precedente diagramma, le cardinalità indicano che zero o più persone possono lavorare per una stessa azienda,
ma una persona lavora esattamente per un’azienda (non ci sono disoccupati!).
È possibile anche che ci sia un’associazione riflessiva (come nell’esempio a lato), ovvero nella quale entrambe le
estremità ricadono sulla stessa classe.
Figura 25: Esempio di associazione riflessiva in un UML class diagram
49
Capitolo 6: UML
Ingegneria del Software 2
Come si traducono le associazioni: l’uso delle frecce
Le associazioni si traducono concretamente attraverso l’inserimento di opportuni attributi. In particolare, ogni
estremo è un “attributo implicito”: se la cardinalità è 0..1 oppure 1, allora l’attributo sarà semplicemente una variabile
di tipo di riferimento all’altra classe interessata dall’associazione, altrimenti si tratterà di un array o di una collezione.
Tuttavia, inserire un attributo aggiuntivo da entrambe le parti è ridondante: ciò potrebbe comunque rivelarsi comodo,
per facilitare ad esempio le operazioni di ricerca, ma talvolta lo si vuole evitare. In questo caso, si inserisce sulla linea
che rappresenta l’associazione una freccia. La classe verso la quale è rivolta la freccia non conterrà attributi aggiuntivi
per memorizzare l’associazione.
Ad esempio:
Figura 25: Esempio di traduzione di un’associazione all’interno di un class diagram in codice Java
Le aggregazioni e le composizioni
Le aggregazioni
Come accennato, le aggregazioni sono una forma particolare di associazione; in particolare, si tratta di associazioni
nelle quali una classe è in relazione con un’altra perché una di esse rappresenta una parte dell’altra. Diciamo quindi
che si mettono in relazione un oggetto con una sua parte.
In sintesi quindi le aggregazioni servono per indicare quando un oggetto è costruito da altri oggetti.
Le aggregazioni vengono rappresentate mediate delle linee (come le normali associazioni), alle quali però non è
associato un verbo (perché è scontato il tipo di relazione tra le due classi); si utilizza però un rombo bianco in
prossimità della classe che rappresenta l’oggetto completo. Si indicano comunque le cardinalità, come nelle normali
associazioni.
Figura 26: Esempio di aggregazione
50
Ingegneria del Software 2
Capitolo 6: UML
Le composizioni
Le composizioni poi sono un caso particolare di aggregazione: una composizione è infatti un’aggregazione forte,
ovvero un’aggregazione nella quale le parti componenti non esistono senza il contenitore. Ciò significa che la
creazione e la distruzione dei componenti avvengono all’interno del contenitore e che i singoli componenti non
possono essere parti di altri oggetti. Quindi, se si distrugge una parte, non si distrugge il tutto, ma se si distrugge il
tutto, si distruggono anche tutte le parti.
In Java le composizioni di fatto non vengono usate. In ogni caso, aggregazioni e composizioni vengono poi tradotte allo
stesso modo al momento di stendere il codice.
La rappresentazione di una composizione è del tutto analoga a quella dell’aggregazione, con l’unica differenza che il
rombo è in questo caso annerito.
Figura 27: Esempio di composizione
I package
In UML, così come in Java, esiste il concetto di package. Un package è un meccanismo generale per organizzare degli
elementi in gruppi omogenei. Un package può contenere altri package, e inoltre possono esserci delle relazioni tra i
vari package.
Si osserva però che il concetto di package in UML è diverso da quello in Java: in UML possiamo infatti instaurare anche
relazioni di dipendenza e di generalizzazione (ereditarietà) tra package. Ad
Di seguito è riportato un esempio di rappresentazione di package, dove sono mostrate anche una relazione di
generalizzazione e una di dipendenza (tipicamente ciò significa che il package JavaCompiler accede al package Java).
Figura 28: Esempio di uso dei package
Componenti
I componenti sono invece utili per decomporre il sistema in esame. Essi sono così rappresentati:
Figura 29: Esempio di uso dei componenti
I “quadrati” ai margini del componente rappresentano delle porte, che implementano delle interfacce e dei protocolli
opportuni per la comunicazione con l’esterno.
51
Capitolo 6: UML
Ingegneria del Software 2
Object diagram
Gli object diagram sono molto simili ai class diagram, ma prevedono che si risolvano le molteplicità, scegliendo cioè il
numero di oggetti da istanziare. Gli oggetti vengono rappresentati mediante dei rettangoli. All’interno del rettangolo è
indicato il nome dell’istanza, seguito dal suo tipo, separato dal nome mediante i due punti.
Figura 30: Esempio di object diagram
Use case diagram
Cosa sono?
I diagrammi dei casi d’uso (use case diagram) sono dei diagrammi che definiscono le funzionalità offerte dal sistema e
le modalità attraverso le quali il sistema agisce e reagisce.
In particolare, i diagrammi dei casi d’uso descrivono:
1.
2.
3.
Il sistema;
L’ambiente;
Le relazioni tra il sistema e l’ambiente.
Lo use case diagram viene realizzato prima del class diagram, al fine di catturare i requisiti e le funzionalità necessarie
(indicando però solo quelle visibili). Il sistema può essere definito a diversi livelli di granularità.
In particolare, lo use case diagram consente di:
1.
2.
3.
4.
Organizzare i requisiti in macro-funzionalità.
Definire relazioni tra le diverse funzionalità.
Definire il ruolo assunto dai vari attori.
Definire le interazioni tra gli utenti e il sistema.
La terminologia usata in questi diagrammi è la terminologia del dominio applicativo, non quella del sistema
informatico.
Gli elementi dello use case diagram
Sulla base di quanto abbiamo appena detto, uno use case diagram è costituito dai seguenti elementi:
1.
2.
3.
4.
5.
52
Il sistema;
Gli use case;
Gli attori;
Le relazioni tra gli attori e gli use case;
Le relazioni tra use case e use case.
Ingegneria del Software 2
Capitolo 6: UML
Il sistema e gli use case
Gli use case elencano le funzionalità a disposizione, che vengono fornite dal “sistema”. In uno use case diagram il
sistema è identificato da un rettangolo che racchiude gli use case relativi alle funzionalità fornite. Il rettangolo è
accompagnato da una label, all’interno della quale è indicato il nome del sistema stesso.
SISTEMA
Ambiente
esterno
Figura 31: Rappresentazione del sistema all’interno di uno use case diagram
Il rettangolo che rappresenta il sistema quindi separa ciò che è all’interno del sistema stesso da ciò che invece
appartiene al mondo esterno.
All’interno del sistema vengono indicati gli use case, ciascuno dei quali rappresenta una particolare funzionalità messa
a disposizione dal sistema stesso. Il punto di vista con il quale devono essere definiti gli use case è sempre quello
dell’utilizzatore. Ogni use case è rappresentato mediante un ovale all’interno del quale viene indicato il nome dello
use case stesso.
SISTEMA
nome use case
Figura 32: Rappresentazione di uno use case all’interno di uno use case diagram
Attori
Come accennato, gli attori sono gli utilizzatori del sistema. Ogni attore è rappresentato mediante il seguente simbolo,
con un’etichetta nella quale è riportato il nome (o una descrizione) dell’attore stesso:
Attore
Figura 33: Rappresentazione di un attore
Più nel dettaglio, possiamo dire che un attore è un’entità esterna (una persona, ma potrebbe anche essere un sistema
hardware esterno) che interagisce con il sistema, assumendo un certo ruolo e che:
1.
2.
Controlla le funzionalità del sistema informativo;
Fornisce input o riceve output dal sistema.
Da quanto detto, consegue che:
1.
2.
Possono esserci diversi attori con lo stesso ruolo, ma anche diversi ruoli con lo stesso attore;
Diversi attori possono esercitare su uno stesso use case.
Gli attori possono essere il mezzo per individuare gli use case (si individua cioè una lista di attori). Nell’interazione tra
attore e sistema, l’attore può rivestire un ruolo attivo (cioè richiede delle funzionalità) oppure passivo (cioè è il
sistema che, per svolgere una sua funzione, ha bisogno dell’attore).
53
Capitolo 6: UML
Ingegneria del Software 2
Relazioni tra gli elementi di uno use case diagram
Abbiamo introdotto già tutti gli elementi che possono comparire in uno use case diagram. Tuttavia, la parte forse più
significativa di uno use case diagram è rappresentata dai legami che esistono tra tali elementi, e che possono essere di
tre tipi:
1.
2.
3.
Legami tra diversi use case dello stesso sistema;
Legami tra uno use case ed un attore;
Legami tra attori.
Legami tra attori e use case: associazione
Iniziamo analizzando il modo in cui possono essere legati tra loro un attore ed uno use case. Tale relazione, detta
associazione, è una relazione di “uso”: l’attore nei confronti dello use case può essere il beneficiario, il controllore,
può essere informato, … .
L’associazione si rappresenta mediante un semplice segmento:
SISTEMA
Attore
nome use
case
Figura 34: Rappresentazione di un’associazione tra un attore e uno use case
L’associazione può anche avere un verso, che è rappresentato orientando il segmento mediante una freccia. In
particolare, la freccia va verso lo use case se l’attore è il soggetto attivo dell’associazione (cioè è l’attore a richiedere la
funzionalità), mentre ha verso opposto se l’attore è soggetto passivo (cioè è lo use case che richiede l’intervento
dell’attore). Ad esempio:
PRENOTAZIONE VIAGGIO
Ricercare
viaggio
Prenotare
viaggio
Cliente
Pagare
viaggio
Figura 35: Rappresentazione di un’associazione orientata tra un attore e uno use case
54
Ingegneria del Software 2
Capitolo 6: UML
Legami tra diversi use case
Nel precedente esempio, abbiamo definito un sistema con 3 funzionalità completamente scollegate tra loro, senza che
sia definito alcun ordine tra di esse (si noti che negli use case non interviene mai la variabile tempo). Tuttavia, tale
situazione non modella efficacemente la realtà. Possiamo allora introdurre delle relazioni tra gli use case, le quali
mettano in evidenza dei legami tra di essi. Questi tipi di legami possono essere di vario tipo:
Inclusione
L’inclusione, rappresentata dallo stereotipo «includes», si rappresenta come:
SISTEMA
Nome use
case 1
«includes»
Nome use
case 2
Figura 36: Rappresentazione di una relazione di inclusione tra due use case
In questo tipo di legame, lo use case dal quale ha origine la freccia ha bisogno, per poter essere completato,
dell’esecuzione della funzionalità descritta dallo use case al quale arriva la freccia. In altri termini, lo use case
“sorgente” utilizza quello “di arrivo”.
La relazione di inclusione è stata introdotta perché spesso esistono use case che rappresentano delle attività
ricorrenti, condivise tra use case più complessi; per evitare di ripetere in ogni use case la descrizione dell’attività
comune, la si mette a fattor comune, indicando che viene inclusa in uno o più use case più complessi (il meccanismo è
quindi analogo a quello della chiamata ad una sottoprocedura).
Un altro motivo per il quale l’inclusione viene utilizzata è che la funzionalità corrispondente ad uno use case è troppo
complessa per essere risolta in maniera immediata, perciò viene tale funzionalità viene descritta come l’aggregazione
tra un insieme di funzionalità più semplici. Tale procedimento è detto decomposizione funzionale. Gli use case
corrispondenti alla funzionalità originaria e alle sue sottofunzionalità sono legati perciò da una relazione di inclusione.
Figura 37: Esempio di utilizzo della relazione di inclusione
Estensione
La relazione di estensione tra due use case si verifica quando si dispone già di uno use case, che però deve essere
“esteso”, mediante cioè l’aggiunta di ulteriori operazioni da compiere. Lo use case di partenza è completo di per sé,
ma in alcuni scenari necessita di essere esteso come mostrato dallo use case che lo estende. Di conseguenza, risulta
chiaro che lo use case di base può essere eseguito anche senza l’esecuzione dell’altro use case. L’estensione
rappresenta quindi solo una variazione dal comportamento standard.
55
Capitolo 6: UML
Ingegneria del Software 2
L’estensione è rappresentata dallo stereotipo «extends». Si noti che lo use case di base deve fornire dei “punti di
estensione” e che lo use case di estensione può aggiungere comportamento a quello di base solo per quanto riguarda
le funzionalità del punto di estensione.
Figura 38: Esempio di utilizzo della relazione di estensione
Per comprendere il concetto di punto di estensione, consideriamo la seguente figura:
«extends»
1
2
A
A
B
B
C
C’
Figura 39: Estensione e punto di estensione
In questo caso, abbiamo un oggetto 1 e un oggetto 2, il cui comportamento è diverso. Tuttavia, entrambi hanno lo
stesso obiettivo, solo che 2 lo raggiunge in maniera parzialmente diversa rispetto ad 1 (anziché eseguire C, esegue C’).
Diciamo quindi (in base a come abbiamo orientato la freccia) che 2 è l’estensione di 1, mentre 1 è l’esteso. In UML
dovremmo rappresentare questa situazione nella maniera seguente:
SISTEMA
1
C
«extends»
C’
Figura 40: Rappresentazione completa di una relazione di estensione tra use case
Dove 1 è il nome dello use case, mentre C è il punto di estensione (cioè è l’elemento che può essere sostituito).
Tuttavia, per maggiore semplicità, si usa spesso la notazione seguente, in cui si finge che il punto di estensione
coincida con l’intera attività.
1
SISTEMA
«extends»
2
Figura 41: Rappresentazione semplificata di una relazione di estensione tra use case
56
Ingegneria del Software 2
Capitolo 6: UML
Generalizzazione tra use case
È anche possibile definire una relazione di generalizzazione tra due use case. In questo caso, lo use case figlio eredita
tutti gli attributi, scenari… definiti nello use case padre (ricalca il concetto di ereditarietà della programmazione ad
oggetti). Inoltre, lo use case figlio partecipa a tutte le relazioni in cui è coinvolto lo use case padre e può prevedere
nuovi scenari non previsti nello use case padre. Uno use case può ereditare da n altri use case e può essere il padre di
n altri use case.
La generalizzazione si utilizza quando si identifica un comportamento comune tra due use case e lo si vuole
fattorizzare. Di seguito è mostrato un esempio di generalizzazione.
CheckPassword
ValidateUser
CheckFingerprint
Figura 42: Esempio di generalizzazione tra use case
Legami tra attori: generalizzazione
L’unico possibile legame tra due attori è quello di generalizzazione. La generalizzazione tra attori si rappresenta nel
modo seguente:
Padre
Figlio
Figura 43: Rappresentazione della generalizzazione tra attori
L’attore figlio eredita tutte le caratteristiche dell’attore padre e può partecipare a tutti gli use case a cui partecipa
l’attore padre.
Descrizione degli use case
Oltre al semplice diagramma nel quale sono indicati tutti gli specifici casi d’uso, è necessario indicare, per ogni use
case,una relativa descrizione, con associati ad essa uno o più scenari. Le informazioni che devono essere elencate
all’interno della descrizione dello use case sono state già discusse nel precedente capitolo.
57
Capitolo 6: UML
Ingegneria del Software 2
Sequence diagram
Cosa sono?
I sequence diagram (diagrammi di sequenza) sono dei particolari interaction diagrams di UML, i quali consentono,
come suggerisce il nome stesso, di rappresentare l’interazione tra oggetti, materializzando così degli scenari specifici.
In particolare, essi evidenziano il modo in cui uno scenario (ovvero uno specifico percorso in un caso d’uso) viene
risolto dalla collaborazione tra un insieme di oggetti. Per fare, ciò, specifica la sequenza dei messaggi scambiati tra gli
oggetti. I diagrammi di sequenza possono specificare nodi decisionali e iterazioni.
Da quanto appena detto segue che in questo caso, a differenza di quanto accade con gli use case diagram, si parla di
istanze specifiche del sistema e ci si sofferma ad analizzare la cooperazione non tanto tra categorie di elementi, ma tra
specifici partecipanti (oggetti, o attori, …).
Tali diagrammi sono utili per vari motivi:
1.
2.
3.
In fase di analisi, consentono di modellare un flusso di eventi all’interno di uno use casse.
In fase di design, permetto dono di identificare quali partecipanti faranno parte della soluzione finale.
In fase di validazione, servono per validare l’interazione tra i partecipanti scelti.
I diagrammi di sequenza sono di tipo bidimensionale: orizzontalmente vengono rappresentati i partecipanti
Come si realizzano i diagrammi di sequenza
I diagrammi di sequenza prevedono che ci sia una “linea verticale”, che rappresenta la linea del tempo, mentre in
orizzontale sono disposti i vari oggetti. Oltre agli oggetti, viene rappresentato anche l’utente (il cliente), mediante la
stessa notazione adottata per rappresentare gli attori in uno use case diagram.
Quando ad un oggetto corrisponde una linea verticale tratteggiata, significa che quell’oggetto esiste, ma è passivo
(cioè non fa nulla); se invece la linea verticale è doppia, significa che l’oggetto è attivo. Infine, se la linea non è
presente, significa che l’oggetto non è ancora stato creato, oppure è stato distrutto (sappiamo che in Java gli oggetti
non vengono realmente distrutti, perché ci penserà poi il garbage collector, però possiamo considerare tale istante, a
livello logico, come l’istante in cui si perde il riferimento a quell’oggetto).
Le interazioni tra oggetti vengono rappresentate mediante frecce, sulle quali è riportato un nome o un verbo che
specifica il tipo di interazione. Il significato delle frecce è descritto in tabella:
Tipo di freccia
Significato
Invio di un messaggio sincrono.
Invio di un messaggio asincrono.
Invio di una risposta ad un messaggio.
X
partecipante
Invio di un messaggio di distruzione di un oggetto.
Invio di un messaggio di creazione di un oggetto.
Tabella 2: Significato dei diversi tipi di freccia che si presentano all’interno di un sequence diagram
58
Ingegneria del Software 2
Capitolo 6: UML
Ad esempio, se vogliamo rappresentare la sequenze per la prenotazione di un volo, disegneremo il diagramma
mostrato in figura 44. La figura 45 mostra un secondo esempio di sequence diagram.
Figura 44: Esempio n. 1 di sequence diagram
Figura 45: Esempio n. 2 di sequence diagram
Potrebbe però rivelarsi utile anche definire dei cicli, oppure delle alternative, … . Per tale scopo sono stati introdotti i
frame d’interazione, che consentono appunto di specificare questo tipo di situazioni.
Alcuni dei più usati frame di interazione sono i seguenti:
alt
opt
loop
Indica un’alternativa del tipo (o … o): se è verificata una condizione si fa una certa cosa, altrimenti se ne
fa un’altra.
Indica un’azione opzionale, che viene cioè svolta solo se è verificata una certa condizione (in caso
contrario non si fa nulla).
Indica l’iterazione di una certa operazione. La condizione di iterazione è espressa da una descrizione a
parole affiancata alla parola loop.
ref
Indica un’operazione che è descritta all’interno di un diverso diagramma.
par
Indica che più frammenti vengono eseguiti in parallelo. Il frame è perciò diviso a metà.
neg
Serve per evidenziare un’interazione non valida.
Tabella 3: Elenco dei principali frame usati nei sequence diagram
All’interno di ogni frame si specifica poi a parole una breve descrizione che indica la condizione entro la quale certe
operazioni vengono eseguite e/o iterate. Ad esempio, possiamo usare i frame di interazione per correggere il
precedente esempio in modo tale che si eviti che nell’intervallo tra la verifica di disponibilità e l’effettiva prenotazione,
la disponibilità dell’auto venga a mancare: tale situazione creerebbe ovviamente un errore. Possiamo allora disegnare
il seguente diagramma di sequenza:
Figura 46: Esempio di utilizzo dei frame all’interno dei sequence diagram
59
Capitolo 6: UML
Ingegneria del Software 2
Activity diagram
Cosa sono?
Gli activity diagram sono molto simili ad un semplice diagramma di flusso (flow diagram): essi sono diagrammi
comportamentali che rappresentano un flusso di attività (come ci suggerisce il loro nome), cioè l’evoluzione del
workflow del sistema. In particolare, tali diagrammi vengono usati per descrivere il comportamento dinamico di un
sistema, fornendo la sequenza di operazioni che definiscono un’attività più complessa. Più precisamente, l’activity
diagram rappresenta il flusso di controllo di attività computazionali per l’esecuzione di una serie di calcoli o di un
workflow.
L’activity diagram può descrivere sia flussi di controllo interni al sistema, sia la comunicazione tra elementi diversi.
Inoltre, è in grado di mettere in evidenza i task eseguiti in parallelo e di descrivere i flussi di dati tra attività e oggetti.
Un activity diagram può essere associato:
1.
2.
3.
Ad una classe.
All’implementazione di una operazione.
Ad uno Use case.
Gli elementi fondamentali degli activity diagram sono le attività, le swim lane e le scelte (diamond).
Attività
Un’attività (activity) o azione (action) è un lavoro svolto da un oggetto in maniera continuativa. Le attività possono
essere:

Attività atomiche (action state)
Sono attività eseguibili in maniera atomica, cioè che non possono essere decomposte in una sequenza di attività,
né possono essere interrotte.

Attività non atomiche (activity state)
Sono attività non eseguibili in maniera atomica: possono cioè essere decomposte in una sequenza di attività e
possono anche essere interrotte durante la loro esecuzione. Un’attività di questo tipo può a sua volta essere
descritta mediante un activity diagram.
In entrambi i casi il simbolo usato è un rettangolo con gli angoli smussati:
Nome attività
Figura 47: Rappresentazione di un’attività all’interno di un activity diagram
Se l’attività è contraddistinta da un * in alto a sinistra, significa che l’attività può essere ripetuta più volte. L’attività ha
un inizio ed una fine, ed il controllo di tutto il flusso rimane all’attività per tutto il tempo che intercorre tra l’inizio
dell’attività e la sua fine (tempo che può anche essere molto lungo).
Il passaggio del controllo da un’attività ad un’altra è detto transizione. Ovviamente, la transizione avviene quando
termina l’attività sorgente e dà il controllo all’attività di destinazione. Ogni attività ha una transizione in ingresso ed
una transizione in uscita, entrambe rappresentate mediante frecce (quella in ingresso è una freccia entrante
nell’attività, quella in uscita è uscente):
Nome attività
Figura 48: Rappresentazione di un’attività e delle relative transizioni all’interno di un activity diagram
60
Ingegneria del Software 2
Capitolo 6: UML
Inizio e fine del diagramma
All’interno dell’activity diagram esistono anche due attività “fittizie”, che sono l’inizio e la fine del diagramma stesso.
L’inizio è rappresentato mediante un cerchio pieno, mentre la fine è rappresentata da un cerchio pieno all’interno di
un cerchio vuoto più grande. L’attività di inizio ha solo una transizione di uscita, mentre quella d’uscita ha solo una
transizione d’ingresso.
Figura 49: Attività di inizio dell’activity diagram
Figura 50: Attività di fine dell’activity diagram
Branch (divisione) & merge (fusion)
Un branch (o scelta, o diamond) è un punto a partire dal quale il flusso di controllo si divide in due o più cammini. Si
tratta quindi di un’attività particolare, con una sola transizione d’ingresso e almeno due transizioni d’uscita. Ad ogni
freccia d’uscita è associata una condizione. Tali condizioni devono essere mutuamente esclusive, e si deve sempre fare
in modo che esattamente una di tali condizioni sia vera. L’unica transizione che avviene veramente è quella associata
alla condizione verificata in quella particolare istanza del diagramma.
È anche possibile che un ramo d’uscita non abbia ad esso associata alcuna condizione (o che vi sia scritto else): in tal
caso, quel ramo viene eseguito quando nessuna delle altre condizioni risulta verificata.
Il branch è perciò una struttura del tutto analoga ad un if (ad una condizionale) in un linguaggio di programmazione. Il
simbolo usato è il seguente (nell’esempio abbiamo 2 cammini possibili).
Il branch deve poi essere seguito da un simbolo di fusione (merge). Il merge è il punto in cui i diversi cammini si
riuniscono. In pratica, quando arriva il primo percorso in ingresso al merge, si va avanti, passando il controllo
all’attività di destinazione dell’unica transizione d’uscita del merge stesso.
[A]
[else]
Figura 51: Rappresentazione di un branch nella notazione dei
sequence diagram
Figura 53: Rappresentazione di un merge nella notazione dei
sequence diagram
Di seguito sono riportati due esempi, il secondo dei quali mette in evidenza come sia possibile realizzare un’iterazione
utilizzando il branch & merge e una semplice transizione che “ritorna indietro”:
Figura 54: Esempio n. 1 di sequence diagram con branch
Figura 55: Esempio n. 2 di sequence diagram con branch
61
Capitolo 6: UML
Ingegneria del Software 2
Fork & Join
Il flusso può anche essere diviso mediante un altro tipo di costrutto, il fork. Tale costrutto però divide il flusso di
controllo in due o più flussi indipendenti (threads), che proseguono in parallelo. Tutti i flussi vengono quindi attivati (e
non solo uno, come accadeva con il branch), a patto che le eventuali condizioni ad essi associate siano verificate (le
condizioni non devono essere mutuamente esclusive, ma comunque devono essere costruite in modo che almeno una
di tali condizioni risulti essere vera).
[C1]
[C2]
Figura 56: Rappresentazione di un fork all’interno di un activity diagram
Ogni fork deve poi essere accompagnato da un simbolo di join, che rappresenta la sincronizzazione di due o più flussi
in uno solo. Il join prevede quindi che si attenda il completamento di tutti i processi sulle linee di ingresso (a patto che
siano stati attivati), e solo quando tutti sono giunti al simbolo di join, si procede con l’attività successiva. Il simbolo del
join è il seguente:
Figura 57: Rappresentazione di un join all’interno di un activity diagram
Si seguito sono riportati due esempi di uso del meccanismo fork/join:
Figura 58: Esempio n. 1 di sequence diagram con fork
62
Figura 59: Esempio n. 2 di sequence diagram con fork
Ingegneria del Software 2
Capitolo 6: UML
Swimlane
È possibile anche separare diverse swimlane in modo tale da separare le diverse responsabilità in relazione alle varie
operazioni da svolgere nel workflow. Le swimlane sono semplici divisioni verticali che corrispondono sostanzialmente
ad attori diversi del sistema. Nell’esempio in figura, si hanno due swimlane: Student e System.
Figura 60: Esempio di activity diagram con due swimlane
Segnali
Inoltre, all’interno degli activity diagram può essere rappresentato lo scambio di segnali. Ciò avviene mediante dei
simboli di attività particolari, come mostrati nelle figure seguenti:
Segnale
Segnale
Figura 61: Simbolo per l’invio di segnali nell’activity diagram
Figura 62: Ricezione di segnali nell’activity diagram
Activity diagram robusto
Un activity diagram si dice robusto se tutti i costrutti sono correttamente annidati. Un activity diagram robusto ha
sempre uno ed un solo simbolo di fine. Il precedente diagramma dovrebbe allora diventare:
Le figure seguenti mostrano un activity diagram non robusto ed il corrispondente diagramma robusto (dal punto di
vista semantico, i due diagrammi sono equivalenti).
Attività 1
Attività 1
[A]
[A]
[else]
[else]
Attività 2
Attività 2
Figura 63: Esempio di activity diagram non robusto
Figura 64: Esempio di activity diagram robusto
63
Capitolo 6: UML
Ingegneria del Software 2
Osservazioni sugli activity diagram e sulla divisione del flusso
Si noti che in realtà non sembre un branch è seguito da un merge, e non sempre un fork è seguito da un join: il
costrutto branch/merge e il fork/join possono infatti essere “mescolati”, come mostrato nei seguenti esempi:
A
A
[C1]
B
[else]
C
B
Figura 65: Esempio di activity diagram con fork e merge
C
Figura 66: Esempio di activity diagram con branch e join
Nell’esempio di sinistra, le attività B e C vengono avviate contemporaneamente e poi, appena termina la prima, si
termina l’intero processo. Nell’esempio di destra invece la semantica è ambigua: teoricamente infatti si dovrebbe
attendere la terminazione di entrambi i rami in ingresso al join, ma questo equivale ad attendere per un tempo
infinito (essendoci infatti un branch, solo uno di tali rami verrà attivato). Si potrebbe però anche voler intendere che si
deve aspettare la terminazione dei soli rami attivati (e quindi in questo caso la semantica è equivalente a quella che si
avrebbe con un merge al posto del join).
Component diagram
Cosa sono?
Un component diagram è un diagramma UML che mostra quali sono i diversi componenti che costituiscono un
sistema e quali sono le loro dipendenze reciproche.
Un componente è un modulo fisico di codice, spesso corrispondente ad un package. Tuttavia, è possibile che i
componenti e i package non coincidano, perché il concetto di package non è fisico, mentre quello di componente sì.
Ad esempio, una classe può essere presente in diversi componenti, ma può essere definita in un solo package.
Possiamo quindi dire che un componente è una parte modulare del sistema, che nasconde al proprio interno la
realizzazione di una funzionalità e che può essere usato da altri componenti, comportandosi cioè come una black-box.
Ogni componente è auto contenuto e interagisce con gli altri mediante opportune interfacce di utilizzo. Si noti quindi
che la differenza tra il concetto di componente e quello di classe è spesso piuttosto sottile. Inoltre,
nell’implementazione è possibile che un componente non compaia, perché implementato semplicemente come una
serie di classi. Se due componenti soddisfano la stessa interfaccia, allora possono essere sostituiti l’uno con l’altro.
Le dipendenze tra componenti mettono in evidenza come i cambiamenti apportati ad un componente possono
causare cambiamenti negli altri componenti. Le dipendenze che possono essere usate sono molteplici (tra queste, si
hanno per esempio dipendenze di comunicazione e di compilazione).
I componenti possono essere decomposti gerarchicamente in altri componenti.
64
Ingegneria del Software 2
Capitolo 6: UML
I componenti
Un componente è rappresentato graficamente mediante il simbolo in figura.
<<component>>
Nome componente
Figura 67: Rappresentazione di un componente
Ogni componente è logicamente organizzato in interfacce e porte. Il componente può fornire e richiedere interfacce.
L’interfaccia è la definizione di un insieme di metodi (almeno uno), con l’aggiunta di eventuali attributi, che
idealmente definisce un insieme di comportamenti coesi. Un interfaccia fornita è rappresentata mediante la
cosiddetta lollipop notation, ovvero mediante il simbolo rotondo mostrato in figura 68. Un’interfaccia richiesta è
invece rappresentata attraverso la socket notation, che prevede di disegnare una semicirconferenza (fig. 69).
Figura 68: Lollipop notation
Figura 69: Socket notation
Le porte consentono invece di rappresentare un punto di interazione tra un componente e l’ambiente esterno. Le
porte sono rappresentate semplicemente come dei quadrati al margine del componente. Alla porta può essere
associato un nome.
<<component>>
Nome componente
Porta
Figura 70: Rappresentazione di un porta all’interno di un componente
Le porte possono soddisfare la comunicazione di tipo unidirezionale oppure bidirezionale:
1.
2.
3.
Le porte unidirezionali di output sono collegate solamente ad interfacce offerte dal componente stesso;
Le porte unidirezionali di input sono collegate solo a interfacce richieste dal componente stesso.
Le porte bidirezionali sono collegate sia ad interfacce richieste, sia ad interfacce offerte.
I componenti sono collegati tra loro mediante l’uso delle interfacce e delle relazioni di delegazione.
Figura 71: Esempio di component diagram
Come si osserva dalla figura 71, un componente può contenere al proprio interno degli altri componenti. Si tratta in
questo caso di classi composite, nelle quali viene messo in evidenza il ruolo di tutte le singole istanze che vi
compaiono.
65
Capitolo 6: UML
Ingegneria del Software 2
Statechart
Cosa sono?
Gli state chart sono dei diagrammi a stati che consentono di rappresentare l’evoluzione di un sistema. Si tratta dunque
di diagrammi comportamentali.
In particolare, il comportamento del sistema viene descritto in relazione allo stato del sistema stesso, in maniera
indipendente dalle modalità attraverso le quali avviene l’interazione con l’esterno. Per ogni diversa classe di oggetti si
avrà uno state chart. Si noti però che sono state definite diverse semantiche per questi diagrammi.
Il concetto di statechart è molto simile a quello di automa a stati finiti.
Automi a stati finiti
Uno automa a stati finiti è formalmente definito come una terna del tipo



, dove:
è un insieme finito di stati;
è un insieme finito di possibili input da fornire al sistema;
à la funzione di transizione tra uno stato ed un altro del sistema.
Esistono diverse classi di automi a stati finiti: si hanno infatti automi deterministici e non deterministici, automi
riconoscitori e traduttori, … . Per tali concetti, si rimanda ai corsi di Informatica Teorica.
Gli automi a stati finiti si prestano molto bene ad essere rappresentati in maniera grafica: ogni stato è identificato da
un pallino all’interno del quale è indicato il relativo nome, mentre le transizioni da uno stato
ad uno stato
appartenenti ad sono rappresentate con una freccia dal cerchio che rappresenta
al cerchio che corrisponde a ,
sulla quale viene esplicitato l’input che determina la transizione stessa.
Push switch
OFF
ON
Push switch
Figura 72: Esempio di automa a stati finiti, che rappresenta il funzionamento di una lampada
Problemi degli automi a stati finiti e soluzioni adottate negli statecharts
Gli automi a stati finiti presentano alcuni problemi: innanzitutto, essi dispongono solo di una memoria finita.
Un altro importante problema è dato dal fatto che il numero di stati aumenta in maniera considerevole all’aumentare
della complessità dell’automa. Considerando infatti un certo numero di automi a stati finiti di partenza, se costruiamo
un nuovo automa che ne sia la composizione, il numero di stati risultante sarà dato dal prodotto tra i numeri di stati
dei singoli automi di partenza (si ha quindi una crescita esponenziale, e non lineare).
Per risolvere questo problema, gli statecharts, pur rifacendosi molto da vicino agli automi a stati finiti, hanno
introdotto tutta una serie di strumenti che consentono di rappresentare gli automi in maniera modulare e gerarchica.
66
Ingegneria del Software 2
Capitolo 6: UML
Notazione grafica adottata dagli statecharts
Negli statecharts, ogni stato è indicato mediante un rettangolo con gli angoli smussati, all’interno del quale è indicato
il nome del relativo stato.
Lo stato iniziale viene inoltre indicato mediante una freccia entrante, che ha origine da un cerchio annerito.
Stato
Stato
Figura 73: rappresentazione di uno stato
in uno statechart
Figura 74: Stato iniziale in uno
statechart
Figura 75: rappresentazione di uno stato
finale in uno statechart
Di seguito è mostrato un primo semplice esempio di statechart.
Figura 76: Esempio di statechart
Per ridurre il numero di stati, si possono utilizzare i cosiddetti “sottostati”: all’interno di uno stato si rappresenta di
fatto un nuovo statechart. Quando si giunge allo stato in esecuzione, viene di fatto avviata l’esecuzione dello state
chart interno, congelando di fatto quella del precedente e partendo dallo stato iniziale. Lo stato contenente il
diagramma innestato potrebbe inoltre contenere delle altre transizioni, che vengono attivate al verificarsi di un
particolare evento, indipendentemente dallo stato in cui ci si trova all’interno dello state chart interno.
Uno stato S può inoltre avere un history substate, ovvero un sottostato che viene utilizzato perché ogni transizione in
ingresso nell’history substate equivale a dire che si ritorna nel sottostato di S nel quale ci si trovava prima di passare
ad un nuovo stato. L’history substate viene indicato mediante un pallino con all’interno la lettera H.
Figura 77: Esempio di state chart con sottostati
Figura 78: Esempio di state chart con history substate
67
Capitolo 6: UML
Ingegneria del Software 2
Sottostati concorrenti
Uno stato può essere suddiviso in sottostati diversi che evolvono in maniera concorrente, ovvero in parallelo tra loro.
Questo viene rappresentato mediante la suddivisione dello stato in diverse aree; all’interno di ognuna di queste aree è
presente un diverso state chart, che rappresenta una delle attività concorrenti in esecuzione.
Figura 79: Esempio di statechart con uno stato con sottostati concorrenti
Uso delle condizioni e delle azioni
Su una transizione, oltre al nome di un evento, può essere indicata anche una condizione, racchiusa tra parentesi
quadre, che deve essere verificata affinché la transizione avvenga realmente.
Inoltre, è possibile indicare un’azione, separata dalla parte restante dei dati della transizione mediante il simbolo di
slash (“/”). La struttura è perciò:
Evento [Condizione] / Azione
Nessuna delle 3 parti è obbligatoria. Si noti che:



L’evento può essere di diversi tipi:
a) Evento di chiamata (una chiamata sincrona proveniente da un certo oggetto).
b) Evento di cambiamento (il valore di un’espressione booleane varia).
c) Evento di segnale (ricezione di un segnale asincrono).
d) Evento di tempo (si arriva ad un certo istante assoluto).
L’azione è ciò che viene fatto durante la transizione. Può essere un’azione semplice, oppure un’azione più
complessa, descritta mediante un altro diagramma UML di tipo comportamentale.
I segnali sono descritti mediante classi con lo stereotipo <<signal>>. Essi possono essere tra loro legati da relazioni
di generalizzazione.
Figura 80: Esempi di segnali
68
Ingegneria del Software 2
Capitolo 6: UML
Utilizzare l’UML per modellare i requisiti
Use case diagram
Se si vogliono modellare i requisiti, il primo passo da compiere è la stesura di uno use case diagram, nel quale viene
definito l’insieme di tutte le funzionalità del sistema.
Le regole per la stesura del diagramma sono già state definite nei precedenti paragrafi. Inoltre, ad ogni use case deve
essere associata una descrizione testuale, come già accennato in precedenza.
Dagli use case agli oggetti
Partendo da uno use case, che definisce le “funzionalità di alto livello” del sistema, si individuano di volta in volta le
funzionalità di livello inferiore, ovvero descritte con un maggior livello di dettaglio. Il procedimento viene ripetuto fino
ad arrivare a definire le singole operazioni, sulla base delle quali potranno essere definiti gli oggetti partecipanti.
Class diagram
I class diagram definiti al livello di specifica dei requisiti sono dei diagrammi che rappresentano un modello
concettuale del dominio applicativo, e non sono dei diagrammi che modellano la struttura in classi del software object
oriented che verrà poi realizzato: talvolta le classi in essi definite non saranno presenti nel software che verrà
realizzato, e in ogni caso non vengono indicati i metodi delle entità definite nel modello.
Ogni classe deve avere una descrizione in linguaggio naturale che definisce le condizioni che la classe stessa deve
rispettare per trovarsi in un certo stato. Ogni attributo inoltre deve avere una descrizione in linguaggio naturale che ne
definisca il significato all’interno del mondo reale. Infine, la descrizione della classe contiene un invariante di dominio,
che specifica a parole le proprietà di dominio della classe stessa.
Per individuare gli oggetti, si possono seguire alcune semplici regole:
1.
2.
3.
4.
5.
6.
7.
È necessario conoscere il dominio applicativo, perciò è necessario osservare il cliente ed interagire con esso;
Occorre applicare l’intuizione e le conoscenze generali a propria disposizione;
Si analizza il flusso di eventi e si analizzano gli oggetti che partecipano nei singoli use case;
Si deve cercare di stabilire una tassonomia;
Si deve analizzare in maniera sintattica la descrizione del problema, lo scenario o il flusso di eventi.
I termini utilizzati all’interno di questo class diagram saranno solitamente nomi per quanto riguarda le classi,
mentre le operazioni devono avere dei nomi rappresentati da verbi.
Nella stesura degli use case diagram, è spesso utile applicare i design pattern.
Nella stesura del class diagram, è opportuno tener sempre presente che il suo scopo è quello di descrivere le proprietà
statiche di un sistema. Inoltre, i destinatari degli use case possono essere multipli:
1.
2.
3.
Gli esperti del dominio applicativo li usano per modellare la realtà di riferimento;
Gli sviluppatori li usano durante la fase di sviluppo, analisi, design e implementazione.
Il cliente e l’utente finale solitamente non sono interessati ai class diagram e focalizzano la loro attenzione
soprattutto sulle funzionalità del sistema.
69
Capitolo 6: UML
Ingegneria del Software 2
Individuare gli oggetti partecipanti negli use case
Per individuare quali sono gli oggetti che partecipano ad un certo use case, le regole principali da osservare sono:
1.
2.
3.
4.
Scelto uno use case, si osserva il relativo flusso di eventi e:
a) Si individuano i termini che gli sviluppatori o gli utenti devono chiarire per comprendere il flusso di eventi.
b) Si cercano i nomi più ricorrenti.
c) Si identificano le entità del mondo reale di cui il sistema deve tener traccia.
d) Si identificano le procedure del mondo reale delle quali il sistema deve tener traccia.
e) Si identificano le sorgenti dei dati o la destinazione dei dati stessi.
f) Se identificano le interfacce
Con molta probabilità, in questa fase alcuni oggetti verranno dimenticati, e bisogna perciò essere pronto ad
individuarli in un secondo momento.
Il flusso di eventi deve essere modellato con un diagramma di sequenza.
I termini utilizzati devono essere gli stessi che l’utente è in grado di comprendere.
Modellazione dinamica
A questo punto si può passare alla modellazione dinamica, il cui scopo è quello di fornire dei modelli per l’interazione
e il comportamento dei partecipanti e del workflow.
Il procedimento generale che viene seguito prevede alcuni passi di base:
1.
2.
3.
4.
Si usa come punto di partenza lo use case diagram o lo scenario;
Si modella l’interazione tra gli oggetti, mediante i sequence diagram. Gli oggetti e le classi che partecipano al
sequence diagram devono essere identificati durante i precedenti passi di analisi. È tuttavia possibile che vengano
identificati nuovi oggetti e classi in questa fase. Vengono così messe in evidenza le relazioni temporali tra gli
oggetti e le sequenze di operazioni, intese come sequenze di eventi e di risposte agli eventi.
Si rappresenta il comportamento dinamico dei singoli oggetti, mediante state chart. Tali diagrammi permettono di
mettere in evidenza i cambiamenti interni all’oggetto nel corso del tempo
Si modella il workflow, mediante gli activity diagram.
Si noti che i modelli dinamici devono essere realizzati solamente per quelle classi che hanno un comportamento
dinamico significativo.
70
Ingegneria del Software 2
Capitolo 7: Specifica
Capitolo 7: Specifica
Il concetto di specifica
Che cos’è una specifica?
Il termine specifica viene qui utilizzato come sinonimo di “descrizione precisa” e di alto livello. In generale però il
termine rappresenta un vero e proprio contratto che viene stretto tra il produttore e il cliente del servizio, oppure tra
chi definisce il software e chi lo deve implementare.
Il concetto di specifica è perciò ben distinto da quello di implementazione; la specifica:



Definisce cosa fa il sistema, in modo astratto (ovvero trascurando tutta una serie di dettagli);
Non è necessariamente eseguibile in modo diretto;
Spesso è indeterminata, cioè sono possibili diverse implementazioni della stessa specifica.
L’implementazione invece definisce in maniera concreta il comportamento del sistema.
Proprietà della specifica
La specifica deve di conseguenza possedere le seguenti proprietà:
1.
2.
3.
4.
5.
6.
Chiarezza
Precisione
Assenza di ambiguità
Consistenza (in caso contrario non esisterà alcuna implementazione che soddisfi la specifica)
Completezza, che a sua volta può essere:
a) Interna (se si parla di un certo oggetto “x” in un punto della specifica, bisogna che “x” sia già stato definito).
b) Esterna (la specifica deve dire tutto ciò che è necessario a riguardo del fenomeno che ha per oggetto).
Deve essere incrementale (il processo di specifica avviene in maniera incrementale, ovvero si parte da una
descrizione che viene poi gradualmente “allargata”).
Notazioni per la specifica
Il linguaggio naturale non consente di ottenere specifiche dotate di tutte le proprietà appena elencate: ad esempio,
ogni testo scritto in linguaggio naturale ha al suo interno tutta una serie di ambiguità, che il lettore disambigua sulla
base delle proprie conoscenze; tuttavia, ogni individuo può avere dei background diversi, e di conseguenza è possibile
che disambigui le frasi in linguaggio naturale in maniere diverse.
Si utilizzano allora delle notazioni apposite, che possono essere formali (ovvero matematicamente precise) oppure
semiformali (cioè che utilizzano dei linguaggi in parte formalizzati, in parte no). Il linguaggio naturale è invece una
notazione informale.
Tipi di specifica
Una specifica può essere di diverse tipologie:

Specifica operazionale
Si tratta di una specifica nella quale vengono utilizzati i concetti tipici delle macchine astratte (automi), come ad
esempio il concetto di “stato”, per specificare il comportamento del sistema. Ad esempio, una specifica
operazionale può essere ottenuta per mezzo di uno state chart.

Specifica descrittiva (o dichiarativa)
Una specifica descrittiva consente di indicare il comportamento del sistema sulla base di asserzioni
(normalmente basate sulla logica) che ne rappresentano le propreità.
71
Capitolo 7: Specifica
Ingegneria del Software 2
Come verificare la specifica?
Per verificare la specifica, è necessario:
1.
2.
Osservare il comportamento dinamico del sistema (mediante simulazione, prototipazione, o mediante test sulla
specifica)
Analizzare le proprietà del sistema oggetto di specifica.
Si hanno quindi alcune analogie con le ingegnerie tradizionali, nelle quali è possibile realizzare dei modelli fisici o
matematici di ciò che si sta costruendo (ad esempio, un ponte).
Alcuni esempi di notazioni
Le notazioni che noi vedremo sono 2:

UML
L’UML, di cui abbiamo già ampiamente parlato, è un linguaggio di specifica semiformali, che include notazioni
molto diverse tra loro per la specifica di aspetti differenti. Le notazioni comportamentali sono soprattutto i
sequence diagram, gli state chart diagram e gli activity diagram; le notazioni descrittive sono soprattutto i class
diagram e gli object diagram.

Alloy
In seguito, introdurremo invece la notazione Alloy, che è di tipo descrittivo e che è una notazione formale.
Introduzione ad Alloy
Che cos’è Alloy?
Come abbiamo appena accennato, Alloy è un linguaggio formale e descrittivo per la specifica, sviluppato al
Massachusetts Institute of Technology e giunto oggi alla sua quarta versione. Alloy consente perciò di definire dei
modelli del software.
I concetti utilizzati da Alloy sono molto simili a quelli tipici dei linguaggi orientati agli oggetti; tuttavia, in questo caso il
concetto di classe non contiene al suo interno una descrizione degli algoritmi utilizzati per compiere determinate
operazioni, ma solo le dichiarazioni di proprietà (si tratta di un linguaggio dichiarativo), definite mediante l’uso della
logica. La notazione di Alloy è inoltre compatibile con modelli di rappresentazione grafica degli oggetti.



Alloy utilizza predicati della logica del primo ordine tipizzata.
Il concetto base di Alloy è quello di relazione, di conseguenza l’algebra relazione è alla base di questa notazione di
specifica.
Dispone di un potente e veloce tool automatico per simulare le specifiche e verificare la validità delle proprietà
definite. In dettaglio, questo tool è in grado di stabilire se le specifiche date sono soddisfacibili (potrà cioè esistere
un’implementazione che le soddisfa) e se da esse possono essere tratte delle particolari conseguenze.
Perché usare Alloy?
Alloy può essere utilizzato ovunque sia necessario costruire un modello. Ad esempio, può essere usato nella fase di
ingegnerizzazione dei requisiti, per descrivere il dominio e le sue proprietà, oppure per descrivere le operazioni che il
sistema dovrà essere in grado di eseguire. Inoltre, Alloy può essere usato nella fase di design, per specificare i
componenti e le loro interazioni.
72
Ingegneria del Software 2
Capitolo 7: Specifica
Concetti base di Alloy

Si utilizza la logica non specializzata
Non ci sono costrutti speciali per i concetti di macchina a stati, concorrenza, sincronizzazione, … .

Ambiente ed uso dei controesempi
La verifica delle specifiche si basa in Alloy sulla ricerca di contresempi. In questo si ha perciò un’analogia rispetto
alla tradizionale attività di testing del codice; la differenza fondamentale è però nell’ambiente che viene
utilizzato: mentre il testing avviene campionando in maniera più o meno casuale un certo insieme di valori scelti
all’interno del dominio, in Alloy si considera un sottoinsieme qualsiasi del dominio, purché tale sottoinsieme sia
finito, e si effettua all’interno di tale sottoinsieme una ricerca esaustiva, andando quindi a verificare tutti i valori
appartenenti al sottoinsieme (si parla perciò di small scope hypotesys). In sostanza, il modello è infinito, ma lo
scope (ambiente) è finito.
Figura 81: Confronto tra l’attività di verifica tramite testing e la verifica mediante l’uso di Alloy (small scope hypothesys)

Analisi mediante l’utilizzo di SAT
La verifica in Alloy viene eseguita mediante un algoritmo detto SAT (che, come noto da corsi come quello di
Algebra e Logica Matematica, sta per Satisfiability, ovvero per soddisfacibilità di formule logiche).
Siccome abbiamo già affermato che Alloy utilizza la logica del primo ordine (FOL, First Order Logic) e siccome è
noto che la soddisfacibilità di formule FOL non è un problema decidibile, risulta naturale chiedersi come sia
possibile disporre di un tool software in grado di verificarla, per di più in maniera rapida. La risposta a questa
domanda proviene proprio dall’assunzione che abbiamo descritto nel punto precedente: siccome gli insiemi che
si considerano sono finiti, le formule FOL possono essere in questo caso considerate equivalenti a formule
proposizionali, e quindi in questo caso il problema risulta essere decidibile.
Nonostante questo, il problema rimane nel caso generale NP-completo (e quindi costosissimo in termini di
complessità computazionale). Ciò che ci consente di ottenere strumenti in gradi di verificare efficacemente la
soddisfacibilità delle formule in questione è in realtà un’osservazione pratica: nella realtà dei fatti, le formule che
vengono scritte in Alloy sono quasi sempre tali che l’efficienza che si ottiene è molto elevata.

Tutto è una relazione
Le relazioni sono utilizzate da Alloy per la rappresentazione di tutte le tipologie di dati, compresi gli insiemi, gli
scalari e le tuple.
Questa scelta è legata al fatto che le relazioni sono di facile comprensione (possono anche essere facilmente
rappresentate in maniera grafica), facili da analizzare e consentono la massima uniformità (qualsiasi concetto è
rappresentato mediante quello di relazione).
In Alloy esiste inoltre il concetto di atomo: gli atomi sono le entità primitive di Alloy, e si tratta di elementi
indivisibili, immutabili e non interpretabili.
73
Capitolo 7: Specifica
Ingegneria del Software 2
Le relazioni sono delle associazioni tra atomi e sono di fatto degli insiemi di tuple, nei quali ogni tupla è una
sequenza di atomi.
Tutti i valori sono rappresentati, indipendentemente dal loro tipo, mediante l’uso di una relazione:
a)
Se il valore è un insieme, allora è rappresentato mediante una relazione unaria:
Figli = { (F0), (F1), (F2) }
b) Se il valore è uno scalare, viene rappresentato come un insieme con un solo elemento:
MioNome = { (N0) }
c)
Esiste poi il concetto di relazione in senso proprio (che potrà essere binaria, ternaria, …). Ad esempio:
AutoriLibri = { (B0, A0), (B0, A1), (B1, A0), (B2, A2) }
In questo caso, abbiamo tre libri B0, B1 e B2. Il primo libro è scritto dagli autori A0 e A1, B1 è scritto solo da
A0 e B2 ha come autore A2.
Le relazioni possono intuitivamente essere rappresentate mediante tabelle non ordinate, le cui colonne non
hanno un intestazione, ma sono ordinate. Tutte le relazioni sono del primo ordine, ovvero non possono
contenere al loro interno delle altre relazioni.
Ad esempio, la relazione precedentemente indicata come esempio verrà rappresentata dalla seguente tabella:
B0
B0
B1
B2
A0
A1
A0
A2
Tabella 4: Rappresentazione tabellare di una relazione di esempio
Più nel dettaglio, una relazione è definita come un insieme di un certo numero n di elementi, dove n prende il
nome di arità della relazione. Le relazioni Alloy sono sempre tipizzate (ad esempio, il primo elemento deve
essere di tipo Book, il secondo di tipo String).
Una relazione binaria viene anche rappresentata come:
Dove

è il tipo di sinistra e
è il tipo di destra. Inoltre diciamo che:
a) R è totale se ogni atomo del tipo di sinistra è mappato ad almeno un atomo del tipo di destra.
b) R è suriettiva se ogni atomo della parte di destra è mappato ad almeno un atomo del tipo di sinistra.
c) R è funzionale se ogni elemento del tipo è mappato ad al più un elemento del tipo .
d) R è iniettiva se ogni atomo del tipo è mappato ad al massimo un atomo del tipo .
L’operatore .
L’operatore fondamentale è l’operatore “.”, che corrisponde al join tra relazioni e che consente di fatto ad
accedere ad uno specifico dato.
La semantica di Alloy
La semantica di Alloy definisce il significato di una specifica Alloy come un insieme di modelli (mondi) nei quali la
specifica Alloy è verificata. In Alloy, un mondo è costituito da atomi e da relazioni tra atomi. Come accennato, un
atomo è un oggetto completamente privo di caratteristiche, indivisibile ed immutabile, senza significato (può
rappresentare qualsiasi cosa).
74
Ingegneria del Software 2
Capitolo 7: Specifica
La sintassi di Alloy
Le costanti
In Alloy sono definite le seguenti costanti:
Significato
Insieme vuoto
Insieme universo
Relazione identità
Costante
none
univ
iden
Tabella 5: Costanti definite in Alloy
Gli operatori
Oltre all’operatore punto, si possono usare anche i seguenti operatori insiemistici:
Operatore
Unione
Intersezione
Differenza
Sottoinsieme
Uguaglianza insiemistica
Prodotto cartesiano
Simbolo
+
&
in
=
->
Tabella 6: Operatori insiemistici ammessi in Alloy
Ed i tradizionali operatori logici, riassunti nella tabella seguente:
Operatore
And
Or
Not
Implicazione logica
Else (alternativa)
Se e solo se
Simbolo
Parola chiave equivalente
&&
||
!
=>
else
<=>
and
or
not
implies
else
iff
Tabella 7: Operatori logici ammessi in Alloy
Per chiarire l’uso di else, di seguito sono riportate due formule equivalenti:
F implies G else H
(F && G) || (!F && H)
Come già accennato, l’operatore più importante è l’operatore “.”, che equivale al join relazionale. Ad esempio:
X = { (A1, B1), (A2, B2) }
Y = { (B1, C1), (B3, C3), (B1, C2), (B2, C4) }
X . Y = { (A1, C1), (A1, C2), (A2, C4) }
Cioè si costruiscono le tuple partendo dai due insiemi di partenza, considerando solo le coppie di tuple tali che la
prima tupla appartenga all’insieme indicato come primo operando, la seconda appartenga all’insieme indicato come
secondo operando, e l’ultimo elemento della prima tupla coincida con il primo della seconda; la tupla costruita è
ottenuta semplicemente “concatenando” le due tuple di partenza, me eliminando l’ultimo elemento della prima e
tupla e il primo della seconda. L’operatore . è detto anche dot join, in contrapposizione all’operatore box join, del
tutto equivalente al precedente, ma con una notazione diversa:
e1.e2
equivale a
e2[e1]
75
Capitolo 7: Specifica
Ingegneria del Software 2
Esistono inoltre alcuni operatori unari, riassunti nella tabella di seguito riportata:
Operatore
Trasposta
Chiusura transitiva
Chiusura transitiva e riflessiva (solo per relazioni binarie)
Simbolo
~
^
*
Tabella 8: Operatori unari ammessi in Alloy
Gli operatori unari vengono indicati prima del nome della relazione. Ad esempio, se abbiamo:
e = {(A1, A2), (A3, A4)}
~e = {(A2, A1), (A4, A3)}
Altri operatori consentiti sono quelli di restrizione e di override:
Operatore
Restrizione di dominio
Restrizione di codominio
Override
Simbolo
<:
:>
++
Tabella 9: Operatori di restrizione e di override ammessi in Alloy
La restrizione di dominio ha come primo operatore un insieme e come secondo operatore una relazione (anche se
come noto l’insieme è di per sé una relazione, …) e restituisce la relazione costituita da quelle tuple della relazione
data tali che il primo elemento appartenga all’insieme indicato come parametro. Ad esempio:
A = { (A1), (A2) }
r = { (A1, B1), (A1, B2), (A2, B1), (A2, B4), (A3, B5), (A4, B1)}
A <: r = { (A1, B1), (A1, B2), (A2, B1), (A2, B4) }
La restrizione di codominio è del tutto simmetrica:
B = { (B1), (B2) }
r = { (A1, B1), (A1, B2), (A2, B1), (A2, B4), (A3, B5), (A4, B1)}
r :> B = { (A1, B1), (A1, B2), (A2, B1), (A4, B1)}
Un po’ più complesso è l’operatore di override: date due relazioni, l’operatore di override costruisce una terza
relazione nella quale vengono inserite tutte le tuple delle due relazioni di partenza, salvo quelle della prima relazione il
cui primo elemento è presente anche come primo elemento di altre tuple della seconda relazione (tali tuple vengono
perciò “sovrascritte” o “scavalcate” da quelle della seconda relazione). Più formalmente:
p ++ q = p – (domain[q] <: p) + q
I quantificatori
Alloy consente anche l’uso dei quantificatori, similmente a quanto accade nella logica del primo ordine. Le sintassi
attraverso le quali un quantificatore può essere indicato sono le seguenti:
nomeQuantificatore
nomeQuantificatore
nomeQuantificatore
nomeQuantificatore
varVincolata : Tipo | Condizione
varVincolata1, varVincolata2 : Tipo | Condizione
varVincolata1 : Tipo1, varVincolata2 : Tipo2 | Condizione
disj varVincolata1, varVincolata2 : Tipo | Condizione
Il significato di tali sintassi risulta molto intuitivo. I quantificatori esistenti sono:
Significato
Condizione verificata solo se per ogni elemento del tipo dato la condizione è vera.
Condizione verificata solo se per almeno un elemento del tipo dato la condizione è vera.
Condizione verificata solo se per 0 o 1 elemento del tipo dato la condizione è vera.
Condizione verificata solo se per 1 e solo un elemento del tipo dato la condizione è vera.
Condizione verificata solo se per nessun elemento del tipo dato la condizione è vera.
Tabella 10: quantificatori ammessi in Alloy
76
Simbolo
all
some
lone
one
no
Ingegneria del Software 2
Capitolo 7: Specifica
Definizione di una signature
Una signature definisce un tipo e le relazioni nelle quali gli oggetti di quel tipo sono coinvolti come primi elementi. Se
vogliamo definire un tipo di oggetti senza alcuna relazione, scriviamo:
sig TipoOggetto{}
Se vogliamo definire un tipo di oggetti che partecipa come primo elemento in una relazione binaria, scriviamo:
sig TipoOggetto{ NomeRelazione : TipoSecondoElemento }
Questo assomiglia molto nella programmazione ad oggetti a definire un attributo che viene associato alla “classe” che
si sta definendo, e possiamo in un certo senso interpretarlo in tal modo, anche se nella realtà i concetti che si
nascondono dietro tale sintassi sono quelli delle relazioni, fino ad ora analizzati.
Questa dichiarazione può comprendere anche delle parole chiave, che possono essere interpretate come la
definizione di un attributo che rappresenta un insieme di elementi:
sig TipoOggetto{ NomeRelazione : parolaChiave TipoSecondoElemento }
Le parole chiave che possono essere usate per la definizione di un insieme sono le seguenti:
Significato
Insieme con un numero qualsiasi di elementi
Insieme con almeno un elemento
Insieme con non più di un elemento (o zero, o uno)
Insieme con uno ed un solo elemento
Simbolo
set
some
lone
one
Tabella 11: dichiarazione degli insiemi in Alloy
Se non si scrive nulla, è come se venisse utilizzata la parola chiave one. Come accennato, in realtà si avrà una
relazione; ad esempio, se utilizziamo la parola chiave lone, significa che ogni elemento di TipoOggetto potrà essere
associato da NomeRelazione ad al massimo un elemento del tipo TipoSecondoElemento. In ogni caso, l’operatore
dot join introduce un’altra analogia con la programmazione ad oggetti: scrivendo:
Oggetto.NomeRelazione
Otterremo infatti il valore o l’insieme dei valori di TipoSecondoElemento associati ad Oggetto, in maniera analoga
a quanto accadrebbe se la relazione fosse in realtà un attributo di una classe.
Si possono inoltre utilizzare delle relazioni di arità superiore. Ad esempio, per definire relazioni ternarie scriviamo:
sig TipoOggetto{ NomeRelazione : TipoSecondoElemento -> TipoTerzoElemento}
Anche in questo caso si possono indicare delle parole chiave (di seguito sostituite da n e m):
sig TipoOggetto{ NomeRelazione : TipoSecondoElemento n -> m TipoTerzoElemento}
I cui valori possibili sono gli stessi definiti nella precedente tabella, in relazione alla definizione di insiemi. Ad esempio:
sig TipoOggetto{ NomeRelazione : TipoSecondoElemento lone -> some TipoTerzoElemento}
Espressioni di quantificazione
La tabella seguente mostra la possibilità di utilizzare delle espressioni di quantificazione che, applicate a relazioni,
producono dei valori booleani:
Significato
Vero se la relazione non è vuota
Vero se la relazione è vuota
Vero se la relazione ha al più un elemento
Vero se la relazione ha esattamente un elemento
Simbolo
some
no
lone
one
Tabella 12: espressioni di quantificazione in Alloy
Scriveremo poi:
operatore NomeInsieme
77
Capitolo 7: Specifica
Ingegneria del Software 2
Espressioni per indicare una relazione derivata
È possibile anche indicare una certa relazione “derivata”, mediante espressioni del tipo:
{x1: e1, x2: e2, ..., xn: en | F}
Tale espressione corrisponde a tutte le tuple di n elementi, il cui i-esimo elemento è del tipo ei, che verificano la
condizione F.
Utilizzo della parola chiave let
Una parola chiave utile è la parola let, che consente di “rinominare” una certa espressione. Ad esempio, di seguito
sono mostrate 4 espressioni tra loro equivalenti, che servono a chiarire come è possibile usare la parola chiave let.
all n: Name | (some n.workAddress implies n.address = n.workAddress
else
n.address = n.homeAddress)
all n: Name | let w = n.workAddress, a = n.address |
(some w implies a = w else a = n.homeAddress)
all n: Name | let w = n.workAddress |
n.address = (some w implies w else n.homeAddress)
all n: Name | n.address = (let w = n.workAddress | (some w implies w else n.homeAddress))
Cardinalità
La cardinalità di un insieme o di una relazione è indicata mediante l’operatore #. Per gestire le cardinalità, si possono
utilizzare i tradizionali operatori di confronto tra interi ( >, <, =, >=, <=), si possono definire degli interi costanti
(semplicemente mediante i numeri decimali), si possono utilizzare le operazioni di addizione e sottrazione tra interi ( +
e –). Esiste infine l’operatore di somma:
sum x: e | ie
Che calcola la somma di tutte le espressioni ie valutate per gli elementi x di tipo e.
Definizione di un fact
I fatti sono delle proprietà dei modelli, ovvero dei vincoli ai quali i modelli devono sottostare. I fatti vengono indicati
mediante una notazione del tipo:
fact { condizione }
Dove la condizione può essere espressa mediante tutti gli operatori precedentemente introdotti (ad esempio,
mediante un quantificatore).
Definizione di predicati
I predicati (in inglese predicates) sono delle espressioni riutilizzabili. Essi consentono di definire delle proprietà che
dovranno essere rispettate dai mondi utilizzati, e che vengono utilizzate per l’individuazione di tali mondi: quando
verrà eseguito il modello, Alloy si preoccuperà di fornire uno dei mondi (se esistono) che soddisfano il predicato.
Un esempio di definizione di predicato è il seguente:
pred show(b: Book) {
#b.addr > 1 or #b.addr >= 1
}
I predicati possono anche essere utilizzati per definire delle operazioni. Ad esempio, possiamo scrivere:
pred add(b, b': Book, n: Name, a: Addr) {
b'.addr = b.addr + n ->a
}
In questo modo, anche se il predicato è in realtà solo un vincolo, riusciamo a forzare b e b' in modo che
rappresentino l’uno il libro prima dell’operazione (b), e l’altro il libro dopo l’operazione (b’).
78
Ingegneria del Software 2
Capitolo 7: Specifica
Definizione di funzioni
Alloy permette inoltre di definire delle funzioni in senso vero e proprio, mediante la sintassi:
fun name (parameter1: domain1, paramter2: domain2, ..., parameterN: domain): domain {
[body must evaluate to a value in Domain]
}
Per esempio, possiamo scrivere:
fun between (lower : Int, upper : Int) : Int {
{answer: Int | lower < answer && answer < upper }
}
La funzione così definita restituisce l’insieme di tutti gli interi compresi tra i due valori ricevuti come parametri. Si
possono inoltre aggiungere dei vincoli ai parametri e al valore di ritorno. Ad esempio:
fun name (param: some Int) : lone Int {
...
}
Potremo poi utilizzare la funzione all’interno di espressioni, semplicemente scrivendo (se riprendiamo il precedente
esempio):
Between[par1, par2]
Definizione di asserzioni
Le asserzioni sono delle proprietà che devono essere verificate. Esse, a differenza dei predicati, vengono controllate
per ricercare dei controesempi: le proprietà espresse all’interno delle asserzioni devono essere verificate per tutti i
mondi e Alloy si preoccupa di cercare degli esempi di mondi in cui ciò non accade; se Alloy individua un mondo di
questo tipo, significa che esistono dei problemi nel nostro modello.
Un esempio di asserzione è il seguente:
assert delUndoesAdd {
all b, b', b": Book, n: Name, a: Addr |
add[b,b',n,a] and del[b',b",n, a] implies b.addr = b".addr
}
Dove si è assunto che, oltre al predicato add definito nell’esempio precedente, sia stato definito anche il predicato
del come segue:
pred del (b, b': Book, n: Name, a: Addr) {
b'.addr = b.addr - n ->a
}
Definizione di comandi
Infine, è possibile specificare di comandi, ovvero delle istruzioni che servono per comunicare all’analizzatore Alloy
quali asserzioni e predicati devono essere controllati, e come.
Se vogliamo eseguire un predicato e mostrare i mondi possibili coerenti con tale predicato, allora utilizziamo il
comando run, nel quale possiamo anche specificare la dimensione dello scope, mediante un numero che rappresenta
la massima quantità di atomi di ognuno dei tipi definiti:
run predicato() for n
Se invece vogliamo cercare i controesempi di una certa asserzione, utilizziamo in maniera del tutto simile il comando
check:
check asserzione() for n
79
Capitolo 7: Specifica
Ingegneria del Software 2
Le gerarchie in Alloy
In Alloy è possibile anche definire delle gerarchie tra le signature definite e, similmente a quanto accade nel mondo
della programmazione ad oggetti, una signature può essere definita astratta.
Per definire una signature che eredi da un’altra si utilizza la parola chiave extends:
sig NomeFiglio extends NomePadre {
//elenco dei campi
}
In sostanza, l’erede è un sottoinsieme della signature padre. Se si definiscono due diverse signature eredi (più
propriamente dette subjects), allora si tratterà di insiemi disgiunti.
Per definire una signature astratta, è sufficiente utilizzare la parola chiave abstract:
abstact sig NomeSignature { }
In questo modo si è sicuri che non esisteranno degli oggetti del tipo dato che non appartengano ad uno dei suoi
sottotipi.
Incorporare i fatti nelle signature
Per rendere più concisa la scrittura dei fatti, è possibile incorporarli direttamente all’interno di una signature, come
mostrato nel seguente esempio:
abstract sig Target {}
sig Addr extends Target {}
abstract sig Name extends Target {}
sig Alias, Group extends Name {}
sig Book { addr: Name -> Target } {no n: Name | n in n.^addr}
In questo modo, siccome la dichiarazione è dentro alla signature, è sottinteso che varrà per tutti gli oggetti di tipo
Book.
Tracce di esecuzione
È possibile descrivere un ordine all’interno di un qualsiasi insieme, mediante l’uso della libreria module util/ordering.
Per comprenderlo meglio, vediamo il seguente esempio:
module tour/addressBook3
open util/ordering [Book]
abstract sig Target {}
…
pred init (b: Book) { no b.addr }
fact traces {
init (first ())
all b: Book - last() | let b' = next(b) |
some n: Name, t: Target | add (b, b', n, t) or del (b, b', n, t)
}
In questo modo abbiamo definito un ordine tra i libri, in modo tale che il primo libro soddisfi una certa condizione
iniziale, e tutti i libri adiacenti ad un altro sono correlati a quest’ultimo mediante una certa operazione.
80
Ingegneria del Software 2
Capitolo 7: Specifica
Un esempio di utilizzo di Alloy
Consideriamo adesso un primo esempio di specifica in Alloy. Consideriamo un mondo costituito da persone che
devono avere le seguenti proprietà:
1.
2.
3.
4.
Ognuna ha esattamente un nome;
Ognuna ha uno o zero coniugi;
Ognuna ha dei genitori;
Ognuna ha un’agenda dei compleanni.
Inoltre, ogni agenda dei compleanni:
1.
2.
Contiene una lista di persone;
Riporta, per ogni persona nella lista, il relativo giorno di nascita.
Infine, devono sussistere i seguenti vincoli:
1.
2.
Nessuna persona è sposata con un suo fratello o una sua sorella;
Ogni persona è contenuta nell’agenda dei compleanni di ognuno dei suoi amici (dove gli amici di una persona
sono coloro che fanno parte della lista dei compleanni di quella persona).
module birthday_book
sig Name {}
sig Person {
name: one Name,
spouse: lone Person,
parents: set Person,
birthdayBook : Person -> lone Date
}
sig Date {}
fact notMarriedWithBrotherOrSister {
(no p: Person | some (p.spouse.parents & p.parents))
}
fact birthdayBookConstraint{
all disj p1, p2 : Person |
let b1 = p1.birthdayBook, b2 = p2.birthdayBook
| (some p2.b1 implies some p1.b2)
}
fact noDiffDate{
all p1, p2, p3 : Person | (some p3.(p1.birthdayBook) and some
p3.(p2.birthdayBook)) implies p3.(p1.birthdayBook) = p3.(p2.birthdayBook)
}
fact noHimselfInBirthdayBook{
all p1, p2 : Person | some p2.(p1.birthdayBook) implies (! p1 = p2)
}
pred show(){ }
run show for 5 but exactly 3 Person
81
Capitolo 7: Specifica
Ingegneria del Software 2
Si noti che oltre ai vincoli richiesti ne sono stati imposti alcuni aggiuntivi. Un esemio di mondo generato è il seguente:
Figura 82: Esempio di mondo generato dall’Alloy Analizer
Osservazioni conclusive su Alloy
Prima di concludere, è bene sottolineare che:


82
Se si utilizza il comando check e l’analizzatore Alloy non è in grado di individuare alcun controesempio, questo
non significa che il modello sia esatto, perché l’analizzatore si limita a considerare un ambiente finito; tuttavia, se
l’ambiente è sufficientemente grande, non trovare controesempi significa con buona probabilità che il modello è
corretto.
Anche se il modello Alloy è corretto, non esiste un modo automatico per stabilire se il software che verrà in
seguito realizzato sarà aderente o meno al modello specificato.
Ingegneria del Software 2
Capitolo 8: Software design
Capitolo 8: Software design
Software design & software architecture
Distinzione tra il concetto di design e software architecture
Spesso si parla indistintamente di design e architettura software; in realtà però si tratta di concetti distinti, e possiamo
evidenziare tale differenza affermando che l’attività di design produce l’architettura software. Più in dettaglio:

Design
Consiste nella decomposizione del sistema in moduli e si focalizza sulle relazioni e interazioni tra i moduli.

Architettura
È la definizione di un sistema software nei termini dei componenti che lo costituiscono delle interazioni tra tali
componenti.
I componenti possono essere considerati a vari livelli di astrazione: può così trattarsi di moduli, unità di routine,
procedure, oggetti. Di conseguenza, anche il concetto di interazione si differenzierà in base al livello di astrazione
considerato: ad esempio, un’interazione può corrispondere a una chiamata di procedura, all’invocazione di un
metodo, … .
La descrizione dell’architettura software
La descrizione dell’architettura software definisce tutti gli elementi che costituiscono l’architettura, e che abbiamo
fino ad ora citato (componenti, connettori, configurazioni, unità di sviluppo, unità a runtime, moduli).
Si noti che l’architettura software esiste sempre, per ogni tipo di software, e può avere una descrizione più o meno
esplicita (o non avere una descrizione del tutto) ad essa associata.
Lo scopo dell’Ingegneria del Software è di esplicitare sempre l’architettura, in modo da analizzarla, comprenderla e
migliorarla prima che avvenga la realizzazione concreta del software.
Le diverse viste architetturali che costituiscono la descrizione di un’architettura software
Nella maggior parte dei casi, la descrizione dell’architettura è strutturata in diverse viste che mettono in evidenza
aspetti differenti:

Viste funzionali (functional view)
Definiscono l’allocazione funzionale dei diversi componenti. Una vista funzionale definisce quindi i componenti e
i connettori e controlla se gli assegnamenti alle funzioni corrispondenti sono completi.

Viste di runtime (runtime view)
Definiscono le unità di runtime (i componenti disponibili in esecuzione), mostrando come collaborano tra loro.

Viste di distribuzione (deployment view)
Definiscono le principali unità di distribuzione e le linee guida all’installazione.

Viste di modulo (module view)
Forniscono una decomposizione logica del codice in diversi moduli (design in piccolo).
83
Capitolo 8: Software design
Ingegneria del Software 2
Ruolo centrale dell’architettura software
Si può inoltre affermare che l’architettura del software è un elemento centrale del software stesso:
Strutturato in accordo a
Descrizione
architetturale
Descrive
Architettura
software
Strutturato in accordo a
Lavora in accordo a
Codice
sorgente
Programma
eseguibile
Programma in
esecuzione
Figura 83: Centralità dell’architettura software
Possiamo infatti affermare che gli elementi visualizzabili del software sono 3: il sorgente, l’eseguibile ed il programma
in esecuzione; tutti e 3 tali concetti sono legati all’architettura, che definisce la struttura del sorgente e dell’eseguibile
e che descrive ciò che accadrà durante l’esecuzione. Nonostante ciò, i 3 punti diversi sono strettamente correlati tra
loro. Più nel dettaglio:



Dal punto di vista del sorgente, si definiscono quali sono le classi, le interfacce, … .
Se si adotta il punto di vista dell’esecuzione, l’architettura rappresenta la definizione dei vari eseguibili e delle
interfacce che possono utilizzare, specificando come sono divisi gli elementi descritti dal punto di vista del
sorgente all’interno dei vari eseguibili finali.
Dal punto di vista del runtime, viene descritta, ad esempio, la distinzione tra i vari client e server, … .
Una descrizione architetturale più di alto livello è quella dei componenti funzionali. Tale descrizione è di livello
concettuale, perciò i componenti qui individuati non sono necessariamente mappati su componenti concreti mediante
una relazione uno ad uno. Con componente funzionale si intende un componente in grado di offrire un certo insieme
di funzionalità.
84
Ingegneria del Software 2
Capitolo 8: Software design
Il processo di design
Che cos’è il processo di design
Il processo di design è un’insieme di azioni che, partendo da una descrizione del problema, solitamente rappresentata
dal documento di analisi e specifica dei requisiti (spesso indicato con RASD), consente di definire tutte le viste
architetturali che si ritengono necessarie nello specifico caso. L’output di tale processo è quindi la descrizione
architetturale, che presenta tutti gli aspetti che appaiono più appropriati per il problema che si sta considerando.
È possibile adottare dei processi tra loro molto diversi per la realizzazione dell’attività di design. Alcuni di questi
approcci sono l’approccio waterfall (a cascata), l’approccio bottom-up e l’approccio incrementale.
Il processo top down (a cascata, o waterfall)
L’approccio top down può essere riassunto dalla seguente figura:
Fasi alte
Creazione
vista
funzionale
RASD
Fasi di design dettagliato
Creazione
vista di
runtime
Vista
funzionale
Creazione
vista di
distribuzione
Vista di
runtime
Vista di
distribuzione
Creazione
vista di
modulo
Vista di
modulo
Figura 84: Processo di design ti tipo top down
Nella precedente figura, sono stati indicati in rosso i documenti che rappresentano gli input e gli output delle varie
fasi, mentre in azzurro sono indicate le fasi del processo. Vediamo ora nel dettaglio quali operazioni vengono
compiute nelle singole fasi del processo:

Creazione della vista funzionale
Durante la definizione della vista funzionale, si individuano i componenti e i connettori e si verifica che tutte le
funzionalità richieste vengano assegnate ai componenti così individuati.

Creazione della vista di runtime
Si verifica che per ogni funzionalità esista un’entità di runtime che le contenga.

Creazione della vista di distribuzione
Si definiscono le tecnologie ed i linguaggi che verranno usati. Occorre verificare che ogni unità di runtime abbia
una corrispondente unità di distribuzione.

Creazione della vista di modulo
In questa fase si definiscono quali sono i moduli e le classi del sistema e come sono mappati alle unità di
deployment (o distribuzione) individuate nella fase precedente.
Il processo bottom up
Un processo diverso è quello bottom-up, il quale viene adottato nel caso in cui si abbiano a disposizione alcuni pezzi di
codice riusabili, attorno ai quali costruire le parti restanti del sistema da sviluppare.
Gli elementi preesistenti sono rappresentati da una vista di distribuzione. L’approccio consiste quindi nel lavorare su
tale vista, in modo da identificare la relativa vista di runtime. Inoltre, se è necessario, si passa a definire la vista
funzionale.
85
Capitolo 8: Software design
Ingegneria del Software 2
Uso dei diagrammi UML per il design architetturale
Durante la fase di design, si cerca spesso di astrarre alcuni dettagli del codice utilizzando diagrammi uml:




Per le viste funzionali si utilizzano class diagrams e interaction diagrams.
Per le viste di runtime, si usano component diagrams, class diagrams e interaction diagrams.
Per le viste di distribuzione, si usano soprattutto i package diagrams.
Per le viste di modulo, si usano i class diagrams, gli interaction diagrams e gli statecharts.
Strumenti a supporto delle decisioni architetturali
Spesso i designer di architetture usano dei tools di supporto alle decisioni da essi intraprese. In particolare:



Per il design di alto livello, si usano:
a) I DSSA (Domain Specific Software Architecture)
b) Gli stili architetturali
Si possono utilizzare inoltre i middleware e il loro modello a componenti (sistemi che definiscono i componenti
che il software deve avere e come essi devono essere organizzati).
Per il design dettagliato, si usano i design patterns.
I DSSA
I DSSA (Domain Specific Software Architecture) sono delle architetture di riferimento, già realizzate da altri, utilizzate
per sviluppare software in uno specifico contesto. Per esempio, esistono delle architetture ricorrenti che si usano per
il design di software nel contesto delle telecomunicazioni o dell’aereonautica. Risultano particolarmente utili se il
sistema da realizzare necessita di interfacciarsi con altri sistemi, perché si utilizza così un’architettura standard, che
semplifica la comunicazione con gli altri sistemi.
Tali architetture sono pensate per minimizzare i rischi e massimizzare la coerenza e la consistenza del sistema. Sono
inoltre riutilizzabili, perciò se una certa funzione risulta non necessaria o necessita di essere espansa, è possibile
intervenire su essa senza compromettere il resto dell’architettura.
Un DSSA è comprensivo di:
1.
2.
3.
4.
5.
Un modello del dominio;
Un insieme di requisiti di riferimento;
Un’architettura di riferimento;
Un’infrastruttura o ambiente a supporto del DSSA stesso;
Una metodologia di processo per instanziare, raffinare e valutare il DSSA stesso.
Gli stili architetturali
Il concetto di stile architetturale
Uno stile architetturale (o di design) è una rappresentazione formale di una conoscenza condivisa, che viene utilizzato
soprattutto nelle ingegnerie mature. Uno stile architetturale consente così di creare un vocabolario condiviso, creando
dei canoni di progettazione maturi.
Come abbiamo accennato nei capitoli introduttivi, l’ingegneria del software non ha ancora raggiunto la completa
maturità, ma ci si sta muovendo sempre di più in tale direzione.
Più precisamente, nell’ambito dell’ingegneria del software uno stile di design comprende:
1.
2.
86
I componenti che costituiscono il sistema, come ad esempio i client, i server, i database, i livelli di uno stile
gerarchico, ... .
Le interazioni tra i componenti, che possono essere semplici e famigliari, come le chiamate di funzioni e l’accesso
a variabili condivise, oppure complessi e ricchi dal punto di vista semantico, come i protocolli client server.
Ingegneria del Software 2
Capitolo 8: Software design
Lo stile architetturale client-server
Lo stile architetturale di tipo client-server è uno dei più semplici e noti. Esso prevede che esistano almeno due diversi
processi, che risiedono su altrettanti host; i due processi hanno delle interfacce ben definite che consentono di
accedervi e sono caratterizzati da ruoli diversi:

Client
Il client è quel processo che ricopre il ruolo attivo, occupandosi di effettuare richieste di servizio, indirizzate al
server. È il client ad occuparsi di inizializzare la comunicazione, attraverso l’invio di messaggi e di chiamate
remote.

Server
Il server ricopre un ruolo passivo: si limita a ricevere delle richieste e a fornire le relative risposte. Esso è in grado
quindi di offrire un determinato set di servizi.
Figura 85: Schema dello stile client-server
Figura 86: distribuzioni di funzionalità nello stile client-server 2-tier
L’allocazione delle funzionalità sui vari host che costituiscono il sistema può essere di diverso tipo. Supponendo che i
calcolatori esistenti siano 2, le possibilità che si hanno a distribuzione sono mostrate nella figura precedente.
Man mano che ci si sposta dalle architetture di sinistra verso le architetture presentate più a destra nella precedente
figura, si passa da architetture di tipo thin client ad architetture di tipo fat client. In particolare, abbiamo

Architettura distributed presentation
In questo tipo di architettura l’intelligenza risiede nel server, mentre il client si occupa solo di gestire la GUI; il
client non ha a suo carico tutte le questioni di rappresentazione dei dati, ma solo alcune (si nota infatti che parte
della GUI risiede sullo stesso tier che contiene i dati ed il livello applicativo, e per questo si parla di presentazione
distribuita).

Architettura remote presentation
In questo caso il client è responsabile di tutte le questioni di rappresentazione e visualizzazione.

Architettura distributed logic
Tale architettura prevede che parte della logica applicativa sia decentralizzata ed eseguita sul client.

Architettura remote data access
L’architettura remote data access è caratterizzata dal fatto che il client ha in carico sia la logica applicativa, sia le
operazioni di presentazione dei dati, mentre il server serve solo da modulo per accedere ai dati, tipicamente
attraverso un’interfaccia SQL.

Architettura distributed database
Nell’architettura a database distribuito, la gestione dei dati è eseguita in parte dal client e in parte dal server.
87
Capitolo 8: Software design
Ingegneria del Software 2
Negli ultimi anni si tende sempre di più ad utilizzare un’architettura in cui il client non incorpora la logica, ma si
occupa solo della presentazione dei dati; il client si connette perciò a dei server che implementano la logica applicativa
e comunica con il database server. Il vantaggio di tale approccio è che si disaccoppia la logica dalla rappresentazione e
dai dati. In questo caso è però necessario disporre di tre diversi livelli fisici, e si parla di architetture three-tier; le
principali architetture di questo tipo sono riassunte in figura.
Figura 87: Diverse distribuzioni di funzionalità nell’architettura client-server 3-tier
Dall’architettura a 3-tier si può poi passare ad architetture con un numero di livelli fisici superiori. Nelle figure seguenti
sono mostrati dapprima un esempio di architettura a 3-tier, e a destra è riportata un’architettura a 4 livelli fisici.
Figura 88: Esempio di sistema 3-tier client server
Figura 89: Esempio di sistema 4-tier client-server
In un’architettura a più tier gli oggetti possono essere:

Persistenti o volatili
Ad esempio, in JEE gli oggetti persistenti sono ottenuti mediante gli entity-beans.

Attivi o passivi
Esistono a tale scopo diversi modelli di coordinamento possibili.

Stateful o stateless
Alcuni oggetti devono tenere traccia del loro stato (oggetti stateful); tale risultato può essere ottenuto mediante
la memorizzazione di dati sul file system, mediante basi di dati ad oggetti o mediante una mappatura con un
database relazionale.
88
Ingegneria del Software 2

Capitolo 8: Software design
Locali o distribuiti
Se gli oggetti sono locali, significa che i riferimenti ad altri oggetti sono sempre riferimenti ad entità che risiedono
sullo stesso tier sul quale risiede il riferimento stesso. In caso contrario, sono remoti.
Gli oggetti distribuiti possono essere messi in stato dormiente e poi riattivati al momento dell’arrivo di una
richiesta; tali operazioni risultano però trasparenti all’attivazione.
Naturalmente, una chiamata remota ha tempi di risposta molto più alti di una chiamata locale, perciò gli oggetti
distribuiti hanno solitamente un numero ridotto di metodi che compiono un elevato numero di operazioni e
richiedono molti parametri; inoltre, con oggetti distribuiti possono sorgere problemi di affidabilità, di sicurezza
delle informazioni trasmesse e di localizzazione dei dati; a ciò si aggiunge che i riferimenti distribuiti hanno una
maggiore occupazione di memoria.
Gli oggetti possono essere eseguiti in parallelo, perciò si hanno in ogni caso dei problemi di sincronizzazione.
Una tecnica molto usata per la gestione di oggetti distribuiti è la migrazione, che consiste nell’effettuare una vera e
propria copia da una macchina all’altra degli oggetti da usare. Inoltre, in un sistema distribuito non è possibile
utilizzare strumenti come il garbage collector per l’eliminazione automatica degli oggetti.
Solitamente gli oggetti vengono tenuti in una memoria virtuale dalla loro creazione fino alla loro distruzione; tale
scelta potrebbe comportare dei problemi:
1.
2.
3.
Il numero di oggetti istanziati potrebbe essere molto elevato;
Gli oggetti potrebbero restare in memoria per molto tempo, pur essendo usati solo per brevi istanti;
Alcuni host potrebbero dover essere riavviati senza sospendere tutte le loro applicazioni; in questo modo,
l’applicazione verrebbe compromessa, perché i suoi dati risiedono nella memoria virtuale.
Si utilizza anche un meccanismo di attivazione e disattivazione: gli oggetti vengono portati nella memoria centrale
(attivati), elaborati, ed in seguito aggiornati dalla memoria centrale (disattivati).
È inoltre fondamentale definire un modello di esecuzione nei casi di failure e anche fare delle casistiche di sicurezza
per trasferire oggetti di diversi livelli di importanza.
Lo stile architetturale funzionale
Nello stile funzionale, il sistema è decomposto in operazioni astratte e le operazioni si conoscono a vicenda. I
connettori tra i vari componenti (cioè tra le vaie funzioni) sono rappresentati dalle operazioni di chiamata e ritorno di
una funzione. Altre possibili interconnessioni sono rappresentate dall’uso di dati condivisi.
Figura 90: Rappresentazione esemplificativa dello stile funzionale
89
Capitolo 8: Software design
Ingegneria del Software 2
Lo stile architetturale a livelli
Lo stile a livelli viene adottato per mettere in evidenza l’organizzazione gerarchica dei vari livelli che costituiscono il
sistema. Naturalmente tali livelli gerarchici rappresentano un’astrazione e questo modello non è del tutto sostitutivo
degli altri modelli che abbiamo presentato o che presenteremo a breve. Ogni diverso livello rappresenta una macchina
astratta.
La gerarchia può essere messa in evidenza mediante un classico grafo orientato aciclico (DAG), oppure mediante uno
schema a cipolla (onion-ring structure), come mostrato nella precedente figura.
Figura 91: Rappresentazioni alternative di uno stesso sistema con architettura a livelli
Lo stile architetturale pipes&filter
Lo stile architetturale pipes&filter è molto diverso da quelli precedentemente elencati; tale stile è costituito da una
serie di filtri (o componenti), ciascuno dei quali utilizza come dato d’ingresso l’output del filtro precedente e fornisce il
proprio output come input del successivo. Ciascun filtro ignora la presenza degli altri filtri.
Questo filtro è utilizzato dal sistema operativo Unix, nel quale i singoli comandi rappresentano i vari filtri:
ps … | more … |
Il termine pipe viene invece usato per indicare i flussi di dati tra i vari filtri. È possibile anche filtrare i risultati di diversi
componenti prima di fornirli come input del successivo filtro.
Lo stile prevede 2 possibili modalità per il controllo del flusso: si può scegliere l’approccio batch, ovvero sequenziale,
oppure l’approccio concorrenziale, che prevede che un filtro a monte di un altro possa continuare la propria
esecuzione anche mentre è in corso l’esecuzione del filtro a valle, fornendo così degli input in maniera continuativa.
I vantaggi di questo stile architetturale sono:
1.
2.
3.
La composizionalità, ovvero la possibilità di comporre tra loro componenti creati in maniera indipendente, in
maniera molto semplice e affrontando il problema solo a posteriori.
La riusabilità dei componenti.
La facilità di modifica, dovuta soprattutto al fatto che la sostituzione di un filtro non influenza gli altri. Possiamo
affermare che il comportamento globale del sistema è composta da tanti individuali comportamenti di ogni filtro.
Gli svantaggi fondamentali sono invece:
1.
2.
3.
90
L’assenza di supporto alla persistenza, a meno che un filtro rappresenti un repository; lo stile di per sé però non
supporta la memorizzazione persistente dei dati.
L’assenza di replicazione dei componenti.
L’approccio è solitamente di tipo batch, mentre l’approccio concorrente è scarsamente usato.
Ingegneria del Software 2
Capitolo 8: Software design
Lo stile architetturale peer2peer
Lo stile architetturale peer2peer è per certi versi contrapposto allo stile client-server: tale stile prevede infatti
l’assenza di una distinzione di ruoli tra i componenti, i quali interagiscono tra loro “da pari”, in maniera simmetrica e
mediante interazioni di richiesta e risposta di tipo bidirezionale (ciò significa che ognuno dei componenti può
comportarsi alternativamente come client e come server, ovvero ognuno di essi può essere il componente che dà
inizio all’interazione).
Ogni peer è inoltre un’entità autonoma rispetto a tutti gli altri: tutti i peer condividono dei servizi e traggono beneficio
reciproco dalla loro presenza, ma possono funzionare anche senza la presenza di uno o più dei restanti peer.
Un tipico esempio di applicazioni che seguono questo stile architetturale sono le applicazioni per il file sharing;
l’architettura è usata anche per condividere i cicli di CPU nelle architetture GRID.
I principali vantaggi di questo stile sono:
1.
2.
3.
La possibilità di avere una condivisione “democratica” delle informazioni;
La riduzione dei costi (non si ha bisogno di acquistare un server);
La presenza di ridondanza naturalmente insita nell’architettura, che consente così una maggiore sicurezza contro
la perdita dei dati.
Lo stile architetturale ad eventi
Lo stile architetturale ad eventi ha l’obiettivo di fornire uno schema di coordinamento tra componenti disaccoppiati
tra loro. I diversi componenti possono ricoprire il ruolo di mittenti o di destinatari di eventi; in quest’ultimo caso, è
necessario che il componente abbia notificato al mittente il proprio interesse a ricevere la notifica di eventi di una
determinata categoria.
È possibile individuare una tassonomia dei sistemi che adottano lo stile ad eventi; le principali categorie esistenti sono:

Sistemi broker mode
Questi sistemi prevedono che i riceventi inviino un messaggio di registrazione ad una certa categoria di eventi; a
tale scopo, il messaggio di sottoscrizione (subscribe message) viene inviato ad un event dispatcher, detto anche
broker o publish/subscribe service. Il broker rappresenta perciò un servizio di middleware, e l’azione di
sottoscrizione è di fatto una “dichiarazione di interesse” nei confronti di determinati eventi.
L’azione indicata con il termine publish è invece la generazione di un evento da parte del componente mittente.
L’evento, viene comunicato al broker, il quale lo invia in broadcast a tutti i componenti che si erano
precedentemente registrati per quella categoria di eventi.
Si noti che la registrazione può essere eseguita in base a caratteristiche diverse: ad esempio, si potrà avere una
sottoscrizione per “tutti i voli da Milano a New York nel mese di dicembre”, oppure una sottoscrizione per “tutti i
voli verso l’America con prezzo inferiore a 500 dollari”; il broker valuterà quindi di volta in volta, a seconda
dell’evento, verso quali componenti inviare la notifica dell’evento generato.
Il comportamento di questi sistemi è di tipo asincrono; inoltre, si tratta di un comportamento reattivo (la
computazione è guidata dalla ricezione di messaggi). Altra caratteristica importante è l’assenza di
accoppiamento: il mittente e i ricevitori vengono aggiunti senza la necessità di riconfigurare il sistema. È inoltre
bene sottolineare che la destinazione dei messaggi è determinata in sostanza dal ricevente stesso, e non dal
mittente, il quale non sa quali componenti riceveranno la notifica degli eventi che genera.
Questo approccio è oggi sempre più usato, soprattutto per le interfacce grafiche, che si sottoscrivono a tutti gli
eventi che devono comportare la modifica della loro stessa vista di presentazione; i vantaggi principali sono la
facilità con la quale è possibile aggiungere o cancellare dei componenti e la semplicità delle strategie di
integrazione adottate.
91
Capitolo 8: Software design
Ingegneria del Software 2
Per contro, si possono avere problemi di scalabilità e di ordinamento degli eventi, e non sempre è possibile
garantire la ricezione dei messaggi. Riguardo all’ordinamento degli eventi, sarebbe desiderabile che gli eventi
venissero ricevuti nell’ordine mediante il quale sono stati emessi; tuttavia, nella realtà dei fatti può non essere
così, perché la comunicazione è asincrona. I middleware possono allora cercare di adottare diversi approcci:
a) Possono limitarsi a non dare alcuna garanzia di ordinamento;
b) Possono dare una garanzia di ordinamento totale (nella realtà però ciò è quasi impossibile);
c) Possono garantire l’ordine causale: se un evento e2 è causato da un evento e1, allora viene garantito che e1
verrà ricevuto prima di e2.
d) Possono garantire l’ordine relativo rispetto ad un certo mittente, ovvero garantire che se lo stesso mittente
emette un evento e1 seguito dall’evento e2, allora il ricevente otterrà prima la notifica di e1 ed in seguito
quella di e2, e non viceversa.
Tra gli approcci elencati, quello che risulta di fatto essere solitamente il più indicato è quello che prevede la
garanzia di causalità.
Un ulteriore problematica alla quale abbiamo accennato in precedenza è la garanzia di ricezione dei messaggi:
anche in questo caso, i possibili approcci sono:
a)
b)
c)
d)

Approccio best effort (non si dà di fatto alcuna garanzia);
Approccio at least one (il messaggio viene ricevuto almeno una volta);
Approccio at most once (il messaggio viene ricevuto al più una volta);
Approccio once and only once (il messaggio viene ricevuto una ed una sola volta).
Sistemi listener mode
In questa tipologia di sistemi, il ricevitore si registra al mittente, creando così una comunicazione punto-punto.
L’approccio è perciò simile a quello visto per i sistemi broker mode, ma in questo caso la registrazione avviene in
maniera diretta tra l’osservatore (o listener) e l’elemento osservato, ovvero il componente che emette gli eventi.
Tale componente si occupa quindi anche di ricevere le sottoscrizioni e di gestirle.
L’approccio listener mode è adatto soprattutto alle applicazioni centralizzate.

Sistemi database trigger
L’approccio usato all’interno dei database prevede che i mittenti (senders) aggiornino le tabelle, eseguendo delle
opportune query di modifica della base di dati; ai ricevitori viene poi notificato il cambiamento, mediante
l’attivazione del trigger corrispondente all’evento di modifica avvenuto.
Si può affermare che tale approccio è in realtà una specializzazione del precedente: l’elemento osservato è la
tabella e gli osservatori sono i trigger ad essa associati.
C1
Messaggio evento
C2
1: sottoscrizione
3: ricezione
evento
Elemento
osservato
Broker
2: generazione
evento
C3
C4
C5
Figura 92: architettura ad eventi broker mode
92
Listener 1
Sottoscrizione
Sottoscrizione
Messaggio evento
Listener 2
Figura 93: Approccio ad eventi listener mode
Ingegneria del Software 2
Capitolo 8: Software design
Lo stile architetturale repository
In un sistema repository-based i componenti comunicano tra loro mediante l’uso di un magazzino, detto appunto
repository. Lo stile è perciò piuttosto simile all’architettura ad eventi broken mode, ma in questo caso l’elemento
centrale è il repository, nel quale i messaggi vengono memorizzati in maniera persistente (a differenza di quanto
accade con l’approccio ad eventi broker mode).
Il repository è di fatto una sorta di database, che si comporta in maniera passiva e persistente. I componenti sono
invece attivi. Le azioni sono talvolta raggruppate in transazioni.
I messaggi memorizzati all’interno del repository sono detti tuple; i componenti che leggono una tupla possono
decidere se togliere dal repository la tupla letta, o se lasciarla memorizzata al suo interno. Le letture sono nondeterministiche: nel caso in cui esistano più tuple che corrispondo al pattern adottato per interrogare il repository,
quest’ultimo restituirà una tupla scelta in maniera casuale tra quelle disponibili. L’accesso al repository è inoltre
bloccante.
Lo stile architetturale mobile code
In realtà, il termine mobile code racchiude in sé più di uno stile architetturale; tutti gli stili che vanno sotto questo
nome prevedono però che si abbia un’interazione tra client e server nella quale viene di fatto spostato del codice dal
server, eseguito poi localmente dal client.
Questi approcci vengono adottati soprattutto al fine di rendere più efficiente l’uso dei canali di comunicazione: se
un’operazione eseguita sul server dovesse richiedere un’interazione troppo forte con il client, è talvolta conveniente
lasciar eseguire l’operazione direttamente al client, fornendo a quest’ultimo un agente, ovvero un componente che
agisce in sua vece. Naturalmente, ciò è conveniente a patto che il trasferimento dell’agente (o agent) abbia un costo
ragionevole.
Gli approcci possibili sono 4:
Figura 94: Approccio mobile code di tipo client-server
Figura 95: Approccio mobile code di tipo remote evaluation
Figura 96: Approccio mobile code di tipo code on demand
Figura 97: Approccio mobile code di tipo mobile agent
93
Capitolo 8: Software design
Ingegneria del Software 2

Approccio client-server
Il client invia una richiesta al server, il quale la esegue in locale e invia solamente la risposta finale.

Approccio remote evaluation
Il client anziché inviare una semplice richiesta di servizio, include nel proprio messaggio di richiesta anche il
codice che deve essere eseguito dal server.

Approccio code on demand
Il client invia una richiesta al server, il quale gli invia il codice che poi il client eseguirà in locale.

Approccio mobile agent
In questo caso, si trasferisce da un calcolatore ad un altro un vero e proprio agente, il quale è dotato di un
proprio stato e durante la propria esecuzione può scegliere di trasferirsi da un computer ad un altro per sfruttare
le risorse del nuovo calcolatore.
Si parla di mobilità forte quando lo stato e il codice vengono trasferiti da un’unità di esecuzione ad un diverso
ambiente computazionale; si parla di mobilità debole quando solo il codice può essere trasferito tra ambienti
computazionali diversi.
Lo stile architetturale a plug-in
Un plug-in è un frammento di codice che aggiunge funzionalità ad un’applicazione, detta host application. Tale
applicazione deve offrire opportuni meccanismi per l’aggiunta dei plug-in stessi, i quali a loro volta devono sottostare
ai vincoli di estensibilità definiti dalla host application e, in generale, dall’architettura.
Alcuni tipici esempi di applicazioni sviluppate sulla base dello stile a plug-in sono Eclipse e i filtri grafici di Photoshop.
I vantaggi derivanti dall’adozione di questo stile architetturale sono molteplici; per gli sviluppatori:
1.
2.
3.
4.
5.
È possibile implementare e incorporare nuove funzionalità per l’applicazione in maniera molto rapida.
Siccome i plug-in sono separati dall’applicazione e comunicano con essa mediante interfacce ben definite, è
possibile isolare e risolvere rapidamente eventuali problemi del funzionamento dell’applicazione.
È possibile creare versioni personalizzate dell’applicazione senza intervenire sul sorgente dell’applicazione stessa.
Parti terze possono aggiungere funzionalità all’applicazione;
Le interfacce per i plug-in possono essere usate per adattare il codice di sistemi preesistenti, scritti in altri
linguaggi di programmazione.
Inoltre, per gli utenti finali è possibile personalizzare le funzionalità e le caratteristiche dell’applicazione, ed
eventualmente si possono disabilitare alcune funzionalità, in modo da ridurre l’occupazione in memoria, aumentare le
prestazioni e semplificare l’interfaccia utente.
Per definire un’architettura a plug-in, è necessario:
1.
2.
3.
Definire una lista di metodi o funzioni che un plug-in deve implementare, oppure definire una classe di base che
dovrà essere usata dai plug-in.
Definire un meccanismo per la registrazione.
Definire quale tipo di comportamento deve essere esibito da ogni metodo o funzione.
La dichiarazione dei plug-in avviene tipicamente in XML, mentre l’implementazione avviene, ovviamente, in un
opportuno linguaggio di programmazione (ad esempio, Java): si ha perciò un’importante distinzione tra questi 2
concetti.
In particolare, la dichiarazione di un file avviene mediante un manifest file, che specifica le interconnessioni del plug-in
con il mondo esterno, in termini di punti di estensione e di estensioni a punti di estensione definiti da altri plug-in.
94
Ingegneria del Software 2
Capitolo 8: Software design
Ad esempio, per estendere un plug-in in Eclipse, lo sviluppatore del plug-in da estendere deve aver definito
un’interfaccia associata al punto di estensione. Il plug-in che lo estende deve fornire un oggetto che implementi tale
interfaccia, in modo tale da consentire al plug-in esteso di interagire con il nuovo plug-in. L’interazione nella direzione
contraria dipende invece dall’implementazione.
Il caricamento dei plug-in avviene allo start-up dell’applicazione: vengono individuati tutti i plug-in disponibili, si
leggono i relativi manifest file e si costruisce un registro dei plug-in nella memoria centrale; i plug-in non possono
essere aggiunti durante l’esecuzione dell’applicazione.
I plug-in vengono poi attivati solo quando è effettivamente necessario eseguire il loro codice. Una volta attivati, i plugin utilizzano il registro dei plug-in presente in memoria centrale per accedere alle loro estensioni.
Lo stile architetturale SOA
Lo stile architetturale SOA (Service Oriented Architecture) prevede la presenza di un’infrastruttura aperta, nella quel i
componenti sono raggruppati e pubblicati come servizi, ovvero i componenti sono accompagnati da un contratti
rivolto ai clienti, nel quale viene specificata la qualità del servizio offerto. In questo modello architetturale però non è
sufficiente fornire un’interfaccia semantica: deve infatti essere possibile individuare i servizi esistenti.
I nuovi servizi possono essere costruiti componendo dei servizi preesistenti; inoltre, ogni servizio viene eseguito
all’interno del proprio dominio.
L’architettura prevede la presenza di 3 diversi ruoli:
1.
2.
3.
Richiedenti del servizio, ovvero coloro che richiedono l’esecuzione di un certo servizio;
Fornitori del servizio;
Intermediari (come il broker, i service registry, …), che forniscono informazioni riguardanti altre informazioni
(metainformazioni) oppure riguardanti i servizi.
Le interazioni tra i 3 ruoli appena elencati avvengono attraverso:
1.
2.
3.
Pubblicazione della descrizione di un servizio, effettuata dal fornitore del servizio interagendo con il service
registry (che è appunto l’intermediario che ha il compito di gestire le descrizioni dei servizi esistenti). Questa
operazione è detta anche service descritpion publication.
Ricerca di un servizio (service discovery).
Interazione tra il richiedente e il fornitore del servizio (service binding).
Tali interazioni sono riassunte anche all’interno della seguente figura.
Service
description
Service
registry
UDDI
find
WSDL
publish
Service
Service
requestor
bind
SOAP
Service
provider
Service
description
Figura 98: Schema delle interazioni previste dall’architettura SOAP
95
Capitolo 8: Software design
Ingegneria del Software 2
La principale differenza tra un’applicazione SOA e un sistema component-based è che un’organizzazione che dispone
di un sistema basato sui componenti acquisisce i componenti e li mette insieme fino a costruire il sistema completo; il
componente perciò, una volta acquisito, viene eseguito nel dominio dell'organizzazione che l'ha acquisito. Nei sistemi
SoA invece si segue un diverso approccio: l’organizzazione utilizza dei “pezzi” messi a disposizione da altre
organizzazioni, ma tali porzioni di software (che rappresentano dei servizi) non vengono vendute, non si spostano, e
l’organizzazione che li deve eseguire può solamente utilizzarli attraverso l'interfaccia a servizi.
L’esempio tipico dell’architettura SOA è quello dei Web Service: il tradizionale paradigma del web viene esteso in
modo da poter ricercare anche delle applicazioni (servizi), anziché solamente delle informazioni passive. Per fare ciò si
utilizzano dei protocolli standard per lo scambio dei dati (XML e SOAP).
In tal modo, le applicazioni che interagiscono tra loro appartengono a domini amministrativi diversi, e ciascuna
espone una certa interfaccia; per una certa funzionalità potranno essere individuati diversi fornitori di servizi.
I problemi di questo tipo di approccio sono soprattutto:
1.
2.
3.
4.
5.
6.
La sicurezza: trattandosi di un sistema aperto nel quale possono essere trasmesse informazioni critiche, è
necessario avere un certo grado di fiducia nei servizi che vengono utilizzati;
Affidabilità dello scambio di messaggi;
Controllo della qualità del servizio;
Manutenibilità del servizio;
Gestione delle transazioni;
Composizione dei servizi;
I design pattern
Il concetto di design pattern
I design pattern sono degli schemi che rappresentano il modo di gestire i componenti in un sistema software,
solitamente sviluppato mediante un linguaggio ad oggetti; essi hanno perciò l’obiettivo di rendere più semplice lo
sviluppo di moduli (che, come accennato, saranno in tal coso principalmente delle classi).
Per fare ciò, i design pattern forniscono delle linee guida per l’interazione reciproca tra le classi, costituendo così degli
schemi riutilizzabili e adattabili a sistemi diversi.
La differenza principale rispetto agli stili architetturali è data dal fatto che i design pattern riguardano il design in
piccolo: essi fanno riferimento al design di singoli moduli e delle loro interazioni con il sistema. In ogni caso, non
sempre il confine tra questi due concetti è netto e ben definito.
I vantaggi derivanti dall’uso dei design pattern sono diversi:
1.
2.
3.
Siccome si utilizza dell’informazione consolidata, ovvero vengono usati dei paradigmi già sviluppati, testati e
consolidati da altri, il processo di sviluppo risulta essere più rapido;
Forniscono un linguaggio comune agli sviluppatori;
Forniscono una base solida per creare del codice più robusto rispetto a quello che si otterrebbe mediante schemi
realizzati ad-hoc.
Per contro, i pattern introducono spesso delle ridondanze che possono portare ad inefficienze: la riusabilità del
pattern comporta un certo livello di astrazione che potrebbe complicare le cose, soprattutto in progetti troppo piccoli,
e questa è una delle ragioni per le quali talvolta non vengono usati.
96
Ingegneria del Software 2
Capitolo 8: Software design
Classificazione dei design pattern
I design pattern possono essere classificati in 3 diverse categorie:

Pattern creazionali
Sono pattern che nascondono i costruttori delle classi, mettendo a disposizione degli utilizzatori dei metodi
opportuni che consentono la creazione delle relative istanze. In tal modo vengono forniti meccanismi ad-hoc per
la creazione delle nuove istanze di una nuova classe, senza che l’utilizzatore conosca tali meccanismi.

Pattern strutturali
Sono pattern che forniscono delle semplici modalità per realizzare relazioni e composizioni tra le entità esistenti.
Tali pattern consentono perciò di usare gli oggetti esistenti, fornendo agli utilizzatori un’interfaccia più adatta
alle loro esigenze.

Pattern comportamentali
Questi pattern forniscono degli schemi per la comunicazione tra gli oggetti esistenti.
I pattern creazionali
Tra i pattern creazionali, i più noti sono:

L’abstract factory
Questo pattern, il cui nome significa letteralmente, "fabbrica astratta", fornisce un'interfaccia per creare famiglie
di oggetti connessi o dipendenti tra loro, in modo che non ci sia necessità da parte degli utilizzatori di specificare i
nomi delle classi concrete all'interno del proprio codice.

Il Factory method
Letteralmente “metodo fabbrica”, questo pattern fornisce un’interfaccia per creare un oggetto, ma lascia che le
sottoclassi decidano quale oggetto istanziare.

Il Builder
Questo pattern separa la costruzione di un oggetto complesso dalla sua rappresentazione, in modo che il
processo di costruzione stesso possa creare diverse rappresentazioni.

Il Prototype pattern
Si tratta di un pattern che permette di creare nuovi oggetti clonando un oggetto iniziale, o prototipo.

Il Singleton
È un pattern che ha lo scopo di assicurare che di una classe possa essere creata una sola istanza e fornisce un
unico accesso ad essa, negando così l’accesso ai costruttori.
Tra tutti i pattern appena elencati, noi ci soffermeremo soprattutto sul pattern singleton, che risulta essere il più
semplice da analizzare. Tale pattern viene usato quando è necessario che in una certa applicazione non esistano
diverse istanze di una stessa classe, come ad esempio durante il login su un server: tutte le parti devono utilizzare la
stessa istanza per loggarsi sul server, il quale non dovrebbe essere più in grado di cambiare l’istanza una volta
effettuato il login.
Figura 99: Class diagram che rappresenta il pattern singleton
Per assicurare che la classe abbia al massimo un’istanza, si fornisce un punto di accesso globale all’istanza stessa: tale
punto di accesso è il metodo pubblico getInstance(), generalmente di tipo syncronized (in modo da consentirne
97
Capitolo 8: Software design
Ingegneria del Software 2
l’utilizzo in applicazioni multi-thread), definito all’interno della classe Singleton, che rappresenta la classe di cui si
vuole avere una sola istanza. Naturalmente, il metodo getInstance deve essere statico.
In genere, si sceglie di implementare una classe astratta Singleton esattamente come descritta dal precedente
diagramma UML; le classi che devono essere singleton verranno semplicemente dichiarate come eredi di tale classe.
Il costruttore della classe Singleton deve essere dichiarato privato, in modo che non sia possibile accedervi
dall’esterno. Di seguito è riportato il codice di una possibile implementazione in Java di questo pattern:
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {}
private synchronized static void createInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
public static Singleton getInstance() {
createInstance();
return INSTANCE;
}
}
I pattern strutturali
Tra i pattern strutturali, i più diffusi sono:

L’Adapter
Questo pattern converte l’interfaccia di una classe in una interfaccia diversa.

Bridge
Un pattern che permette di separare l’astrazione di una classe dalla sua implementazione, per permettere loro di
variare indipendentemente.

Il Composite
È un pattern utilizzato per dare la possibilità all'utilizzatore di manipolare gli oggetti in modo uniforme, organizza
gli oggetti in una struttura ad albero.

Il Decorator
Consente di aggiungere metodi a classi esistenti durante il run-time (cioè durante lo svolgimento del
programma), permettendo una maggior flessibilità nell'aggiungere delle funzionalità agli oggetti.

Il Façade
Permette, attraverso un'interfaccia più semplice, l'accesso a sottosistemi che espongono interfacce complesse e
diverse tra loro.

Flyweight
Letteralmente “peso piuma”, è un pattern che permette di separare la parte variabile di una classe dalla parte
che può essere riutilizzata.

Proxy
Il pattern fornisce una rappresentazione di un oggetto di accesso difficile o che richiede un tempo importante
per l’accesso o creazione. Il Proxy consente di posticipare l’accesso o creazione al momento in cui sia davvero
richiesto.
98
Ingegneria del Software 2
Capitolo 8: Software design
Focalizziamo ora la nostra attenzione sul pattern proxy. Questo pattern viene usato sia in contesto distribuito, sia in
contesto centralizzato, allo scopo di consentire a più client di interagire con i servizi offerti da un server, ma non in
maniera diretta, al fine di rendere trasparente la localizzazione del servizio e di garantire sicurezza ed efficienza.
Per implementarlo è perciò necessario implementare un intermediario (solitamente una classe apposita) che svolga il
ruolo di intermediario, eseguendo delle azioni pre e post-processo.
Di seguito è riportato il diagramma UML relativo al pattern in esame:
Figura 100: Class diagram che rappresenta il pattern proxy
Figura 101: Sequence diagram che rappresenta il pattern proxy
Come si osserva dal class diagram, il server ed il proxy devono avere la stessa interfaccia, in modo tale che il client non
si accorga, durante le interazioni, se il proprio interlocutore è il client oppure il server.
Esistono diverse tipologie di proxy:

Proxy remoto
Fornisce un riferimento a un oggetto localizzato in un diverso spazio di indirizzamento, su macchine differenti o
sulla stessa macchina.

Cache proxy
Consente l’immagazzinamento temporaneo di risultati in modo che diversi client possano condividere i risultati.

Proxy di protezione dell’accesso
Permette a diversi client l’accesso al servizio con differenti livelli di permesso.

Proxy virtuale
Consente la creazione di oggetti che richiedono un grande costo solo quando è strettamente necessario,
nascondendo questa ottimizzazione.

Smart reference proxy
Fornisce azioni addizionali quando un oggetto viene referenziato.

Copy-on-write proxy
Rinvia la copia o clonazione di un oggetto fino all’istante in cui è effettivamente richiesta dal client.
99
Capitolo 8: Software design
Ingegneria del Software 2
I pattern comportamentali
Tra i pattern comportali più importanti, possiamo ricordare:

Chain of Responsibility
Letteralmente “catena di responsabilità”, è un pattern che diminuisce l’accoppiamento fra l’oggetto che effettua
una richiesta e quello che la soddisfa, dando a più oggetti la possibilità di soddisfarla.

Il Command
Permette di isolare la porzione di codice che effettua un'azione dal codice che ne richiede l'esecuzione.

L’Interpreter
Dato un linguaggio, definisce una rappresentazione della sua grammatica insieme ad un interprete che utilizza
questa rappresentazione per l’interpretazione delle espressioni in quel determinato linguaggio.

L'Iterator
Risolve diversi problemi connessi all'accesso e alla navigazione attraverso gli elementi di una struttura dati, senza
esporre i dettagli dell’implementazione e della struttura interna del contenitore. Si tratta dei classici iteratori
Java.

Il Mediator
Si interpone nelle comunicazioni tra oggetti, allo scopo di aggiornare lo stato del sistema quando uno qualunque
di essi comunica un cambiamento del proprio stato.

Il Memento
Pattern il cui nome sta per “promemoria”; è l'operazione di estrarre lo stato interno di un oggetto, senza violarne
l’incapsulazione, e memorizzarlo per poterlo ripristinare in un momento successivo.

L’Observer
Definisce una dipendenza uno a molti fra oggetti diversi, in maniera tale che se un oggetto cambia il suo stato,
tutti gli oggetti dipendenti vengono notificati del cambiamento avvenuto e possono aggiornarsi.

Lo State
Permette ad un oggetto di cambiare il suo comportamento al cambiare di un suo stato interno.

Lo Strategy
È un pattern utile in quelle situazioni dove è necessario modificare dinamicamente gli algoritmi utilizzati da
un’applicazione.

Il Template method
Ovvero “metodo schema”, permette di definire la struttura di un algoritmo lasciando alle sottoclassi il compito
di implementarne alcuni passi come preferiscono.

Il Visitor
Permette di separare un algoritmo dalla struttura di oggetti composti a cui è applicato, in modo da poter
aggiungere nuovi comportamenti senza dover modificare la struttura stessa.
100
Ingegneria del Software 2
Capitolo 8: Software design
Analizziamo ora nel dettaglio il pattern Observer. Al suo interno, si individuano due ruoli diversi: il soggetto (che è
l’osservato) e gli osservatori (o listener). Lo scopo è quello di consentire ai listener di osservare come varia lo stato del
soggetto osservato, in modo che si possano aggiungere durante l’esecuzione dei nuovi listener.
Figura 102: Class diagram che rappresenta il pattern observer
Figura 102: Sequence diagram del pattern observer
Questo pattern dovrebbe essere utilizzato quando i cambiamenti del soggetto devono essere inviati in broadcast a
diversi observer, e il soggetto non conosce chi siano esattamente i listener in ascolto.
I pattern architetturali
Il concetto di pattern architetturali
I pattern architetturali sono degli schemi architetturali ricorrenti, definiti ad un livello di astrazione più elevato rispetto
ai design pattern.
Il pattern MVC
Tra i pattern architetturali più noti ed utilizzati, va citato certamente il pattern MVC (Model-View-Controller), il quale
consente la separazione tra il modello dei dati, la logica applicativa e la presentazione dei dati; tali elementi sono
infatti implementati in 3 componenti distinti:

Model
È la rappresentazione delle informazioni che riguardano lo specifico dominio applicativo, indipendentemente
dalle operazioni che su tali dati verranno eseguite.

View
Presenta i dati in modo tale che sia possibile interagire con essi, tipicamente mediante un’interfaccia utente.

Controller
Risponde agli eventi, solitamente rappresentati da azioni dell’utente, invocando cambiamenti sul modello e,
talvolta, anche sulla view.
In questo modo, la modifica di ciascuno dei tre componenti ha un impatto minimale sugli altri.
Il pattern MVC è stato definito per la prima volta nel 1979, nel linguaggio di programmazione Smalltalk (il primo
linguaggio ad oggetti). In realtà il pattern MVC è considerato la composizione di pattern diversi tra quelli finora
elencati. Ad esempio, la sincronizzazione tra i 3 diversi componenti viene ottenuta mediante l’applicazione del pattern
observer. In alcuni la view interagisce solamente con il controller; in altri casi (più frequenti), è il model a segnalare i
propri cambiamenti alla view.
101
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Capitolo 9: Verifica e validazione
Introduzione e terminologia
A questo punto della nostra trattazione, abbiamo analizzato tutte le fasi di progettazione del software; oltre a tali fasi,
è necessario eseguire anche la fase di implementazione e quella di verifica e validazione.
In questo capitolo vogliamo soffermarci su quest’ultima, in modo tale da stabilire quali sono i criteri attraverso i quali
accettare o meno il software prodotto e quali sono le possibili modalità attraverso le quali individuare errori di vario
genere.
La differenza tra verifica e validazione
Prima di tutto, è bene operare una distinzione tra i due concetti di verifica e di validazione del software.

Verifica del software
La verifica del software consiste nel verificare se l’applicazione è stata costruita in modo corretto rispetto alle
specifiche che sono state definite nelle precedenti fasi.

Validazione del software
La validazione del software consiste nel verificare se “è stata costruita l’applicazione corretta”, ovvero se il
software è corretto rispetto alle aspettative del cliente.
Naturalmente, è possibile che un software superi la fase di verifica, ma non quella di validazione: ciò accade
tipicamente a causa di errori nell’individuazione delle specifiche del software.
A che cosa servono le fasi di verifica e validazione?
Come si può facilmente intuire, il software completamente privo di errori e/o difetti è impossibile da ottenere;
partendo da questa semplice constatazione, si ricava la necessità di una continua e attenta attività di verifica del
software e di tutti i prodotti che si ottengono durante il suo ciclo di vita: non solo il codice vero e proprio, ma anche il
documento di specifica dei requisiti, il design document, … . In altri termini, l’attività di verifica e validazione non deve
essere eseguita solo al termine del processo di creazione del software.
Complessità della verifica del software
La verifica e validazione del software risulta particolarmente complessa, perché il software è un prodotto il cui
comportamento è per definizione discreto: valutare il risultato di una funzione in corrispondente di un certo valore
non ci dà alcuna informazione sul comportamento che la funzione ha in corrispondenza di qualsiasi altro valore.
Inoltre, la verifica di molte qualità non può essere eseguita pensando di ottenere solo un valore di verità del tipo “è
rispettata” o “non è rispettata”; a ciò si aggiunge la difficoltà di individuare alcune proprietà, che sono enunciate solo
implicitamente, e la soggettività delle proprietà stesse. Altre difficoltà derivano dal fatto che l’introduzione di nuove
tecnologie può portare nuovi problemi ed errori.
Tutte le problematiche sopra elencate rendono necessario individuare un modo per ottenere il “giusto mix” di verifica
e validazione per lo specifico software in oggetto.
102
Ingegneria del Software 2
Capitolo 9: Verifica e validazione
Tempi e modalità della fase di verifica e validazione
Quando eseguire la verifica e validazione?
La fase di verifica e validazione deve iniziare sin dall’inizio del processo si sviluppo del prodotto.
Ad esempio, durante lo studio di fattibilità occorre considerare le funzionalità e i requisiti di qualità, con i loro impatti
e i loro costi. Per tale ragione, esiste la figura del quality manager, che partecipa allo studia di fattibilità focalizzandosi
sul controllo della qualità e sulle modalità attraverso le quali deve essere gestito e pianificato tale controllo durante
tutto lo sviluppo del prodotto software. Il quality manager può inoltre influenzare la scelta iniziale dell’architettura del
sistema, in modo che venga assicurata la possibilità di testare e analizzare il sistema stesso con maggiore semplicità.
Le tecniche della verifica e validazione
La scelta delle tecniche mediante le quali effettuare la verifica e validazione del software dipende da vari parametri,
come la qualità, i costi, i vincoli di tempo e di risorse a disposizione.
Solitamente si utilizza un insieme di tecniche diverse, perché ogni tecnica potrebbe risultare efficace per una
particolare categoria di problemi, oppure potrebbe essere applicabile solo in certe fasi del progetto, o comunque
potrebbe avere diversi obiettivi o diversi rapporti tra costi e risultati garantiti.
Quando il prodotto è pronto per essere messo sul mercato?
Come abbiamo detto, individuare tutti gli errori è praticamente impossibile; tuttavia, ad un certo punto si rende
necessario immettere sul mercato il prodotto, e perciò non è possibile portare avanti la fase di testing all’infinito.
Occorre quindi stabilire dei parametri per poter decidere quando il prodotto è pronto per essere commercializzato.
Una possibilità è quella di stabilire dei parametri per i test, come ad esempio:

Copertura del codice
Si stabilisce una certa percentuale di copertura del codice, e il software viene rilasciato solo quando il testing ha
coperto la percentuale di codice prefissata.

Copertura di funzionalità
Si stabilisce una certa percentuale di copertura delle funzionalità, e il software viene rilasciato solo quando la
percentuale prefissata delle funzionalità da esso fornite è stata già testata.
Si possono inoltre utilizzare delle altre misure per valutare se il prodotto è pronto:

Disponibilità
Si valuta la qualità del servizio in termini di rapporto tra tempo durante il quale è disponibile e tempo durante il
quale è “down”.

MTBF (Mean Time Between Failure)
È l’intervallo di tempo che intercorre tra due guasti successivi.

Affidabilità
L’affidabilità è la percentuale che indica quante delle operazioni eseguite dal sistema sono terminate
correttamente.
Nella pratica però capita molto spesso che il software viene semplicemente rilasciato quando scade il tempo
prefissato dal cliente, oppure quando il budget a disposizione per il progetto è terminato; tale scelta comporta però
molto spesso una lunga serie di problemi.
103
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Verifica e validazione dei rilasci successivi del software
Quando si rilascia una nuova versione di un software, è necessario verificare che i test già eseguiti sulle precedenti
release siano ancora superati. Si parla perciò di regression testing: tali test vengono eseguiti per verificare se il nuovo
software è in grado di fornire ancora tutte le funzionalità che forniva prima. Questi test vengono solitamente eseguiti
con strumenti automatici, per evitare che i tempi diventino troppo lunghi.
Naturalmente, è possibile aggiungere anche dei nuovi test, oltre a quelli di regressione (relativi ad esempio a nuove
funzionalità aggiunte nella versione del software che sta per essere rilasciata).
Il processo di verifica e validazione
Processo di V&V
Vediamo ora di descrivere nel dettaglio come è strutturato il processo di verifica e validazione (V&V, Verification and
Validation). Questo processo non solo deve seguire tutte le fasi del processo di sviluppo del software, ma deve anche
prevedere delle soluzioni per il proprio miglioramento, mediante l’identificazione degli errori più comuni che sono
stati commessi in passato e la realizzazione di test automatici per verificare che tali errori non si ripetano in seguito. In
sostanza quindi è necessario stabilire procedure automatizzate di identificazione dei casi di test.
A tale scopo, è possibile usare degli strumenti automatici che consentono di tener traccia dei bachi verificatisi in
passato e di come sono stati gestiti. Tali tool sono spesso integrati in sistemi di Configuration Management; talvolta
esistono però anche degli strumenti opensource (ad esempio, Bugzilla), non collegati ad altri tool.
Le fasi che costituiscono il processo di verifica e validazione sono 4:
1.
2.
3.
4.
Definizione di quali dati riguardanti gli errori è necessario raccogliere;
Analisi dei dati raccolti al fine di identificare le categorie di appartenenza degli errori;
Analisi delle classi di errori e identificazione dei punti di debolezza del processo di qualità e sviluppo, mediante
opportune misure;
Perfezionamento del processo di qualità e sviluppo.
Il “V model”
La figura riporta il cosiddetto “V model”, adottato come modello di processo di verifica e validazione del software.
Figura 103: il “V model”
In questa figura sono indicate in colore più scuro e sul “lato destro” della V tutti i prodotti software realizzati; sul “lato
sinistro”, rappresentati mediante un grigio più chiaro, sono invece indicati i prodotti del processo di sviluppo del
software che non sono rappresentati da codice, ma da parti di documentazione (sono cioè i documenti prodotti nelle
fasi alte dello sviluppo). Le frecce indicano invece le attività di verifica (in bianco) e di validazione (in grigio). Come si
nota, la validazione è un’attività che richiede la comunicazione con l’esterno, mentre la verifica avviene internamente
all’azienda.
104
Ingegneria del Software 2
Capitolo 9: Verifica e validazione
Pianificazione e monitoraggio
Quando si effettua una pianificazione dell’analisi e dei test su un prodotto software, è necessario identificare:
1.
2.
3.
4.
5.
Obiettivi e scopi;
Documenti e oggetti che devono essere disponibili per eseguire le diverse attività in garanzia della qualità;
Gli elementi e le caratteristiche che richiedono di essere testati;
Le attività di analisi e test da eseguire;
Il personale da coinvolgere.
Nella pianificazione devono inoltre essere inclusi i vincoli, i criteri per stabilire se il test viene superato oppure no, una
previsione dei tempi, un’analisi dei rischi, ed una serie di requisiti hardware e software.
Gli obbiettivi della qualità devono essere chiaramente definiti, ragionevoli e bisogna identificare le relazioni con gli
obbiettivi degli altri progetti in termini di qualità.
Approcci alla verifica e alla validazione
Gli approcci possibili per la verifica e validazione del software sono principalmente 2: il testing e l’analisi
Il testing
L’approccio del testing prevede che si sperimenti il comportamento del prodotto e si cerchino dei controesempi
attraverso dei comportamenti in corrispondenza di particolari input. Si tratta quindi di una tecnica dinamica (richiede
cioè l’esecuzione del codice)
L’analisi
L’approccio dell’analisi prevede invece lo studio analitico delle proprietà del software. Siccome non richiede
l’esecuzione del codice, si dice anche che l’analisi è una tecnica statica.
L’analisi può a sua volta prevedere due diversi approcci fondamentali:
1.
2.
Ispezione manuale
Analisi statica automatizzata
L’approccio dell’analisi può essere adottato in ogni fase del processo di sviluppo, ed è particolarmente adatto alle fasi
alte di tale processo, quando non si ha ancora a disposizione il codice da eseguire per il testing.
Tecniche di ispezione del software
Per “ispezione del software” si intende la revisione formale da parte di colleghi di pari livello di un prodotto del ciclo di
vita del software, con lo scopo di trovarne i difetti.
La prima formalizzazione delle ispezioni è dovuta a Michael Fagan che le ha impiegate a lungo all’interno di IBM. La
tecnica di ispezione del software, pur essendo una tecnica piuttosto “rudimentale” (non richiede alcuna tecnologia
particolare, anzi, è una tecnica del tutto manuale) risulta spesso efficace. Come accennato dalla definizione
precedentemente fornita, questa tecnica non viene usata solo per il codice, ma anche per tutti gli altri documenti
ottenuti nel corso del ciclo di vita del software (il documento di design, l’SRS o RASD, …).
I ruoli che vengono individuati nell’ispezione del software sono:



Il moderatore, ovvero una persona che non partecipa al progetto, e che si occuperà di presiedere i meeting, di
scegliere i partecipanti e controllare il processo.
I tester e i reader ovvero coloro che si occupano di leggere il codice o il documento ricercando gli errori. I reader si
limitano ad analizzare il codice, mentre i tester svolgono una vera e propria esecuzione manuale.
Gli autori del codice (o del documento), che assistono passivamente, limitandosi a rispondere a eventuali
domande.
105
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Il processo di ispezione del software è solitamente articolato nelle seguenti fasi:
1.
2.
3.
4.
5.
6.
Pianificazione (il moderatore controlla i criteri iniziali, sceglie i partecipanti e fissa i meeting);
Overview (si fornisce un minimo di background ai partecipanti e si assegnano i ruoli; viene fornita una checklist in
cui sono descritti degli aspetti critici da controllare a seconda del linguaggio usato)
Preparazione.
Ispezione.
Rework (lo sviluppatore si occupa di “sistemare” il codice, correggendo gli errori individuati).
Follow-up ed eventuali re-ispezioni (il moderatore verifica che i problemi del codice siano stati risolti ed
eventualmente si procede con una nuova ispezione).
Analizziamo ora come avviene nel dettaglio l’ispezione. Essa si svolge attraverso dei meeting, ciascuno dei quali ha lo
scopo di individuare il maggior numero di errori possibili. Per fare ciò, si realizzano solitamente 2 meeting al giorno,
della durata di 2 ore ciascuno, cercando di analizzare una media di 150 linee di codice all’ora. L’approccio adottato è
quello di parafrasare il codice linea per linea, ricostruirne l’intento e fare un test a mano (con carta e penna). Gli errori
individuati non devono essere corretti: il moderatore verifica che ciò non avvenga, perché sarà poi l’autore del codice
ad apportare le necessarie correzioni (in questa fase è sufficiente riportare gli errori in un log).
La tipologia di errori da ricercare viene definita preventivamente mediante un’apposita checklist. Il contenuto di
questa lista dipende da vari parametri, come ad esempio il tipo di progetto ed il linguaggio di programmazione, ma in
ogni caso può contenere dei controlli di basso livello (riguardanti ad esempio il rispetto degli standard di
programmazione, come le regole seguite per l’attribuzione dei nomi a variabili e costanti), così come controlli più di
alto livello, quali l’aderenza con le fasi precedenti dello sviluppo (con il Design Document).
Per incentivare l’individuazione di errori durante l’ispezione, una possibile strategia è quella di non usare per la
valutazione personale degli autori del codice gli errori individuati durante l’ispezione stessa; gli errori trovati nella fase
di testing (che è quella successiva) vengono invece valutati.
L’ispezione del software è una tecnica efficace sotto diversi punti di vista:
1.
2.
3.
4.
È conveniente dal punto di vista economico.
È un approccio formale e dettagliato, che tiene conto dell’intero spazio degli input.
Può essere applicata anche al software incompleto.
Tiene conto di aspetti sociali come l’esperienza dell’autore.
L’approccio ha però anche alcune limitazioni:
1.
2.
Si tratta per natura di un approccio che permette di effettuare solo dei controlli sulle singole unità, e non dei
controlli di integrazione sul sistema.
Il test non può essere eseguito in modo incrementale: se si realizza una nuova versione del software, è necessario
effettuare nuovamente l’intera ispezione.
L’analisi automatica
L’analisi automatica è un insieme di tecniche che permettono di individuare errori in maniera automatizzata. Tra
queste tecniche, le principali sono:

Data flow analysis
La tecnica data flow analysis è basata sull’identificazione delle definizioni delle variabili e dei punti nei quali le
variabili vengono usate. Questa tecnica viene tipicamente usata dai compilatori per controllare errori come l’uso
di variabili non ancora inizializzate, ma anche per ottimizzare il codice. I possibili controlli effettuati sono:
a)
106
Tutte le variabili vengono utilizzate solo dopo l’inizializzazione? Per fare ciò, si costruisce il diagramma di
flusso che rappresenta l’uso e l’assegnamento delle variabili, e si verifica se in tutti i percorsi le variabili
vengono inizializzate prima di essere usate.
Ingegneria del Software 2
Capitolo 9: Verifica e validazione
b) Esistono variabili dichiarate ma mai usate? Per eseguire questo controllo, si usa ancora il diagramma di
flusso dell’uso e dell’assegnamento delle variabili, e si verifica se esiste almeno un percorso in cui la variabile
viene dichiarata e non usata: se sì, questo viene considerato un errore.
c) Esistono variabili che prima di essere usate hanno ogni volta un valore diverso?
Si noti che i punti b e c non sono di per sé degli errori, ma potrebbero essere sintomi di errore. Il problema di
questo approccio è che assume un punto di vista pessimistico: non è possibile sapere durante il controllo se
esistono percorsi che in realtà non verranno mai eseguiti. Inoltre, non si può sempre determinare se due nomi o
espressioni si riferiscono allo stesso oggetto.

Esecuzione simbolica
In questo caso si costruiscono i predicati che caratterizzano le condizioni per l’esecuzione di un certo percorso e i
predicati che individuano gli effetti che l’esecuzione di un certo percorso ha sullo stato dell’applicazione. Questa
tecnica trova importanti applicazioni nell’analisi dei programmi, nella generazione dei dati per i test e nella
verifica formale della correttezza del software.
Un concetto importante per l’esecuzione simbolica è quello di stato simbolico. Uno stato simbolico è una coppia:
<path-condition, symbolic bindings>
I valori sono cioè espressi non da valori concreti, ma da espressioni costituite da opportuni simboli. Ad esempio,
la figura seguente mette a confronto l’esecuzione concreta con l’esecuzione simbolica di un’applicazione.
Figura 104: Esempio di esecuzione simbolica
Nel caso in cui siano presenti delle istruzioni di brach, occorre utilizzare le cosiddette path condition, le quali
conservano memoria delle condizioni sui dati di input che specificano il percorso seguito durante l’esecuzione
simbolica. Costruiremo così delle triple:
<risultato dell’esecuzione, percorso seguito, path condition>
Dove il percorso seguito è la sequenza di istruzioni seguite. Consideriamo il seguente esempio:
read (y, a);
x = y + 2;
if x > a
a = a + 2;
else
y = x + 3;
x = x + a + y;
Un’esecuzione simbolica sarà:
< {a = A, y = Y + 5, x = 2 * Y + A + 7}, <1, 2, 3, 5>, Y + 2 <= A>
Le path condition possono essere usate per sintetizzare i dati che seguono dall’esecuzione di un certo path. Se un
sottoprogramma o una funzione ha una precondizione e una postcondizione, occorre controllare che la verità
della path condition implichi sempre la verità della precondizione.
107
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Il testing: introduzione
A che cosa serve il testing?
L’attività di testing consente l’individuazione di errori, ma non permette mai di dedurre la loro assenza: se nel testing
non vengono individuati errori, questo non necessariamente significa che il codice ne è privo (è possibile che questi
semplicemente non siano stati individuati).
Le tecniche possibili mediante le quali eseguire il testing sono diverse; la tecnica random (che consiste nello scegliere
valori casuali tra quelli del dominio dei dati di ingresso da fornire al software da testare) è la più semplice, ma non è
efficace nell’individuazione di tutti i possibili flussi, perciò viene affiancata o sostituita da altre tecniche più mirate.
Differenza tra il testing e il debugging
A questo punto, è bene chiarire la distinzione tra il concetto di debugging e testing: mentre il testing ha l’obiettivo di
identificare la presenza di errori, il debugging, deve localizzare il punto in cui gli errori si trovano e, di conseguenza,
correggerli. Il debugging avviene quindi dopo il testing e può essere seguito da un nuovo test, al fine di verificare se
l’errore è stato effettivamente eliminato (la ripetibilità dei test è quindi una caratteristica fondamentale).
Formalizzazione dei concetti legati alla correttezza dell’applicazione

Il programma come funzione
Un programma è una funzione , eventualmente parziale, definita da un certo insieme di valori di ingressi, detto
dominio , su un certo insieme di valori , detto dominio degli output:

Correttezza di un programma
Per definire la correttezza di un programma, si definisce un insieme
e dal corrispondente valore di output atteso:
Il comportamento del programma
programma è corretto se
di coppie costituite da un valore di input
è corretto in corrispondenza dell’ingresso
.
se
. Il

Failure
Chiamiamo failure la manifestazione esterna di un errore, ovvero la situazione nella quale per un certo ingresso
, si ha
. Ciò può accadere perché il programma non è definito per quel valore (mentre dovrebbe
esserlo) oppure produce un valore di output errato.

Error
Chiamiamo errore (error) un qualsiasi evento che provoca una failure (ad esempio, un errore di battitura, la
dimenticanza dell’inizializzazione di una variabile, …).

Fault
Chiamiamo fault lo stato interno in cui si trova il sistema quando si ha un errore. Il fault potrebbe non essere
visibile all’esterno, cioè potrebbe non manifestarsi mediante una failure, e questa è la situazione più pericolosa.
La terminologia appena introdotta non è tuttavia universale. Ora che abbiamo introdotto questi concetti, possiamo
affermare che il testing ha l’obiettivo di fare in modo che la presenza di errori si manifesti mediante delle failure; il
debugging deve invece identificare le situazioni di fault il più presto possibile.
108
Ingegneria del Software 2
Capitolo 9: Verifica e validazione
Formalizzazione dei concetti legati al testing

Caso di test
Chiamiamo caso di test (test case) un singolo elemento del dominio di ingresso .




Insieme di casi di test
Chiamiamo insieme di casi di test (test set)
un sottoinsieme finito del dominio d’ingresso .
Test terminato con successo
Diciamo che un test è terminato con successo (successful test) se
.
Test set terminato con successo
Diciamo che un test set è terminato con successo (successful test set) se, per ogni
Test set ideale
Un test set si dice ideale se è un insieme di casi di test tale che, se il programma
almeno un elemento all’interno di per il quale si ha
non corretto, ovvero
, si ha
.
non è corretto, esiste
.
Se esistesse un test set ideale per ogni programma, e se fossimo in grado di identificarlo, potremmo essere in
grado di provare la correttezza del software; tuttavia, non è possibile stabilire se un test set è ideale oppure no.

Criterio di test
Un criterio di test è un sottoinsieme finito di tutti i possibili insiemi di test per un certo programma . In altri
termini, è un sottoinsieme dell’insieme delle parti di , ovvero un insieme di test set.

Test set che soddisfa un criterio di test
Un test set soddisfa un criterio di test
se è un elemento di , ovvero
.
I criteri di test: proprietà

Consistenza
Un criterio di test è consistente se, per ogni coppia di test
, termina con successo se e solo se
termina con successo. Di conseguenza, il criterio è consistenze se ogni test set che lo soddisfa fornisce di fatto le
stesse informazioni.

Completezza
Un criterio di test è completo se, nel caso in cui il programma sia incorretto, esiste almeno un test set che
soddisfa e che non termina con successo. Un criterio di test completo e consistente identifica un test set
ideale, perciò consente di provare la correttezza del codice.

Finezza
Un criterio di test è detto più fine di un criterio di test
che soddisfa il criterio esiste un sottoinsieme di
se, per ogni programma , si ha che per ogni test set
che soddisfa .
Criteri di test empirici
Nessuna delle proprietà appena elencate è verificabile (siamo di fronte a problemi indecidibili). Di conseguenza, non si
può fare altro che utilizzare dei criteri di test empirici, che cerchino un compromesso tra ciò che è impossibile
ottenere (la prova di correttezza) e ciò che è inadeguato (ovvero non eseguire alcun test, o eseguire solo test casuali).
L’obiettivo di questi criteri è quello di suddividere il dominio di input in sotto-domini , , …,
, in modo il
programma abbia, per ciascuno degli elementi di ognuno dei sottodomini così identificati, un comportamento atteso
“simile”. Naturalmente, l’unione dei vari sottodomini deve coincidere con il dominio di partenza, per il principio di
copertura totale. Si hanno poi diverse possibilità:


Se i sotto-domini costituiscono una partizione (sono disgiunti a 2 a 2), allora si sceglierà un valore per ognuno dei
sotto-domini, e si valuterà il comportamento del software in corrispondenza dei soli valori così trovati.
In caso contrario, si cerca di ridurre il numero dei casi di test, utilizzando gli elementi appartenenti all’intersezione
tra più domini, e considerando per i sotto-domini in questione solo il caso di test comune.
109
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Confronto tra testing strutturale e testing funzionale
Test funzionale e test strutturale
Per l’identificazione dei casi di test, è possibile seguire uno dei due modelli seguenti:

Il test funzionale (il software come scatola nera)
In questo caso, i criteri di partizionamento si basano solo sulla specifica del modulo, ed il software è considerato
come una black box: non si conosce il codice e la struttura interna del codice. Viene così testato solo ciò che il
software dovrebbe fare. Si noti inoltre che questo tipo di test dipende fortemente dal modo in cui le specifiche
vengono fornite. Uno dei suoi difetti è l’impossibilità di distinguere tra implementazioni diverse di uno stesso
metodo o funzione o modulo.

Il test strutturale (il software come scatola trasparente)
Il test strutturale prevede l’uso di criteri di partizionamento del dominio degli input basati sul codice interno dei
moduli. Seguendo questo approccio quindi, a differenza di quanto accadrebbe con il precedente, non è possibile
accorgersi di eventuali funzionalità mancanti. Si va infatti a testare solo ciò che il software effettivamente fa. I
test set vengono individuati sulla base del flusso di controllo e/o sul flusso di dati.
Un’altra differenza tra i due approcci riguarda la scalabilità: l’approccio black box è infatti più scalabile, perché non
rende necessaria l’analisi del codice; di conseguenza, se il codice diviene troppo lungo, anche il test strutturale
diventerà lungo e complesso, mentre lo stesso non accade nel caso del test black box. A ciò si aggiunga che, in casi di
elevato numero di funzionalità, ci si può con semplicità limitare ad analizzare solo una parte della specifica.
Possibili criteri di copertura
Per stabilire se la copertura garantita dai test è sufficiente oppure no, possono essere usati diversi criteri, che
verranno di seguito elencati. Per ognuno di essi, la copertura viene valutata come:
Idealmente la copertura dovrebbe essere del 100%. Si hanno criteri di controllo basati sul flusso di controllo:

Criterio statement coverage
Si valuta quante sono le istruzioni coperte dal test, senza valutare quali sono i rami seguiti a seguito delle
istruzioni condizionali.
La determinazione di un metodo automatico per individuare un test set che garantisca la completa copertura
secondo il criterio statement coverage risulta in realtà impossibile: se potessimo disporre di un metodo per
stabilire se tutte le istruzioni di un programma vengono eseguite, allora saremmo in grado di stabilire se il
programma termina oppure no; come noto, però, tale problema è indecidibile.

Criterio edge coverage
Si valuta quali sono i percorsi seguiti (e quindi quali sono i rami utilizzati nel diagramma di flusso
dell’applicazione); questo significa che per ogni istruzione condizionale, si deve testare il software sia nel caso in
cui la condizione sia vera, sia nel caso in cui sia falsa.
Questo criterio risulta quindi più potente rispetto al precedente.

110
Criterio condition coverage
In questo caso, si valuta anche la ragione per la quale una condizione è vera o falsa: se si è in presenza di una
condizione composta da due parti diverse, si deve testare il comportamento del software rendendo vera o falsa
non solo l’intera condizione, ma anche tutte le sue singole sottocondizioni.
Ingegneria del Software 2

Capitolo 9: Verifica e validazione
Criterio path coverage
Il criterio path coverage prevede che, se ci sono cicli, si valuti quale percorso deve essere seguito, ovvero quante
iterazioni coprire del ciclo stesso.
Naturalmente, non è possibile coprire tutti i percorsi possibili, perché in generale sono infiniti, a seguito della
presenza dei cicli. È quindi necessario stabilire dei criteri per limitare opportunamente il numero di iterazioni del
ciclo. Naturalmente però non si ha alcuna garanzia di correttezza nella scelta di tali limiti.
Esistono inoltre dei criteri basati sul flusso dei dati:

Criterio data-flow
Il criterio di copertura basato sul controllo del flusso dei dati utilizza la tecnica dell’analisi statica, per mezzo del
controllo della dipendenza con le relazioni def e use. In sostanza quindi si va ad identificare per ogni variabile
dove essa viene definita e dove viene utilizzata, e si selezionano dei test set in modo tale che tutte le possibili
coppie <def, use> vengano eseguite. Ad esempio, consideriamo il seguente codice:
0
1
2
3
4
5
6
-
int select(int A[], int N, int X) {
int i = 0;
while(i < N and A[i] < X) {
if(A[i] < 0)
A[i] = - A[i];
i++;
}
return(1);
}
In questo caso, le possibili coppie di definizione ed uso delle variabili sono:
Variabile
i
A
N
X
Coppie Def/Use
<1, 2>, <1, 3>, <1, 4>, <1, 5>, <5, 2>, <5, 3>, <5, 4>, <5, 5>
<0, 2>, <0, 3>, <0, 4>, <4, 2>, <4, 3>, <4, 4>
<0, 2>
<0, 2>
Tabella 13: Coppie def-use di una porzione di codice di esempio
Si osserva perciò che la coppia <5, 3> per la variabile i (così come tutte le altre coppie che hanno il 5 come primo
elemento) viene raggiunta solo a patto che si abbiano almeno 2 iterazioni del ciclo, e quindi nel test set occorrerà
tenere conto di questo. Si sceglierà quindi, ad esempio, un test set del tipo:
<N = 2, X = 5, A[0] = -3, A[1] = -4>
In ogni caso, è nella maggior parte dei casi impossibile raggiungere la copertura totale del secondo i criteri sopra
elencati, perché alcuni comportamenti sintatticamente possibili, nella realtà non si possono verificare. Di
conseguenza, si fissano degli obiettivi inferiori al 100%, che solitamente sono tra l’80% e il 95%.
111
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Il test funzionale
Se si applica il test funzionale, i test set vengono individuati a partire dalla specifica e permettono di verificare ciò che
il software si suppone debba fare.
Siccome la specifica è espressa in linguaggio naturale, i test set non potranno essere derivati in maniera automatica. In
questo possono però venire in aiuto gli standard dell’organizzazione che realizza il software, oltre ad una serie di linee
guida, come ad esempio verificare che il test set contenga:
1.
2.
3.
4.
Un sottoinsieme di dati validi ed omogenei.
Almeno una combinazione non valida di input.
I dati “boundary”, ovvero al confine tra l’insieme dei valori ammessi e quello dei valori non ammessi.
Tutti i valori in input particolari, che vengono trattati indipendentemente dagli altri.
Per rendere più sistematici i test di tipo black box, è possibile utilizzare alcune tecniche:

Testing basato sulle tavole di decisione
Questo metodo consiste nel realizzare una tabella nella quale ad ogni riga viene fatto corrispondere un possibile
comando (o input) fornito al sistema, e ad ogni colonna corrisponde un’azione che risulta dai comandi.
A scopo esemplificativo, consideriamo la seguente specifica scritta in linguaggio naturale:
The word-processor may present portions of text in three different formats: plain text (p), boldface (b),
italics (i). The following commands may be applied to each portion of text: make text plain (P), make
boldface (B), make italics (I), emphasize (E), superemphasize (SE). Commands are available to
dynamically set E to mean either B or I (we denote such commands as E=B and E=I, respectively.)
Similarly, SE can be dynamically set to mean either B (command SE=B) or I (command SE=I), or B and I
(command SE=B+I.)
La tavola di decisione che ne deriva è rappresentata in figura 105. Dovremo poi far corrispondere ad ogni
colonna almeno un caso di test; si noti però che, siccome il numero di colonne dipende dalle possibili condizioni
di ingresso dei dati, avremo nel peggiore dei casi
casi di test, dove è il numero di possibili condizioni (cioè il
numero di righe): naturalmente, tale approccio risulterebbe in questo caso del tutto in applicazione.
Figura 105: Esempio di tavola di decisione
112
Ingegneria del Software 2

Capitolo 9: Verifica e validazione
Testing basato sui grafi di causa ed effetto
Un approccio diverso è quello che consiste nel rappresentare i possibili comandi e le possibili uscite, mettendo in
relazione tali elementi con un cosiddetto grafo di causa-effetto, che appare molto simile ad una rete logica: i
comandi sono gli ingressi, le azioni sono le uscite, ed è possibile legare tra loro mediante operatori logici come
AND ed OR i vari ingressi (con ovvio significato). Ad esempio, il grafo di causa ed effetto che fa riferimento alla
specifica precedentemente riportata è parzialmente raffigurato nella figura seguente:
Figura 106: Esempio di grafo di causa-effetto
Nel grafo possono essere rappresentati anche dei vincoli più dettagliati:
1.
Si può specificare che, per un certo insieme di input, se ne può fornire al sistema al più uno (at most one).
2.
Si può specificare che esattamente un input di un certo insieme deve essere fornire al sistema (one and only
one).
3.
Si può specificare che, per un certo insieme di input, almeno uno deve essere fornito al sistema (at least
one).
4.
Si può specificare che, se viene fornito un certo input al sistema, allora il sistema dovrà ricevere anche un
altro input specificato (requires-implies).
5.
Si può specificare che, se si fornisce un certo input al sistema, allora viene bloccata la possibilità di fornirvi un
secondo input specificato (masks). Ciò può essere specificato anche per gli output.
Figura 107: At most one
Figura 110: Requires (implies)
Figura 108: At least one
Figura 109: One and only one
Figura 111: Masks
113
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Nella figura seguente sono applicati alcuni dei costrutti appena descritti per perfezionare il grafo di causa ed
effetto della specifica che abbiamo finora utilizzato per i nostri esempi:
Figura 112: Esempio di grafo di causa-effetto con costrutti per vincolare input ed output
Occorrerebbe poi testare tutte le possibili combinazioni di input ottenute mediante il grafo di causa-effetto,
controllando ogni volta che l’output ottenuto sia quello atteso. Per ridurre il numero di casi di test, possono
essere utilizzate delle euristiche, che prevedono che si parta dagli output, andando indietro verso gli input:
a)
per ogni nodo OR, se si desidera renderlo vero, si considerano solo le combinazioni degli ingressi di tale nodo
nelle quali solamente uno degli ingressi ha valore true;
b) per ogni nodo OR, se si desidera renderlo falso, si considerano solo le combinazioni degli ingressi di tale
nodo nelle quali solamente uno degli ingressi ha valore false;

Derivazione dei test set a partire dai diagrammi di sequenza
È possibile anche generare i test set a partire dai sequence diagram, nei quali vengono definite delle possibili
sequenze di messaggi scambiati tra i vari oggetti, solitamente corrispondenti sia alle situazioni che si verificano
più frequentemente, sia ai casi particolari.
L’approccio prevede diverse fasi:
1.
In una prima fase, si genera un test-case per ogni sequence diagram.
2.
Nelle fasi successive, si considerano nuovamente i singoli diagrammi e all’interno di ciascuno di essi si
individuano tutte le possibili situazioni alternative, generando per ognuna di esse un diverso test-case.
Anche in questo caso, la selezione dei casi di test avviene in maniera prevalentemente manuale, e non è
facilmente automatizzabile.
Figura 113: Esempio di sequence diagram utilizzato per la generazione dei casi di test
114
Ingegneria del Software 2
Capitolo 9: Verifica e validazione
Considerano il sequence diagram mostrato nella precedente figura, possiamo ad esempio generare i seguenti
casi di test:
1.
2.
3.
4.
5.
Un test case nel quale viene generato un nome non corretto per lo studente;
Un test case nel quale lo studente risulta già iscritto al corso;
Un test case nel quale lo studente non può iscriversi al corso (ad esempio, perché richiede dei corsi come
prerequisiti, ma lo studente non li ha ancora frequentati);
Un test case nel quale viene richiesta l’iscrizione ad un corso inesistente;
Un test case nel quale il corso non ha posti disponibili.
E così via.

Testing basato sulla struttura sintattica degli input
Un altro approccio di tipo black-box si basa sulla struttura sintattica degli input. Conoscendo tale struttura,
rappresentata ad esempio mediante una grammatica generativa, è possibile generare dei test set in modo che
tutte le regole della grammatica siano coperte dai test case considerati.
In questo caso, la specifica è espressa in modo formale, perciò è possibile generare i test in maniera automatica.
Osservazioni varie sulla fase di test
L’analisi dei test: gli oracoli
Una volta effettuato un test, è necessario ispezionare i risultati ottenuti per andare ad individuare gli errori. Il
meccanismo utilizzato per stabilire se un test è stato superato oppure no prende il nome di oracolo. In sostanza
l’oracolo prevede che si confronti il risultato di ogni singolo caso di test con il risultato che l’oracolo stesso ha stabilito
che il sistema avrebbe dovuto fornire.
Gli oracoli sono perciò necessari in ogni fase del testing. Se si deve eseguire un elevato numero di test, allora risulta
indispensabile disporre di oracoli automatizzati. Tuttavia, gli oracoli sono molto complessi da progettare, e non esiste
una ricetta universale che consenta di effettuare tale operazione.
Ad esempio, in JUnit gli oracoli possono essere definiti in maniera precisa e molto semplice, mediante l’utilizzo delle
asserzioni.
Il contesto di esecuzione dei test
Quando si effettua un test di unità, è spesso necessario creare il contesto all’interno del quale eseguire i test stessi. A
tale scopo, oltre all’oracolo, è necessario costruire anche:

Il driver
Un componente che si occupa di inizializzare tutti i parametri e le variabili non locali. Tale componente si
occuperò anche di chiamare l’unità da testare.

Lo stub
Nel caso in cui l’unità utilizzi delle altre unità, può essere necessario realizzare dei moduli stub da sostituire a tali
unità. Si tratta perciò di veri e propri “templates” di classi e funzioni richiamate all’interno del codice da testare.
115
Capitolo 9: Verifica e validazione
Ingegneria del Software 2
Sia gli stub, sia i driver vengono realizzati mediante la stesura di porzioni di codice ad-hoc. La figura seguente mostra la
relazione tra l’unità da testare, il driver, gli stub e l’oracolo:
Figura 114 Il test e il contesto di esecuzione
Per evitare di dover realizzare degli stub ad hoc, è possibile adottare l’approccio bottom-up, il quale prevede che si
realizzi un grafo della relazione “usa” tra le varie unità da testare, e si proceda poi partendo dalle unità che non hanno
dipendenze con altre unità da testare, per poi risalire gradualmente tutti i vari rami dell’albero.
Test di regressione
Come già accennato, ad ogni nuovo rilascio del software è necessario effettuare dei testing. Per minimizzare gli sforzi
da affrontare in questa nuova fase di testing, è opportuno:
1.
2.
Al momento del test delle precedenti versioni, memorizzare tutta la documentazione relativa ai test, tutti i casi di
test valutati con i relativi risultati, ma anche gli oracoli, i driver e gli stub utilizzati.
Nella nuova release, si devono poi valutare i cambiamenti apportati, tenendone traccia e considerando quale
impatto possono avere tali cambiamenti.
La documentazione dei testing
La documentazione della fase di test deve essere standardizzata e opportunamente strutturata. Per ogni test set, le
informazioni di cui tenere traccia sono:
1.
2.
3.
4.
Il software testato con la relativa versione.
L’obiettivo dei test.
L’autore dei test.
I risultati complessivi ottenuti dal test.
Per ogni test case bisogna inoltre memorizzare:
1.
2.
3.
4.
5.
116
L’obiettivo del test case.
L’ambiente (oracolo, driver, stub).
L’input fornito.
L’output realmente ottenuto e quello che ci si attendeva.
I risultati e le osservazioni sui risultati stessi.