Sei sulla pagina 1di 73

Appunti di ingegneria del software (A.

A 2017/2018)
Rocco di Torrepadula Franca, Somma Alessandra

1. Introduzione all’ingegneria del software:


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.
Secondo il glossario dell’IEEE (Glossario standard della terminologia dell’ingegneria del software) “l’ingegneria del
software è l’applicazione di un approccio sistematico, disciplinato e quantificabile allo sviluppo, funzionamento e
manutenzione del software”.

I software invece sono “i programmi, le procedure, l’eventuale documentazione associata e i dati relativi
all’operatività di un sistema di elaborazione”.

L’ingegneria del software è quindi la disciplina tecnica, tecnologica e gestionale che riguarda la produzione sistematica
e la manutenzione dei prodotti software che vengono sviluppati e modificati entro i tempi e i costi preventivati.

Storia: Verso la fine degli anni Cinquanta con la diminuzione dei prezzi dei computer e la loro diffusione, un numero
sempre maggiore di persone iniziarono a utilizzarli. I linguaggi di alto livello furono inventati proprio in questo periodo
per rendere più facile comunicare con le macchine. In questo periodo nasce la professione del “programmatore”: una
persona poteva chiedere al programmatore di scrivere un programma, invece di realizzarlo per conto proprio. Ciò
introdusse una separazione tra utente e macchina: l’utente specificava cosa volesse ottenere da una data applicazione
utilizzando un linguaggio diverso dalla notazione di programmazione. Il programmatore leggeva tale “specifica” e la
traduceva in un insieme ben preciso di istruzioni macchina, che molto spesso rappresentavano un’interpretazione
erronea delle intenzioni dell’utente.
Nella seconda parte degli anni Sessanta vi furono tentativi di creare grandi sistemi software commerciali (come il
sistema operativo OS 360 per i computer IBM 360). Vi erano profonde difficoltà nel cercare di adattare le tecniche di
sviluppo di piccoli programmi allo sviluppo di grossi software. Proprio in questo periodo fu coniata l’espressione
ingegneria del software, contestualmente all’espressione “crisi del software”. Si comprese che i problemi riscontrati
nella creazione di grandi sistemi software non riguardavano semplicemente la capacità di strutturare il codice. I
software venivano realizzati da più persone: un problema fondamentale che nacque fu la difficile comunicazione tra i
diversi programmatori. Inoltre, le grosse dimensioni dei progetti portarono a una realizzazione di lunga durata, il
ricambio del personale comportava la trasmissione orale dei requisiti del progetto: nacque la necessità di
documentare ogni attività. Si iniziò dunque a capire che il problema della costruzione di un software andava affrontato
come qualunque altro problema ingegneristico, si necessitava un approccio ingegneristico.

L’ingegneria del software dunque nasce come maturazione dell’attività di programmazione e dall’esigenza di un
processo produttivo sistematico, esattamente come un’attività ingegneristica. La storia mostra inoltre la crescita
dell’ingegneria del software, a partire dalla diminuzione del costo dell’hardware e l’aumento di quello del software.
Anche dal punto di vista economico l’impatto continua a crescere (da 140 miliardi di dollari nel 1985 a 800 miliardi nel
2000). Infine, anche dal punto di vista sociale l’impatto del software è evidente: il software permea la società.

Ciclo di vita del software: il software subisce un’evoluzione che conduce dall’idea iniziale e vaga di un possibile
prodotto o sistema software, alla sua realizzazione. Il software ha quindi un ciclo di vita che consta di varie fasi, ognuna
della quali ha un inizio, una fine e produce un risultato parziale (manufatto intermedio), trasferito alla fase successiva.

1. Analisi: durante la fase di analisi si analizzano i requisiti del software. Esso può essere sviluppato per il mercato
o eventualmente per un committente: nel primo caso verrà fatta un’analisi di mercato, altrimenti si interagirà
direttamente con il committente e con gli utenti. Distinguiamo poi il committente interno dal committente
esterno (rispetto allo sviluppatore del software): il committente esterno è un cliente, un committente interno
è un’altra divisione della stessa azienda produttrice. Ciò che distingue profondamente i due casi sono le
modalità di interazione e dunque gli aspetti contrattuali, i tempi, le scadenze ecc. L’analisi dei requisiti viene
intrapresa dopo uno studio di fattibilità per definire i costi e i benefici del sistema software. Ciò sarà effettuato
del cliente, dallo sviluppatore e da specialisti nel settore. In questa fase quindi si studia cosa il sistema debba
fare, scrivendo i requisiti in maniera chiara e non ambigua. Si creano manuali per gli utenti e si progettano i
test di sistema che saranno effettuati prima della consegna.
2. Progettazione: una volta specificati e documentati i requisiti, gli ingegneri progettano un sistema software che
li soddisfi. La progettazione avviene in due sottofasi: il progetto architetturale, che affronta l’organizzazione
globale del sistema in parti e la loro interazione, ed il progetto dettagliato.
La fase di progettazione definisce come sarà realizzato il prodotto, notiamo dunque una netta dicotomia tra
il “cosa” e il “come”.
3. Realizzazione: durante questa fase abbiamo la codifica dei singoli moduli (e il test di ciascuno di essi nella sua
singolarità) e la loro integrazione in un unico sistema.
4. Testing: una volta assemblati si testano i moduli come un unico sistema
5. Consegna;
6. Manutenzione: vengono incorporate le modifiche effettuate al sistema dopo la prima consegna.

In realtà questo modello “a cascata” è una semplificazione del processo reale, talvolta è necessario tornare indietro
se si riscontrano errori nati nelle fasi precedenti. Inoltre, con questo modello si suppone che ogni fase inizi al termine
della precedente, nella realtà è spesso vantaggioso un parallelismo, iniziare una fase prima che la precedente finisca.

2. Fattori di qualità del software:


Distinguiamo qualità interne ed esterne. Le qualità esterne riguardano il prodotto software, visto come una black-box,
sono le qualità visibili agli utenti del sistema; le qualità interne riguardano gli sviluppatori, che guardano al software
come a una white box.

Un sistema software nasce da un’idea o da una richiesta di mercato, ma più in generale esso ha dei portatori di
interesse, stakeholders, in cui rientrano l’utente e il mercato, o anche il cliente (che non è detto che sia
l’utente/committente, l’organizzazione che lo produce (azienda e/o pubblica amministrazione), finanziatore,
legislatore (ad esempio nel caso dei siti web che devono rispettare le leggi sulla privacy), norme (ad esempio
sull’accessibilità per ipovedenti ecc..), manutentore, installatore, amministratore dati, responsabile della qualità (che
probabilmente sarà proprio l’organizzazione che lo produce). Ogni requisito, se esiste, deve avere uno o più portatori
di interesse.
Oltre a questa prima distinzione vi è una classificazione tra qualità del prodotto e qualità del processo. Le qualità
interne influiscono su quelle esterne, o meglio permettono agli sviluppatori di raggiungere le qualità esterne. Allo
stesso modo la qualità del processo, che comprende anche la conoscenza e le esperienze delle persone che lavorano
al progetto, influisce sulla qualità del prodotto finale.

• Correttezza: un software è corretto se soddisfa i requisiti funzionali (ovvero le specifiche). Questa definizione
assume che le specifiche siano disponibili e non ambigue. La correttezza è una proprietà matematica che stabilisce
l’equivalenza tra il software e la sua specifica. Ovviamente possiamo essere tanto più sistematici e precisi nel
valutare la correttezza quanto più rigorosi siamo stati nello specificare i requisiti funzionali. Possiamo verificarla
attraverso il testing, mediante un approccio analitico o la verifica formale.

• Affidabilità: è la probabilità che un software operi come atteso in un intervallo di tempo determinato. La
correttezza è una qualità assoluta, un qualunque scostamento da ciò che è stato stabilito nelle specifiche risulta
non corretto; l’affidabilità è invece una qualità relativa, gli scostamenti sono tollerabili.
La relazione per cui la correttezza è inclusa nell’affidabilità in realtà è valida sotto l’ipotesi che
la specifica dei requisiti funzionali colga esattamente tutte le proprietà desiderate dall’utente
di un’applicazione, senza contenere erroneamente proprietà indesiderabili.
Nella pratica non è così: il software può essere corretto, e dunque soddisfare i requisiti
funzionali espressi dalle specifiche, senza soddisfare i bisogni dell’utilizzatore. Possono esservi
applicazioni corrette sviluppate sulla base di requisiti non corretti. Questo è dovuto al fatto che l’utente può
aspettarsi che il software operi in maniera diversa da quanto non accada, sebbene il software verifichi le specifiche.
(Anche in fase di testing si distingue la verifica, ovvero il controllo della corrispondenza con le specifiche, dalla
validazione, dove si controlla invece che il prodotto rispetti le aspettative dell’utente).
Una metrica per l’affidabilità è la MTBF (mean time between failures), l’intervallo medio di tempo tra i fallimenti.
Questa tende a diminuire nel tempo. Un altro parametro importante è la MTTF (mean time to failure), intervallo
di tempo per il primo fallimento.

• Disponibilità: è la probabilità che il software operi come atteso in un dato istante di tempo.

• Robustezza: un software è robusto se si comporta in maniera accettabile anche in situazioni non specificate nei
requisiti, come input non corretti o non attesi, malfunzionamenti hardware. Un software può essere non robusto
sebbene sia corretto, se la specifica non indica cosa il programma dovrebbe fare in caso di input scorretto. Se
potessimo definire esattamente cosa occorrerebbe fare per rendere un’applicazione effettivamente robusta
saremmo in grado di specificarlo, dunque la robustezza diventerebbe equivalente alla correttezza. Nel momento
in cui un requisito diventa parte della specifica, il suo soddisfacimento è inerente alla proprietà di correttezza; se
il requisito non fa parte delle specifiche allora sarà un problema di robustezza. La linea di demarcazione tra le due
qualità è la specifica.

• Prestazioni: è una qualità esterna basata sui requisiti dell’utente e sull’utilizzo efficiente delle risorse. Differisce
dall’efficienza in quanto quest’ultima è una qualità interna, si riferisce all’adeguato uso delle risorse da parte del
software. L’efficienza influenza e spesso determina le prestazioni. Per valutare le prestazioni di un algoritmo
utilizziamo la teoria computazione. Per valutazioni più specifiche possiamo effettuare misurazioni su sistemi reali,
costruire modelli analitici o costruire un modello simulativo (queste tecniche possono anche essere
complementari).

• Usabilità: un sistema è usabile (user friendly) se i suoi utenti lo reputano facile da utilizzare. Dalla definizione
stessa si evince la natura soggettiva di tale proprietà. L’interfaccia utente influisce molto sull’ “amichevolezza” di
un’applicazione. In generale l’usabilità è studiata dalla disciplina scientifica che si occupa del “fattore umano”
(human factor), in informatica HCI (human-computer interaction). In molte discipline l’usabilità si ottiene
standardizzando le interfacce uomo-macchina, così che un utente che abbia imparato a far funzionare un
apparecchio sia in grado di far funzionare gli apparecchi analoghi, anche di altri produttori.

• Verificabilità: è fondamentale poter facilmente procedere alla verifica della correttezza di un sistema software.
Per ottenere ciò (ad esempio per la codifica) sono necessarie programmazione strutturata, modulare, regole di
codifica..

• Manutenibilità: è la facilità con cui le attività di manutenzione vengono eseguite e l’economicità dei relativi
processi. Solitamente la manutenzione supera il 60% dei costi totali del software. La manutenzione può essere di
tre tipi: correttiva, adattativa e perfettiva:
• La manutenzione correttiva riguarda la rimozione di errori residui presenti nel prodotto al momento del
rilascio (o eventualmente l’eliminazione di errori introdotti nel software durante l’attività di manutenzione
stessa).
• La manutenzione adattativa riguarda le modifiche dell’applicazione in risposta a cambiamenti
dell’ambiente (hardware, sistema operativo, DBMS). Non è una necessità nata dal software stesso ma
dall’ambiente in cui esso è inserito.
• La manutenzione perfettiva riguarda i cambiamenti nel software per migliorare alcune qualità. La richiesta
può provenire dagli sviluppatori, con l’obiettivo di migliorarne la presenza sul mercato, o dal committente,
al fine di rispondere a nuovi requisiti.

Abbiamo poi due diverse qualità: la riparabilità e l’evolvibilità. Un sistema software è riparabile se i suoi difetti
possono essere corretti con una quantità ragionevole di lavoro. Solitamente un software che consta di moduli
ben progettati è più facile da analizzare e riparare di un sistema monolitico: la corretta strutturazione in moduli
favorisce la riparabilità. Un software ha evolvibilità se facilita cambiamenti che gli permettono di gestire
nuovi requisiti. È una qualità che richiede la capacità di anticipare i cambiamenti in fase di progettazione e
tende a diminuire con i rilasci successivi (con le modifiche aumenta il rischio di introdurre nuovi errori).

• Riusabilità: è la possibilità di poter riutilizzare delle parti del software. È una delle maggiori finalità della
programmazione object-oriented. È una proprietà applicabile non solo ai componenti software ma anche alle parti
del processo, può essere riutilizzato qualunque artefatto intermedio, ma può essere riutilizzato anche il processo
per costruire diversi prodotti (metodologie del processo).

• Portabilità: un software è portabile se può essere eseguito in ambienti diversi. Può essere ottenuta
modularizzando il software, in modo tale che le dipendenze dall’ambiente vengano isolate in pochi moduli,
modificabili in caso di trasporto del software in ambiente differente.

• Comprensibilità: il software dev’essere comprensibile al fine di verificarne la correttezza, apportare modifiche e


riusarlo. Questa dipende sia da come il software è progettato sia dal problema affrontato dal software (più il
problema è semplice più facile risulterà produrre un software comprensibile).

• Interoperabilità: è la capacità di un sistema di coesistere e cooperare con altri sistemi. Negli altri prodotti
ingegneristici è una proprietà molti diffusa (ad esempio possiamo utilizzare un impianto audio costituito da
elementi di diversi produttori e connetterlo a una televisione di un altro produttore). Un concetto correlato con
l’interoperabilità è quello di open system: una collezione estendibile di applicazioni scritte in modo indipendente
tra di loro, ma che funzionano come un sistema integrato. Un sistema di questo tipo consente l’aggiunta di nuove
funzionalità sviluppate indipendentemente, anche dopo la consegna del sistema.

• Produttività: è una qualità del processo di produzione del software, ne indica l’efficienza e le prestazioni. Ciascun
individuo che lavori ad un progetto produce con una propria velocità ed efficienza, la produttività del gruppo è
funzione della produttività dei singoli individui che vi lavorano. Spesso la produttività combinata però è molto
inferiore rispetto alla produttività che si otterrebbe dalla somma delle parti (per il tempo necessario alla
comunicazione e il coordinamento dell’intero gruppo). La produttività del software, sebbene sia una caratteristica
di grande interesse pratico, è difficile da misurare.

• Tempestività: è una qualità del processo che indica la capacità di rendere disponibile un prodotto al momento
giusto. È necessario consegnare il prodotto in tempo a causa della competitività dei mercati odierni. La
tempestività dipende dalle risorse complessive e non sempre è una qualità utile: i produttori spesso rilasciano il
prodotto privo di qualità o con molti difetti, pur di non arrivare tardi alla consegna (il che può precludere
interessanti opportunità di mercato). La tempestività richiede un’attenta pianificazione del processo e un’accurata
stima delle attività. Se non c’è tempestività si rischia che il sistema diventi obsoleto prima ancora di essere
consegnato, il ritardo può far sì che i requisiti formulati siano ormai diversi da ciò che l’utente si aspetti.
Una tecnica per ottenere la tempestività è attraverso la consegna incrementale del prodotto. Non si rilascia da
subito il prodotto in toto ma si rilasciano inizialmente parti singole, in questo modo il prodotto è reso disponibile
in tempi brevi e l’uso del prodotto contribuisce a raffinare i requisiti del prodotto stesso, in modo incrementale.
Per realizzare ciò il sistema deve poter essere spezzato in sottoinsiemi di funzionalità che possono essere sviluppati
successivamente. Ciò va combinato a un processo anch’esso incrementale, che permette lo sviluppo tempestivo.

• Dependability: per i sistemi critici si enfatizzano anche altri attributi di qualità, la dependability è cosistuita da:
o Disponibilità
o Manutenibilità
o Safety: ovvero l’assenza di conseguenze catastrofiche su utenti e ambiente.
o Integrità: ovvero l’assenza di alterazioni improprie.

Un sistema safety critical non deve creare danni catastrofici all’ambente e alle persone circostanti. Abbiamo visto
che il software è un prodotto ingegneristico particolare, in primo luogo perché è immateriale. Data la sua
immaterialità un software di per sé non potrebbe danneggiare l’ambiente o gli utenti ma nell’ambito di un sistema
critico è diverso, basti pensare alle centrali nucleari. Nei sistemi critici alcune qualità del prodotto, tra cui proprio
la safety, sono garantite a partire dalla qualità del processo: il rispetto dei vincoli sul processo aumenta la
confidenzialità con le qualità del prodotto. È difficile definire degli standard per la qualità del processo, ve ne
sono in alcuni campi, ad esempio, nei sistemi ferroviari, avionici o automobilistici. Gli standard sul processo
possono prescrivere cosa debba essere prodotto nel suo corso, in che modo vadano prodotti i documenti di SRS o
i test. Possono inoltre dettare regole di programmazione, da osservare in fase di implementazione. La
programmazione strutturale impone regole: i blocchi devono avere un solo punto d’entrata ed un solo punto
d’uscita (paradigma one in one out), non bisogna fare uso di break o di go to. Altre regole prescrivono che le
variabili vengano inizializzate, di non basarsi sull’ordine di precedenza degli operatori (che potrebbe variare al
variare dei contenuti) e di disattivare eventuali ottimizzazioni automatiche del compilatore, che potrebbero
portare a errori molto gravi, in particolare nei sistemi critici. Un altro vincolo potrebbe riguardare la competenza
del personale, esclusivamente dotato di alta qualificazione o formazione.

• Sicurezza: composta da disponibilità, integrità e confidenzialità, ovvero l’assenza di diffusione non autorizzata
di informazioni.

Non c’è un completo accordo sulle definizioni degli attributi delle qualità del software.

3. Principi dell’ingegneria del software:


I principi che studiamo sono sufficientemente generali da essere applicabili lungo l’intero processo di costruzione del
software ma non sono sufficienti a guidare lo sviluppo del software (sono necessari ma non sufficienti a garantirne la
qualità). Descrivono proprietà desiderabili del processo e dei prodotti in termini astratti. Per poterli applicare
dobbiamo disporre di metodi e tecniche che incorporino le proprietà desiderate nei
processi e nei prodotti. L’individuazione dei metodi e delle tecniche da utilizzare è lo
scopo di una metodologia. Infine, per aiutare l’applicazione dei metodi e delle tecniche
ci serviamo di strumenti. La metodologia promuove un certo approccio alla soluzione
di un problema, valutando alternative e scegliendo tra di esse la più opportuna.

• Rigore: lo sviluppo del software è un’attività creativa, un complemento dev’essere il rigore: attraverso un
approccio rigoroso possiamo realizzare prodotti affidabili, controllandone il costo. Paradossalmente il rigore è un
concetto intuitivo che non può essere definito in maniera rigorosa ma è necessario a strutturare le attività di
sviluppo, fornendo precisione e accuratezza. La progettazione procede come una sequenza di passi ben definiti e
specificati, ad ogni passo corrispondono metodi e tecniche secondo un approccio rigoroso e sistematico: la
metodologia.
Esistono diversi livelli di rigore, il più alto è la formalità: richiede che il processo di sviluppo del software sia
guidato e valutato mediante leggi matematiche. Non sempre conviene essere formali in quanto è qualcosa di
estremamente oneroso. È formale qualcosa di matematicamente trattabile: non ambiguo e dal significato univoco.
Possiamo quindi automatizzare qualcosa di formale. Il processo di produzione del software ci porta da qualcosa di
estremamente informale (l’esigenza iniziale) a qualcosa di estremamente formale: la soluzione. Ciò avviene
attraverso le varie fasi di produzione (analisi, progettazione, codifica, test). Ad esempio, la descrizione di ciò che
un programma svolge può essere espressa sia mediante un linguaggio naturale sia formale. Il vantaggio della
formalità è che essa può essere alla base dei processi automatizzati. La fase in cui tradizionalmente si applica
l’approccio formale è la fase di programmazione: i programmi sono oggetti formali. Il linguaggio di
programmazione è infatti descritto da regole lessicali, sintattiche e semantiche, in maniera formale. I programmi
sono descrizioni formali che possono essere manipolate automaticamente dai compilatori (che verificano la
correttezza e traducono in linguaggio macchina).
Rigore e formalità favoriscono affidabilità e verificabilità ma hanno effetti benefici anche su manutenibilità,
riusabilità ecc… la documentazione rigorosa aiuta nel processo di manutenzione.
• Separazione degli interessi: ci consente di affrontare differenti aspetti del problema, concentrando la nostra
attenzione su ciascuno di essi in maniera separata. Ciò è essenziale per dominare la complessità: isoliamo gli
aspetti scarsamente correlati tra loro e, successivamente, consideriamo ciascun aspetto separatamente,
approfondendo i dettagli importanti per il suo trattamento. Gli aspetti possono essere separati in differenti modi:
▪ Separazione temporale: nello stesso ciclo del software si separano diverse attività in diversi periodi
temporali (ciclo di vita).
▪ Separazione di fattori di qualità: separare, ad esempio, correttezza ed efficienza, progettando prima per
assicurare la correttezza e poi focalizzandosi sull’efficienza
▪ Separazione di viste diverse: ad esempio, quando analizziamo i requisiti di un'applicazione, può essere
utile concentrarsi separatamente sul flusso di dati da un'attività all'altra all'interno del sistema e sul flusso
di controllo che governa il modo con il quale le diverse attività sono sincronizzate.
▪ Separazione di parti del sistema: affrontiamo le parti del sistema separatamente
▪ Separazione di domini: è fondamentale distinguere gli aspetti relativi al dominio del problema da quelli
relativi al dominio dell’implementazione. Le proprietà specifiche del dominio del problema valgono
indipendentemente dagli aspetti implementativi.

Nel progettare applicazioni separiamo la presentation logic, la business logic e la database logic, se ad esempio
per cambiare il dispositivo non dobbiamo cambiare solo la presentation logic ma anche altre parti vuol dire
che non abbiamo separato correttamente gli interessi.
Questo principio può dar luogo a una distinzione di responsabilità nel trattare i diversi interessi, suddividendo
il lavoro necessario ad un problema complesso in assegnamenti specifici di lavoro a diverse persone a seconda
delle capacità, separando quindi i ruoli.
Tuttavia, vi è un inconveniente intrinseco: separando due o più aspetti si può perdere la possibilità di
effettuare alcune ottimizzazioni globali, possibili invece se gli aspetti venissero affrontati
contemporaneamente.

• Differimento delle decisioni: ogni decisione va presa al momento giusto, senza anticipare il momento della
decisione rispetto a quando prenderla è effettivamente improcrastinabile. Ad esempio, la scelta del linguaggio
di programmazione non va anticipata alla fase di analisi, così come le scelte di progetto (architettura software).

• Astrazione: ci consente di identificare gli aspetti fondamentali di un fenomeno, trascurando i suoi dettagli. È un
caso particolare della separazione degli interessi,11 ci permette di separare gli aspetti importanti da quelli che
contengono dettagli secondari. A seconda dello scopo una stessa realtà di interesse può essere modellata da
molteplici astrazioni, ciascuna delle quali fornisce uno specifico punto di vista, per uno specifico scopo. Ciascun
modello usato nell’ingegneria per descrivere i fenomeni è un’astrazione della realtà. Nell’esprimere i requisiti
stessi forniamo un modello che astrae da numerosi dettagli che i progettisti decidono di poter ignorare senza
conseguenze.

• Modularità: è la suddivisione/organizzazione di un modello o di un sistema in parti (moduli), in modo che esso


risulti più semplice da comprendere e manipolare. Il vantaggio principale è che in un primo momento ci consente
di trattare i dettagli di un singolo modulo separatamente, successivamente possiamo esaminare i moduli e le loro
relazioni, in modo da integrarli in un sistema coerente. Gli approcci possono essere di due tipi: bottom-up secondo
cui ci concentriamo prima sulla realizzazione dei singoli moduli e poi sulla loro interazione; top-down per cui
scomponiamo prima il problema in moduli (anche in maniera iterativa) e poi ci soffermiamo sulla progettazione di
ciascuno di essi.
La modularità favorisce la riusabilità di parti del progetto ed è fondamentale ai fini della capacità di evolvere del
software: aiuta a confinare la ricerca di un difetto o del punto in cui intervenire per il miglioramento, limitandola
a un singolo componente. Affinché la modularità sia corretta i moduli devono avere alta coesione e basso
accoppiamento. La coesione è una proprietà interna al modulo, è alta se tutti i componenti di un modulo sono
strettamente connessi, sono raggruppati secondo un motivo logico e cooperano per un obiettivo comune.
L’accoppiamento caratterizza invece la relazione di un modulo con altri moduli, l’interdipendenza tra più moduli.
In questo modo possiamo vedere i moduli come black-box quando si vuole analizzare il sistema come struttura
complessiva.

• Anticipazione del cambiamento: i cambiamenti del software sono dovuti alla capacità di riparare il software e alla
necessità di supportare l’evoluzione dell’applicazione. La capacità del software di evolvere deve essere
pianificata e anticipata con estrema cura. I progettisti devono riuscire in qualche modo a prevedere i cambiamenti
e pianificare il progetto così da renderli agevoli. I probabili cambiamenti devono essere attribuiti a specifiche
porzioni del software e il loro effetto circoscritto a esse: l’anticipazione del cambiamento dev’essere alla base
della strategia di modularizzazione. Anche la riusabilità è favorita da questo principio, un componente è
facilmente riusabile se deve essere sottoposto solo a limitati cambiamenti per poter essere riusato, dobbiamo
quindi progettare il componente in modo che questi cambiamenti possano essere facilmente ottenuti. Questo
principio viene applicato anche al processo: un cambiamento è ad esempio il ricambio di personale.

4. Modularità:
La modularità è la scomposizione in parti del sistema. Scomponiamo per dominare la complessità. Ci sono problemi
complessi che hanno soluzioni semplici, così come ci sono problemi semplici che hanno soluzione complessa o non
hanno soluzione. Prima del concetto di complessità vi è infatti il concetto di calcolabilità. Un algoritmo è ciò che può
essere realizzato con la macchina di Turing. Un esempio di problema non calcolabile è il problema dell’arresto: non è
possibile stabilire se la macchina di Turing su un dato nastro si fermi oppure no. Il programma è la codifica
dell’algoritmo.
Il vantaggio della modularizzazione è focalizzarsi su parti più piccole, su cui è più facile lavorare. Bisogna concentrarsi
su una singola parte a prescindere dalle restanti.
Un modulo è un componente del sistema software che realizza un’astrazione: abbiamo due tipi di astrazione (sul
controllo e sui dati).

L’astrazione sul controllo avviene mediante il meccanismo dei sottoprogrammi. Nei sottoprogrammi si affronta un
problema e quando si ritorna al programma chiamante abbiamo un problema minore: non dobbiamo ragionare su
come è implementato il problema risolto dal sottoprogramma. Il modo con cui il modulo interagisce con il resto è
attraverso l’interfaccia. Nel caso dei sottoprogrammi l’interfaccia è il prototipo ed esprime cosa fa il modulo. Il corpo
è il codice implementativo, che descrive come lo fa. Attraverso il meccanismo dell’information hiding nascondiamo
nel corpo l’algoritmo, il procedimento con cui realizziamo ciò che si fa. Per far funzionare il sottoprogramma gli
passiamo dei dati (il sottoprogramma ha i suoi dati locali).

L’astrazione sui dati ci permette di astrarre le entità costituenti il sistema, descritte in termini di una struttura dati e
delle operazioni possibili su di esse. Un’informazione è descritta in generale dalla tripla tipo-valore-attributo, dove il
tipo identifica il particolare insieme entro cui si effettua la scelta, il valore la scelta effettuata e l’attributo è un
identificato che attribuisce un significato. In realtà il tipo non è solo un insieme di valori, è anche l’insieme delle
operazioni che possiamo effettuare su di essi: ciò definisce il tipo di dato astratto (ad esempio, se consideriamo il tipo
int e il tipo string è ovvio che alcune operazioni, come la somma o il prodotto, definite per il tipo int non avrebbero
senso per il tipo string: la struttura dati concreta è incapsulata nelle operazioni su di essa consentite).
Un oggetto è una struttura dati unita alle operazioni possibili su di essa Se abbiamo necessita di utilizzare più oggetti
definiremo allora un tipo (in quanto vogliamo una molteplicità di istanze): in questo caso l’oggetto sarà un’istanza del
tipo di appartenenza.
Un modo per realizzare un tipo di dato astratto è la classe, altri sono costrutti come il typedef e lo struct. La differenza
è che utilizzando typedef/struct adottiamo una disciplina di programmazione, eventuali comportamenti illeciti saranno
noti solo a tempo di esecuzione (non è appropriato per la progettazione di un software). Attraverso la classe invece
obblighiamo l’utente ad accedere all’oggetto solo attraverso le operazioni consentite: il compilatore impedirà
all’utente eventuali usi/accessi illeciti. Per fare ciò il linguaggio deve essere tipizzato e compilato. Il compilatore
controlla che le operazioni siano coerenti con il tipo e ciò è fondamentale per ottenere un software di qualità. In un
linguaggio tipizzato l’oggetto allora è istanza di un tipo, altrimenti è solo una struttura dati unita alle sue operazioni.
Nelle classi l’interfaccia è costituita dalle operazioni consentite sugli oggetti istanziati. Secondo il meccanismo
dell’information hiding, di un sotto-programma nascondevamo gli algoritmi implementativi, adesso invece di un
oggetto nascondiamo la struttura dati e gli algoritmi delle operazioni: la classe è un’astrazione più potente.
L’interfaccia in un sottoprogramma esprime cosa fa, in una classe cosa è.

Quando scomponiamo il problema procediamo secondo due criteri (astrazione sul controllo e sui dati).
Consideriamo ad esempio la gestione di una banca, secondo una scomposizione funzionale (sul controllo)
distingueremo la gestione dei conti correnti, da quella dei mutui, dei prestiti ecc.. se invece adottiamo una
scomposizione sui dati distingueremo mutui, contocorrenti, prestiti ecc.. individuiamo le entità, ci soffermiamo su cosa
sono e poi sulle operazioni.

ANALISI PROGETTAZIONE PROGRAMMAZIONE


Classi Classi Classi
Oggetto Oggetto Oggetto
Responsabilità Operazione Metodo
Proprietà Attributo Variabili membro

Il livello concettuale è il livello ontologico, riguarda l’essere, infatti nella fase di analisi abbiamo a che fare con
frammenti della realtà. In analisi individuiamo le entità che fanno parte del dominio del problema. In fase di
progettazione determiniamo la soluzione: individuiamo le entità che fanno parte del dominio della soluzione.
Le entità che fanno parte del dominio del problema costituiscono la parte statica: moduli che sussistono nel problema
e dovranno sussistere nella soluzione. Individuiamo gli oggetti che sono nel problema, dato che il livello è ontologico
individuiamo quello che sono in quanto sono e non in funzione di come vogliamo lavorarci. L’analista guarda solo al
problema, la soluzione si tratta in fase di progettazione e programmazione (differimento delle decisioni). L’analista
produce il documento di specifica dei requisiti. In fase di analisi utilizziamo il linguaggio UML, come in fase di
progettazione, in fase di programmazione utilizzeremo i linguaggi di programmazione (JAVA, C++). Il passaggio da
analisi a progettazione è più facile in quanto utilizziamo lo stesso linguaggio.
L’analista individua solo le proprietà visibili (+ in UML).

Un altro tipo di moduli sono i moduli generici (in C++ li otteniamo tramite i template). Sono parametrici (valori che
non sono costanti né variabili) e trattati in maniera generica. Se di un oggetto creiamo un tipo otteniamo un tipo di
dato astratto (ADT), se un oggetto lo rediamo generico creiamo un oggetto generico. Possiamo anche rendere generico
un tipo, ottenendo un tipo di dato astratto generico. Tipizzazione e genericità sono ortogonali.
I moduli generici possono essere template di classe e di funzione, parametrizzati rispetto a un tipo. Per essere usati
devono essere prima istanziati fornendo parametri reali (tipi). La genericità di una funzione è nella lista dei parametri.
Quando della classe astratta specifichiamo il tipo (all’atto dell’istanziazione) otteniamo la classe del tipo specificato e
da questa l’oggetto istanziato.

Un esempio di modulo generico sono gli iteratori che navigano sui contenitori (pila, lista, coda) a prescindere da come
questi siano realizzati.

Coesione e accoppiamento:

Possiamo aggregare più moduli per fare un modulo composto, un esempio è la libreria. La coesione rappresenta il
livello di omogeneità tra i componenti di un modulo, dev’essere alta. L’accoppiamento è l’interdipendenza tra moduli,
dev’essere bassa. L’accoppiamento alto è indice di una struttura software non fatta bene.
Dobbiamo avere dei criteri di coesione. In una libreria matematica tutti gli elementi saranno funzioni che svolgeranno
operazioni matematiche. Il criterio può essere logico (come in questo caso), temporale (aggreghiamo le funzioni che
vengono usate allo stesso tempo, un esempio è durante la fase di boot del computer). Possiamo anche organizzare il
sistema a strati, dove ogni livello può accedere solo al successivo, ad esempio l’applicazione può essere realizzata
molto più facilmente se realizziamo diversi livelli di astrazione. Ogni strato aumenta l’astrazione: un criterio di coesione
può essere proprio quello del livello di astrazione, ragionando a strati, il che è un modo di realizzare l’architettura
software (in fase di progettazione).
Esistono due tipologie di approccio di progettazione:

1. Top down procediamo dall’alto verso il basso. Può essere fatta in due modi, secondo l’astrazione funzionale
o sui dati. L’astrazione funzionale si sposa bene con l’approccio top-down.

2. Bottom-up: dal basso verso l’alto. Solitamente attraverso l’astrazione sui dati partiamo dal basso,
dall’individuazione delle entità facenti parte del sistema, assemblando poi i pezzi individuati.
Secondo un approccio funzionale è il dato che viene mandato alla funzione (ricerca(lista l,elem K)), nell’approccio a
oggetti mandiamo la funzione al dato (l.ricerca(K)). La principale differenza è che secondo il primo approccio partiamo
dalle funzioni, secondo il secondo partiamo dai dati. l.ricerca(K) può essere semanticamente letto come l’invio di un
messaggio di ricerca, contenente K, ad L. Il messaggio sarà mandato da un altro oggetto. Se nella ricerca ci sarà una
funzione di confronto la lista ora chiamerà a sua volta la funzione di confronto.

Il processo è il programma in esecuzione. La dinamica di un programma procedurale è: il programma comincia dalla


prima istruzione eseguibile (comunicata dal compilatore): la funzione main(). Lo spazio necessario per eseguire il
programma è il maggiore spazio necessario al nesting delle chiamate (nello stack). Non serve la somma dello spazio
delle chiamate di tutti i sottoprogrammi ma dipende dalle funzioni allocate insieme. Lo spazio necessario è noto a
tempo di compilazione.

Gli oggetti sono entità passive, il programma comincia quando invochiamo un metodo sull’oggetto ma l’oggetto viene
prima istanziato, l’oggetto viene creato dal main (che infatti è una funzione e non un oggetto). È un linguaggio ibrido,
possiamo attuare l’astrazione sui dati e sul controllo in contemporanea. Se tutto fosse a oggetti non avremmo da cui
partire, in quanto qualcosa deve istanziare gli oggetti.

5. Modellazione a oggetti:
In fase di analisi l’input è una descrizione informale, l’output saranno i requisiti scritti in maniera formale, attraverso
il linguaggio UML. Distinguiamo i requisiti sui dati, funzionali e non funzionali (prestazioni, sicurezza, affidabilità,
security, safety ecc..). Tramite i requisiti sui dati individuiamo le entità, in funzione di quello che sono e non in funzione
delle operazioni che possiamo farvi (dunque non in funzione del loro utilizzo). Attraverso i requisiti funzionali
evinciamo invece le funzionalità del software che vogliamo realizzare (da non confondere con le operazioni che
possiamo effettuare sulle entità, le funzionalità solitamente coinvolgono più entità).
Le entità individuate in fase di analisi fanno parte del dominio del problema e persisteranno anche nella soluzione,
insieme alle entità individuate in fase di progettazione che invece fanno parte del dominio della soluzione.

Se parliamo solo di classi e oggetti parliamo di object-based. L’object-oriented si basa su 4 concetti:

1. Classe
2. Oggetto
3. Ereditarietà
4. Polimorfismo

La differenza tra la programmazione object-oriented e la programmazione mediante tipo di dati astratti (typedef,
struct) è che in questo ultimo caso abbiamo solo una disciplina di programmazione, mentre con la programmazione
tramite classi il compilatore controlla che gli oggetti siano usati concordemente alle operazioni consentite su di essi.
Per l’ingegneria del software prediligiamo i linguaggi tipizzati e compilati, nei linguaggi non tipizzati e interpretati gli
errori saranno noti solo a tempo di esecuzione.
Anche in fase di implementazione parliamo di classi del linguaggio di programmazione, usando le opportune tecniche.

L’ereditarietà supporta relazioni di tipo generalizzazione/specializzazione (gen-spec), espresse da legami del tipo
“è-un”. Nella modellazione ad oggetti è di fondamentale importanza in quanto induce una strutturazione gerarchica
nel sistema software e permette di risparmiare (riuso del software) sulle parti che la classe figlia eredita dalla classe
base, nel momento in cui stiamo effettuando una specializzazione (minimizza la parte di codice da scrivere nella
sottoclasse). Generalizzazione e specializzazione sono due procedimenti mentali, l’uno il duale dell’altro: nel primo
caso passiamo dal particolare al generale, viceversa nel secondo. L’ereditarietà dunque non coincide con questo tipo
di relazione ma lo supporta, siamo in grado di effettuare solo la specializzazione. In realtà possiamo effettuare una
specializzazione anche senza la presenza di un reale legame è-un , per fini puramente pratici.

Il polimorfismo può essere statico o dinamico. L’overloading dei nomi di funzioni è una forma di polimorfismo statico,
spaziale: la funzione assume forme diverse in diversi punti del programma, il compilatore risolve il problema di quale
particolare funzione chiamare, in base alla sua signature. Il polimorfismo dinamico (a cui generalmente ci si riferisce
con il termine polimorfismo) consiste nella proprietà di un’entità di assumere forme diverse nel tempo, si utilizza
quando a tempo di compilazione non è noto il tipo dell’oggetto che invoca un metodo. Un’entità polimorfa può far
riferimento nel tempo a classi diverse (di una stessa gerarchia).
Ora il risparmio si trova nel codice del modulo utente che utilizza la gerarchia (e non nella codifica delle classi, come
era per l’ereditarietà): l’ereditarietà diminuisce la quantità del codice da scrivere, il polimorfismo invece la quantità
del codice da modificare, in entrambi i casi nel momento di aggiunta di una nuova classe. Un’ulteriore differenza è che
per quanto riguarda il codice scritto nell’ereditarietà abbiamo comunque l’esigenza di ricompilare, nel polimorfismo
no in quanto si realizza mediante il late binding (binding dinamico, determina a tempo d’esecuzione il corpo del
metodo da invocare su un dato oggetto).

In C++ un puntatore alla classe base può lecitamente puntare ad un oggetto della classe derivata, non vale invece il
viceversa. Nel campo dell’oggetto c’è a sua volta un puntatore alla tabella dei metodi virtuali (contenente gli indirizzi
delle funzioni virtuali), il compilatore dunque arriva alla funzione tramite indirizzamento indiretto.

Come si realizza il binding dinamico: il binding dinamico si realizza


come azione combinata del compilatore e del linker attraverso
una ricerca tabellare a tempo di esecuzione.
Per ogni classe C viene creata una tavella dei metodi virtuali,
contenente gli indirizzi delle funzioni virtuali di C. Ogni volta che
viene istanziato un oggetto della classe C gli viene attribuito un
puntatore alla tabella dei metodi virtuali della sua classe. Nel
momento in cui abbiamo la chiamata alla funzione virtuale tramite
puntatore a classe base (p->f()):

1. A tempo di compilazione sono noti solo l’indirizzo di p e la signature della funzione f;


2. A tempo di esecuzione il puntatore p punta all’oggetto c e c punta alla tabella dei metodi della sua classe C (in
questo modo viene eseguita una ricerca tabellare della funzione chiamata).

6. Diagrammi dei casi d’uso:


In UML attraverso il diagramma dei casi d’uso modelliamo le funzionalità del sistema software. Un caso d’uso è una
tipica interazione tra un attore ed il sistema, per svolgere un’unità di lavoro utile. L’attore è un utilizzatore del
sistema, identifica il ruolo che un’entità esterna assume quando interagisce direttamente con il sistema e può essere:

• Una classe di persone fisiche (un ruolo)


• Un altro sistema software
• Un dispositivo hardware esterno (ad esempio dei sensori)
• Il tempo (ci sono funzioni del sistema che avvengono in un certo momento noto, non provocate da nessun
attore specifico, ad esempio l’attivazione delle sveglia, un procedura di backup automatica ecc..)

Il caso d’uso inizia quando inizia l’interazione. Ogni caso d’uso comincia quando un attore lo innesta. L’attore che ne
richiede l’esecuzione si dice attore primario, un attore che pur non scatenando il caso d’uso in qualche modo vi
interagisce è detto attore secondario. Con “unità di lavoro utile” si intende utile dal punto di vista dell’attore. I casi
d’uso non rivelano infatti l’organizzazione interna del sistema ma modellano le funzionalità così come sono percepite
dagli attori.

Il diagramma dei casi d’uso è una rappresentazione grafica dell’insieme dei casi d’uso (funzionalità esterne), spesso
comprensibile anche ai “non addetti ai lavori”, consiste di:

1. Attori
2. Casi d’uso
3. Relazioni tra attori e casi d’uso
4. Confini del sistema: dobbiamo stabilire cosa fa parte del sistema (dentro i suoi confini) e cosa non ne fa parte
(fuori dai suoi confini).

Nel diagramma i casi d’uso sono rappresentati da ovali, battezzati da una stringa che descrive brevemente la
funzionalità. I confini del sistema sono individuati da un rettangolo (sulla cui testa è posto il nome del sistema: subject)
e gli attori sono rappresentati da omini. All’interno del sistema vi è tutto ciò che ci interessa, ciò che si deve fare. Gli
attori sono esterni al sistema. Ad ogni ovale è associata almeno una linea, verso il suo attore primario: non ci può
essere un caso d’uso senza un attore che lo innesti. L’insieme dei segmenti definisce l’interazione, la comunicazione
tra attore e caso d’uso: chi fa cosa.

Anche nei diagrammi dei casi d’uso individuiamo delle relazioni, che possono essere:

• Relazioni di generalizzazione tra attori: ci permette di astrarre ruoli comuni a più attori, semplificando i
diagrammi. Gli attori specializzati ereditano i ruoli e le relazioni dell’attore generalizzato.
• Relazioni di generalizzazione tra casi d’uso: possono esservi uno o più casi d’uso che specializzano un caso
d’uso più generalizzato, da cui ereditano comportamento e significato. I casi d’uso figli rappresentano delle
varianti più specifiche del caso d’uso generalizzato, attraverso l’aggiunta o la modifica di alcuni
comportamenti.
• Relazioni d’uso (inclusione): formalizzano il caso in cui più casi d’uso racchiudono una sequenza di passi
comuni. Evitiamo di specificare ogni volta questi passi rendendo la sequenza comune un vero e proprio caso
d’uso, incluso nei casi d’uso di partenza. Il caso d’uso cliente (includente) non può essere svolto senza il caso
d’uso incluso, è incompleto senza di esso: il caso d’uso incluso è parte integrante del cliente. La sintassi
dell’inclusione è molto simile alla chiamata di una funzione: il caso d’uso cliente esegue la propria sequenza di
eventi fino al punto di inclusione, l’esecuzione passa quindi al fornitore (caso incluso) e torna nuovamente al
caso d’uso cliente quando il fornitore ha terminato la propria esecuzione. L’inclusione si rappresenta con una
freccia tratteggiata che porta dal cliente al fornitore.
• Relazioni d’estensione: descrivono una sequenza opzionale (una variante) aggiunta al caso d’uso. Questa volta
le rappresentiamo con una freccia tratteggiata che porta dal caso d’uso d’estensione all’originario. Il caso d’uso
esteso si può svolgere senza il caso d’uso estendente, è del tutto completo senza le sue estensioni
(diversamente dall’inclusione). L’estensione è quindi una variante o un’aggiunta che si verifica sotto particolari
condizioni.

Ogni caso d’uso si svolge attraverso una sequenza di azioni. Documentiamo ciascun caso d’uso con una scheda che
descrive la sequenza di eventi principali, senza entrare nel dettaglio di come si svolgano (siamo ancora in fase di
analisi). È utile in quanto il diagramma esprime solo il nome delle funzionalità, senza descriverle. L’UML non fornisce
standard per la formulazione delle specifiche e delle schede dei casi d’uso. La scheda è espressa tendenzialmente in
forma testuale e mostra i singoli passi che compongono il caso d’uso. In generale sono costituite da:

• Nome del caso d’uso (per convenzione lo indichiamo all’imperativo presente)


• ID
• Breve descrizione
• Attori primari (almeno uno)
• Attori secondari, che invece possono anche non esservi
• Precondizioni, ovvero condizioni che devono essere verificate prima di eseguire la sequenza. Vincolano lo stato
del sistema prima che il caso d’uso possa iniziare l’esecuzione, in questo modo impediscono all’attore di far
scattare il caso d’uso senza che siano state soddisfatte tutte le condizioni previste (è quindi preferibile
esprimere le condizioni come semplici affermazioni che possano risultare vere o false: condizioni booleane)
• Sequenza degli eventi principali
• Postcondizioni, condizioni che devono verificarsi dopo la sequenza
• Sequenza degli eventi alternativi

All’interno della sequenza possiamo trovare un punto d’estensione che indica il punto in cui, sotto indicate condizioni,
si passa al caso d’uso estensione. Il punto d’estensione può anche essere modellato in UML nell’ovale del caso d’uso
esteso. Il caso d’uso che estende avrà una sua scheda, la cui precondizione sarà proprio la condizione espressa nel
punto d’estensione. Anche la relazione d’inclusione piò essere opportunamente annotata: il caso d’uso cliente avrà
nella sequenza degli eventi principali l’inclusione del caso incluso (che a sua volta sarà dotato di una propria scheda).
La sequenza degli eventi elenca i passi che compongono il caso d’uso e comincia sempre con un attore che fa qualcosa
per dare inizio al caso d’uso (Ad esempio, 1. il caso d’uso inizia quando un <attore> <funzione>). L’attore può anche
essere il tempo, in questo caso la sequenza comincerà con un’espressione temporale (ad esempio, 1. Il caso d’uso
inizia quando si conclude un trimestre scolastico). È buona norma che i passi siano concisi, dichiarativi, numerati e
temporalmente ordinati. Le schede prendono il nome di scenari. Ogni caso d’uso ha almeno uno scenario, ovvero lo
scenario principale, che descrive la sequenza di eventi che si verifica quando tutto va a buon fine (senza errori,
interruzioni o deviazioni). Un caso d’uso può avere più scenari, al fine di modellare anche le situazioni in cui non tutto
procede come desiderato. È indicato cercare di limitare il numero degli scenari secondari al minimo necessario,
limitandosi a descrivere i più importanti.

I diagrammi dei casi d’uso sono utili all’analista per comunicare (così come in generale l’UML). Durante la produzione
del software produciamo artefatti intermedi, tra cui documenti. Gli artefatti servono alla comunicazione: l’analista
deve comunicare con il progettista, con il committente/cliente ed in generale con gli stakeholders. Per quanto non sia
una descrizione formale, il diagramma dei casi d’uso è facile da capire ed è utile per la validazione, dunque l’analista
se ne serve per confrontare i requisiti con ciò che gli stakeholders si aspettano, così da correggere errori che altrimenti
si propagherebbero fino alla consegna. La formalità ci consente di esprimere qualcosa matematicamente ed è utile
all’automatizzazione ma non sempre è consigliata: il conseguente appesantimento della notazione comporta maggiore
difficoltà di comprensione (soprattutto per i “non addetti ai lavori”). L’analista ha l’esigenza di comunicare tramite il
diagramma per ricevere dei feedback, in particolar modo quando i requisiti ricevuti non sono chiari (altrimenti si piò
anche evitare). Per questo motivo nei diagrammi va inserito solo ciò che effettivamente si vuole comunicare, evitando
banalità o superfluità. Gli scenari (che, a differenza dei diagrammi, non fanno parte dell’UML) servono allo stesso
scopo.

7. Diagrammi delle classi:


Le classi e le relazioni tra di esse vengono modellate mediante il diagramma delle classi.

• In fase di analisi ci occupiamo di modellare le entità del dominio del problema, fornendo una vista concettuale.
Consideriamo gli aspetti principali del dominio a prescindere da come saranno trattati, rappresentati in dati e
dalle funzionalità che opereranno su di essi. L’attenzione viene rivolta esclusivamente alla comprensione di
cosa il sistema software dovrà fare e non del come lo farà.

• In fase di progettazione ci occupiamo invece di modellare le entità del dominio della soluzione, le entità del
sistema. Sia il diagramma di progettazione che il diagramma in fase di analisi sono diagrammi statici.

• Anche in fase di programmazione è possibile servirsi di questo tipo di diagramma. In questa fase si passa dalla
progettazione della classe alla loro codifica in linguaggio di programmazione. Possiamo utilizzare i diagrammi
UML raffinandoli fino ad arrivare al punto in cui la loro traduzione in codice sarà immediata. In un algoritmo
dobbiamo implementare anche gli aspetti dinamici. Un programma (ovvero la traduzione di un algoritmo in
linguaggio di programmazione) è una descrizione statica, quello che accade dipende dai particolari dati in
ingresso, quello che viene eseguito è quindi solo una delle possibili esecuzioni del programma: non si esegue
un programma ma si esegue un processo. Il teorema di Bohm-Jacopini, riguardo la potenza espressiva del
linguaggio, afferma che tutti i linguaggi hanno la stessa potenza espressiva se sono dotati dei costrutti di
sequenza, selezione ed iterazione. Possiamo tradurre un algoritmo in un linguaggio non obbligatoriamente
testuale ma possiamo utilizzare anche diagrammi e da questi automatizzare il processo di traduzione. I
diagrammi delle classi sono però statici, per generare il codice dinamico vanno quindi arricchiti, si parlerà di
diagrammi dinamici.

La classe si rappresenta attraverso un rettangolo, nella cui parte superiore è posto il nome, nella parte centrale le
proprietà e nella parte sottostante le responsabilità (che in fase di progettazione saranno rispettivamente gli attributi
e le operazioni). Una volta modellate le entità vanno modellati i legami tra di esse. I legami possono essere di 3 tipi:

• Generalizzazione/specializzazione: esprimono legami del tipo è-un tra le entità del dominio del problema.
• Tutto/parti (o contenimento): per questo tipo di legame distinguiamo un legame debole (aggregazione) da
un legame forte (composizione), in base alla vita relativa del tutto e delle parti o dalla perdita o meno
dell’identità della parte quando entra a far parte del tutto. Nel legame di aggregazione quando la parte entra
a far parte del tutto (ad esempio, le ruote di un’auto) non perde la propria identità, nel legame di composizione
invece sì (un esempio è la farina nel pane). Dal punto di vista del linguaggio UML il contenimento è
rappresentato da un rombo, pieno per la composizione, vuoto per l’aggregazione. In realtà esistono 7 diverse
sfumature di questo legame ma se ne modellano solo 2 per non rendere pesante la notazione. (un esempio è
la sfumatura temporale: in un volo Napoli-New York formato dalle tratte Napoli-Roma e Roma-New York
l’ordine delle tratte è fondamentale, se lo invertiamo non possiamo arrivare da Napoli a New York).
• Associazione: l’associazione rappresenta il concetto matematico di corrispondenza, un sottoinsieme del
prodotto cartesiano tra due insiemi. I legami associativi sono tutti i legami che non sono di tipo gen/spec o
tutto/parti. Per esclusione otteniamo quindi una definizione operativa di associazione. La indichiamo
attraverso un sostantivo per non dare una direzione in quanto il prodotto cartesiano è un insieme in cui le
coppie non sono ordinate, non c’è direzione. Se c’è un legame tra A e B, A è legato a B così come B è legato ad
A. In un’associazioni le entità possono assumere dei ruoli, che verranno specificati tra di essi. Si considerano
infine due numeri per ciascuna entità (cardinalità dell’associazione), che rispondono alla domanda: qual è il
numero minimo/massimo di elementi dell’altro insieme in corrispondenza con un elemento dell’insieme
considerato?

A Ruolo di B associazione Ruolo di A B

Per realizzare i diagrammi delle classi dobbiamo identificare i requisiti sui dati, che nell’analisi testuale saranno
identificati da sostantivi. Dobbiamo però distinguere le classi dai loro attributi. I legami vanno invece ricercati tra i
verbi. Quando ci sono legami della realtà questi rappresentano puramente fatti. Possiamo modellare anche i fatti
come classi, parleremo di classi associative, che a loro volta potranno essere dotate di attributi e operazioni. Ci sono
tre condizioni, affinché si possa realizzare una classe associativa:

1. deve esistere l’associazione tra le due entità;


2. l’associazione deve essere molti a molti, non si può realizzare se è uno ad uno oppure uno a molti;
3. l’associazione deve essere univocamente identificata dagli estremi dell’associazione stessa e dai suoi
attributi: data qualsiasi coppia di oggetti delle due classi esiste un’unica associazione, legata ad una sola entità
A ed una sola entità B per quegli attributi. Se può esservi più di un’associazione tra le due classi, non può
essere realizzata la classe associativa.
Se non possiamo utilizzare la classe associativa, creiamo un’altra classe posizionata tra le due classi.
Nel nostro caso, dobbiamo presuppore o che un contatto non lavora dopo aver terminato il lavoro precedente, oppure
che non vogliamo tenere memoria dei lavori precedenti.
Codifica dei diagrammi:

In fase di analisi gli attributi avranno visibilità pubblica (altrimenti non saremmo in grado di individuarli), nella codifica
saranno tradotti con una variabile membro e le rispettive operazioni di get/set per la sua lettura e scrittura. In analisi
le get/set non vengono inserite nel diagramma delle classi in quanto la presenza di un attributo pubblico implica che
ci saranno le corrispondenti operazioni di get e di set nell’implementazione della classe. In realtà vi sono più modi per
tradurre gli attributi, ad esempio l’attributo saldo (di sola lettura) di un conto corrente può essere tradotto in una
varabile membro con la corrispondente operazione di get, oppure come un attributo derivato, ottenendolo
direttamente attraverso calcoli sugli ingressi e le uscite. La scelta spetta al programmatore.
Le operazioni saranno tradotte nelle corrispondenti funzioni membro.

Tendenzialmente il diagramma delle classi non subisce variazioni tra la fase di analisi e la fase di progettazione, si
possono aggiungere o raffinare alcuni dettagli, ad esempio aggiungendo operazioni private (che possono essere
chiamate solo da altre operazioni) ma è importante che le ragioni non riguardino la programmazione.

La relazione gen-spec, ad esempio tra la classe auto e la classe taxi si traduce in questo modo:

class Taxi : <modalità-di-derivazione> Auto{ ….

La relazione di contenimento, in particolare debole, tra la classe Auto e la classe Ruota si traduce inserendo nella
classe Auto una variabile membro di tipo puntatore a Ruota (Ruota * r;), in realtà avendo 4 ruote lo traduciamo come
un puntatore a un vettore di ruote (Ruota * r[4];). Le ruote devono esser già state create ed è necessaria un’operazione
che faccia in modo che i puntatori della classe contenitore puntino agli oggetti contenuti. Se il contenimento è stretto
non inseriamo nella classe contenitore un riferimento a oggetti istanze della classe contenuta ma direttamente oggetti
istanze della classe contenuta.

La relazione di associazione tra la classe Auto e la classe Persona (che possiamo chiamare proprietà e i cui ruoli saranno
rispettivamente proprietà e auto-posseduta) si traduce inserendo nella classe Auto una variabile membro puntatore a
un tipo persona, che chiameremo proprietario (Persona * proprietario;), vi sarà poi un’operazione, con tipo di ritorna
Persona (o riferimento a Persona) che restituirà il proprietario. Potremmo allo stesso modo mettere nella classe
Persona una variabile membro puntatore a un oggetto (o un vettore di oggetti) di tipo Auto, di nome auto-posseduta
(Auto * auto-posseduta[n]), avremo quindi una funzione membro che restituirà tale informazione. Avremo un
puntatore a oggetto o a vettore a seconda della cardinalità dell’associazione (che indica il numero di elementi
dell’eventuale vettore).
Tendenzialmente, dato che l’associazione si basa sul concetto matematico di corrispondenza, non vi dovrebbe essere
direzionalità in questa. L’analista può indicarla con una freccia per indicare un’eventuale direzionalità che proviene
direttamente dai requisiti dinamici (che rappresentano i requisiti funzionali). Una freccia da auto a persona esprime
che si può voler risalire dall’auto al proprietario: è attribuita all’auto la responsabilità di risalire al proprietario.
Possiamo specificare la direzionalità in quattro modi: un segmento (senza direzionalità), freccia a dx, freccia a sx,
freccia bidirezionale. La freccia dalla classe A alla classe B viene tradotta in un’operazione, nella classe da cui parte la
freccia, che restituisce un puntatore a un oggetto della classe in cui arriva la freccia.

La freccia nel diagramma indica la navigabilità delle associazioni:

è navigabile da A a B e viceversa: è attribuita ad A la responsabilità di risalire a B e viceversa.

X è navigabile da A a B ma non da B ad A. In questo modo evitiamo la duplicazione dei dati, che


comporta problemi di inconsistenza. Vi saranno strutture dati concrete, poste sia in A sia in B. Nel momento in cui
effettuiamo una modifica se vi sono malfunzionamenti rischiamo di avere inconsistenze nei dati (causate dalla
ridondanza).

È navigabile da A a B, da B ad A è indefinito. Il concetto di indefinito è analogo al concetto di valore


nullo nelle basi di dati (il valore nullo può indicare sia che il valore non esiste sia che il valore non è noto). L’analista
non da vincoli sulla navigabilità da A a B.

Possiamo poi avere il segmento oX X


Ovviamente esprimere le associazioni in questi modi appesantisce molto il modello, esistono quindi 3 idiomi: il primo
idioma permette l’utilizzo di tutte le notazioni, il secondo permette l’utilizzo unicamente del segmento ed il terzo
consente il segmento e la freccia. Il primo idiomi è il più pesante ma anche il più formale. Il secondo ed il terzo sono
meno pesanti, meno formali e dunque più ambigui.

In fase di analisi i legami rappresentano i fatti della realtà e nella realtà non hanno direzionalità. La direzionalità deriva
unicamente da qualche requisito funzionale, da aspetti dinamici ed è l’unica violazione del fatto che i diagrammi delle
classi siano diagrammi statici.

Consideriamo come esempio di classe associativa la classe Matrimonio: avremo come variabile membro un
riferimento ad un oggetto della classe Uomo con il nome del rispettivo ruolo (Uomo * marito) e un riferimento ad un
oggetto della classe Donna (Donna * moglie). Le operazioni dell’associazione verranno inserite tra le funzioni della
classe.

Il modello del sistema è uno, i vari diagrammi sono viste differenti di uno stesso modello. Ogni diagramma tra l’altro
può essere rappresentato con diversi livelli di astrazione a seconda della persona a cui sarà destinato il modello.
Vediamo la relazione tra UCD (use case diagram) e CD (class diagram): il primo modella le funzionalità che il sistema
software deve fornire ed esse si svolgono sulle entità modellate nel CD, quindi le due viste devono essere tra di loro
consistenti e coerenti. La consistenza si verifica prendendo i vari requisiti funzionali, quindi una per una le varie
funzionalità modellate nell’UCD, il cui svolgimento è descritto dallo scenario e navigando sul CD per vedere se lo
svolgimento dello scenario può aver luogo nel modello delle classi realizzato: se vengono svolti tutti i casi d’uso e c’è
una classe non navigata in nessun caso d’uso, vuol dire che quella classe non è necessaria; viceversa, se c’è qualche
classe che non è stata prevista e la sua assenza non consente lo svolgimento di uno o più casi d’uso, sarà necessario
correggere il diagramma delle classi. è bene notare che la navigabilità del diagramma è una freccia su un’associazione
che stabilisce a chi va attribuita la responsabilità di risalire da un dato all’altro (necessario per i casi d’uso), le entità
modellate nel CD non devono dipendere dalle funzionalità.
I due diagrammi sono, perciò, due viste diverse e complementari, e soprattutto consistenti, per garantire una corretta
ispezione sul diagramma.

Classi boundary e classi control


L’utente interagisce con il sistema mediante interfacce, che devono essere modellate. In particolare utilizziamo due
convenzioni/stereotipi: boundary e control.
Le classi boundary (se una classe è tale, va scritto sopra il nome della classe <<boundary>>) indicano il confine tra
l’utente ed il sistema: nel diagramma dei casi d’uso vediamo questo confine esattamente nel rettangolo. Per ogni
attore primario (NON secondario), che attiva un caso d’uso, esiste una classe boundary corrispondente. Le operazioni
di una classe boundary sono esattamente i casi d’uso attivati da quell’attore. È necessaria una coerenza tra i diversi
diagrammi, quindi non è un caso che le operazioni della classe boundary siano proprio i casi d’uso del sistema. Le
operazioni di una classe boundary non hanno parametri, perché sono solo l’attivazione dei casi d’uso.
Se il caso d’uso è semplice, è stesso il boundary che comunica con le entità. Quando, invece, ci sono cose più
complesse, specialmente quando è necessario comunicare con più classi contemporaneamente, il boundary non
comunica con le entità ma con una sola classe intermedia, che è la control (gestione-nomesistema): essa è la classe
che gestisce tutto il sistema (in analisi solitamente è sufficiente una sola classe control per tutto il sistema, in
progettazione può essere suddivisa in più gestori). Il control ha la responsabilità di mettere insieme più classi o svolgere
algoritmi particolari per gestire quelle classi, solitamente vengono delegate alla classe control tutte le operazioni
(tutti i casi d’uso), ma essa necessita di parametri, prelevati dalla classe boundary.
Quindi una classe qualsiasi del diagramma delle classi utilizza il gestore: l’associazione <<use>> viene realizzata
mediante una freccia tratteggiata.
Il gestore si occupa di tutto il ciclo di vita delle entità.
8. Ingegneria dei requisiti:
Per ogni modello di ciclo di vita del software è fondamentale un’attenta analisi dei requisiti. In questa fase ci occupiamo
di capire le funzionalità e le qualità che devono essere assicurate dal software, tralasciando come queste saranno
realizzate, nel rispetto della dicotomia what versus how. La specifica non dovrebbe sovra-specificare il sistema
vincolando la progettazione e tantomeno l’implementazione a seguire scelte prematuramente definite. Ci limitiamo
ad analizzare il dominio applicativo, il dominio del problema entro il quale nasce l’esigenza. Dobbiamo identificare gli
stakeholders (i portatori di interesse del nostro sistema) e le relative esigenze. Per ogni stakeholders vi sono requisiti
che possono essere funzionali (cosa il software deve fare) o non-funzionali (qualità e vincoli, magari dovuti
all’integrazione del software in un sistema preesistente). I vari stakeholders possono avere esigenze differenti, è
dunque necessario che i requisiti, oltre a essere chiari, non ambigui e facilmente comprensibili, siano anche tra di
loro coerenti (vanno sanate eventuali contraddizioni dovute a differenti esigenze) e completi, sia internamente (se
utilizzano nuovi termini o concetti devono definirli) che globalmente (devono documentare ogni necessità richiesta).
Nella pratica è difficile raggiungere sia la totale completezza sia la totale coerenza in quanto gli stakeholders hanno
necessità molto diverse tra loro. I requisiti sono di fondamentale importanza in ogni fase del ciclo di vita del software
e vanno gestiti in ciascuna di esse. Prima di rilasciare il software dobbiamo verificare che esso sia corretto, dobbiamo
verificare che sia conforme ai requisiti specificati, che dunque ritornano necessari in questa fase finale. Per la stessa
progettazione dei test abbiamo bisogno dei requisiti: pianifichiamo i test non appena abbiamo specificato i requisiti,
ciascun requisito per essere un “buon requisito” dev’essere infatti facilmente verificabile. I requisiti verranno
successivamente aggiunti o modificati in fase di manutenzione.
L’ingegneria dei requisiti è la disciplina che cerca di sviluppare metodi standard e sistematici per la gestione dei
requisiti, dalla loro nascita, alla loro verifica, fino a dopo il rilascio del software.

Possiamo effettuare una distinzione tra:

- Requisiti utente: riguardano le funzionalità e i servizi che il software deve offrire ed eventuali vincoli operativi
da rispettare. Generalmente sono scritti per i clienti (clienti, client manager, system architects..) e per
l’approvazione del contratto. Sono scritti in linguaggio naturale o tramite diagrammi, così da essere facilmente
comprensibili anche ai “non addetti ai lavori”.
- Requisiti di sistema: si tratta di un documento strutturato che fornisce una descrizione dettagliata delle
funzionalità e dei servizi del sistema. I requisiti di sistema sono in parte comuni e in parte diversi dai requisiti
utenti. Nei requisiti utenti, ad esempio, potrebbero esservi requisiti di interfacciamento, non presenti nei
requisiti di sistema.

Abbiamo la necessità di scrivere i requisiti in


diversi livelli di dettaglio in quanto essi
saranno letti da più parti ed in modo diverso.
Tendenzialmente gli utenti non sono
interessati a una specifica più dettaglia,
tuttavia vi sono situazioni in cui i requisiti di
sistema interessano anche il cliente, ad
esempio in gare d’appalto o in relazione al
rapporto tra prezzo e qualità del nostro
progetto rispetto ad altri.

I requisiti funzionali riguardano le funzionalità che il software deve offrire e ciò che dovremo verificare in fase di
testing, pertanto sono espressi mediante forme verbali come “deve”. Essi devono descrivere:

- Quali input il sistema deve accettare


- Quali output deve produrre
- Quali dati bisogna gestire
- Quali elaborazioni bisogna svolgere
- Eventualmente tempificazione e sincronizzazione
Modelliamo i requisiti funzionali tramite il diagramma dei casi d’uso e i relativi scenari, questi ultimi indicano input,
output, precondizioni e post-condizioni.

I requisiti non-funzionali impongono vincoli o qualità, riguardano tempi di risposta, uso delle risorse (efficienza e
prestazioni), affidabilità, manutenibilità ecc..

I requisiti non funzionali possono


riguardare il prodotto (efficienza,
portabilità, security, reliability..),
il processo e possono essere
requisiti esterni, quale ad
esempio l’interoperabilità,
oppure requisiti etici (i dati non
vanno esposti ad usi impropri).
Possono esistere vincoli sul
processo, in particolare nei
sistemi critici. In quest’ultimo
caso, infatti, la garanzia che un
sistema sia safe, sicuro, robusto,
può esser conferita non solo dalla
qualità del prodotto ma anche
indirettamente dalla qualità del processo.
Alcuni sistemi sono soggetti a vincoli sui tempi di risposta, i sistemi real time sono i sistemi in cui la risposta deve
arrivare in tempo utile, entro una deadline (scadenza). In particolare, i sistemi hard real time sono i sistemi per cui la
scadenza è mandatoria, va rispettata obbligatoriamente. Nei sistemi soft real time la scadenza va rispettata in
probabilità. Realizzare un sistema hard real time è più costoso, pertanto si utilizzano quando serve, ad esempio per i
sistemi critici. Per garantire il rispetto della scadenza dobbiamo avere sufficienti risorse, se le risorse non sono
sufficienti non possiamo accettare la scadenza. I sistemi a tempo reale richiedono quindi un vaglio prima delle
richieste: non possono esservi più richieste di quelle rispettabili, altrimenti si arriverebbe ad una failure, il sistema si
discosta da un comportamento atteso (ricordiamo che un sistema è corretto quando rispetta le specifiche).
L’automobile è un esempio di sistema HRT.

Il sistema informatico è diverso dagli altri tipi di sistema. In fisica il rendimento di una macchina è il rapporto tra
l’energia ottenuta in forma utile e quella spesa. È il rapporto tra l’ingresso e l’uscita. Se consideriamo un trasformatore
di un cellulare questo sarà il rapporto tra l’energia trasmessa e quella ricevuta. Il rendimento è una grandezza
adimensionale, compresa tra 0 e 1. Non può essere pari ad 1 in quanto parleremmo di macchine ideali, nel
trasformatore il rapporto non può essere pari ad 1 a causa della
dissipazione dell’energia sotto forma di calore, è pari circa a 0,98.
Se consideriamo un’automobile il suo rendimento è di gran lunga
inferiore, il motore si riscalda talmente tanto da aver bisogno del
liquido di raffreddamento del motore e del radiatore per
raffreddarlo.
In un sistema informatico invece l’input è il lavoro richiesto,
l’output è ciò che viene eseguito (lavoro prodotto). Dire che il
rendimento del sistema è pari a 0,98 vuol dire che la CPU è impegnata al 98%, dunque il sistema diventa lento. Quando
il rendimento si avvicina a 1 la CPU lavora troppo e siamo costretti ad inviare meno richieste, altrimenti rallenteremmo
tutti i processi. Quando il calcolatore esegue i programmi richiesti esegue anche tutti i programmi necessari al loro
svolgimento, consideriamo allora un’unità di tempo: il rapporto tra user program e l’unità di tempo è detto duty cycle
(ciclo di lavoro utile). Il rapporto tra il lavoro non utente e l’unità di tempo dev’essere baso affinchè il duty cycle sia
alto. Il sistema operativo dev’essere quindi fatto da programmi semplici, con poche istruzioni, altrimenti
impiegherebbe troppo la CPU, riducendo il ciclo utile. Quando il rendimento si avvicina a 1 aumentano i tempi di
attesa. Se il rendimento è basso (0,2) il calcolatore non è impegnato. Per i sistemi informatici l’interesse del
rendimento alto è di colui che paga, l’interesse dell’utilizzatore è che invece la macchina sia sufficientemente libera
quando vi si rivolge. Nei sistemi informatici rendimento alto e rendimento basso sono due interessi contrastanti, è
necessario un trade-off. L’efficienza è l’uso adeguato delle risorse ed una CPU utilizzata circa al 70% è utilizzata
adeguatamente.

Un attributo non funzionale che spesso è specificato in maniera molto vaga è l’usabilità. I clienti sono soliti specificare
ad esempio che “il sistema deve essere user friendly” ma questo è un requisito formulato in maniera poco chiara e
quindi è poco testabile.

Gli attributi devono essere chiari, non ambigui, testabili, completi e coerenti. I requisiti non corretti possono portare
a consegne ritardate, all’insoddisfazione dell’utente (che può verificarsi anche quando il software è corretto ma non
soddisfa i bisogni dell’utente). In generale i requisiti non corretti portano ad un aumento dei costi: i difetti sono tanto
più costosi da correggere quanto più tardi vengono corretti rispetto al momento in cui il difetto si inserisce nel sistema.
Un difetto nella specifica va notato prima della fase di progettazione, serve un’ispezione (magari effettuata da esterni)
che controlli che siano effettivamente chiari, verificabili e non ambigui. La completezza è invece una proprietà più
difficile da verificare.

Faults – errors – failures chain


Le cause per cui un sistema può portarsi in uno stato incoerente, e dunque fallire, sono molteplici e possono
manifestarsi in ogni fase del suo ciclo di vita: guasti hardware, errori in fase di progettazione hardware e/o software
oppure errati interventi di manutenzione.

Un difetto, fault, è uno stato improprio dell’hardware e/o del software del sistema derivante dal guasto di un
componente, da fenomeni di interferenza, da errori di progettazione o da errori commessi in fase di utilizzo.
Il fault può portare ad un errore. Un errore, error, è la parte dello stato di un sistema che può indurre al fallimento,
ovvero a fornire un servizio non conforme alle specifiche (unproper service). Se l’errore è opportunamente rilevato si
dice detected, se l’errore esiste ma non è rilevato si parla di latent error.
Un fallimento, failure, è definito come l’evento in corrispondenza del quale il sistema cessa di fornire un servizio
corretto, è lo scostamento dal comportamento atteso.
Fault, error e failure sono legati da una precisa relazione definita come la patologia del guasto schematizzata in questo
modo:

Un fault può degenerare in un errore mediante attivazione (fault activation): in tal caso il guasto si dice attivo (active),
altrimenti è dormine (dormant). L’attivazione di un guasto provoca la transizione del sistema da uno stato di corretto
funzionamento (correct behavior) ad uno stato improprio (error); la rilevazione di un errore e le opportune operazioni
di ripristino riportano il sistema ad operare in maniera corretta, se ciò non avviene l’errore può degenerare in un
fallimento mediante propagazione e si manifesta all’interfaccia del sistema alterando la correttezza del servizio.

La failure dunque è la manifestazione di un difetto (fault). Il difetto conduce alla failure se attivato: error.

Le fasi dell’ingegneria dei requisiti


L’ingegneria dei requisiti sviluppa metodi per raccogliere, documentare, classificare, analizzare e gestire i requisiti. E’
essenziale, in quanto errori in questa fase si propagano nei passi successivi e il costo per porvi rimedio cresce col
passare del tempo. I requisiti non corretti possono portare a consegne ritardati, costi maggiori, insoddisfazioni
dell’utente, comportamenti errati ed imprevisti, un prodotto software corretto (conforme alle specifiche) ma che non
soddisfi i bisogni dell’utente, altri costi di manutenzione (appena finita l’analisi delle specifiche, si può controllare la
correttezza, la verificabilità, la completezza mediante una review da parte di altri).

Le fasi principali della Requirements Engineering sono:


- Elicitation (esplicitazione): in questa fase, si acquisiscono i requisiti tramite consultazioni con gli stakeholders,
documenti, conoscenza del dominio e studi del mercato: si cerca di far emergere tutti i requisiti funzionali e
tutte le caratteristiche dei requisiti non funzionali. Talvolta si sfrutta la possibilità di confrontarsi mediante
attività di brainstorming (ognuno esprime le funzionalità che si vorrebbero avere e vengono segnate su un
post – it), ma solitamente si utilizza una raccolta collaborativa dei requisiti, basata su riunioni strutturate con
gli stakeholders con l’uso di un’agenda e la presenza di un moderatore, per identificare il problema, accordarsi
sui diversi possibili approcci e specificare un insieme preliminare di requisiti.
- Analisi e negoziazione: vengono valutati i requisiti rispetto a completezza, conflitti (tra viste diverse dei vari
stakeholders), compatibilità. Questa fase si articola, a sua volta, in:
1. Checklist: si verifica che i requisiti abbiano le caratteristiche desiderate;
2. Requirements prioritization: si conferiscono le priorità ai requisiti;
3. Interaction matrix: si controlla l’interazione tra i diversi requisiti, per evitare inconsistenze;
4. Risk Analysis: analisi dei rischi.
Successivamente, i requisiti vengono elaborati e si procede alla negoziazione con gli stakeholders.
- Documentazione: si produce un documento tecnico che descriva le funzioni, prestazioni e vincoli del sistema.
Si possono distinguere diversi documenti (testuale, insieme di modelli, insieme di scenari utente, un prototipo)
anche a seconda degli stili: approccio informale (le specifiche sono espresse in linguaggio naturale,
strutturato), approccio formale (automa a stati finiti), approccio semi – formale (UML). La specifica dei
requisiti è un documento strutturato che contiene descrizioni dettagliate dei servizi del sistema (specifica
funzionale), può servire come contratto tra committente e sviluppatore. La specifica del software è una
descrizione astratta del software che è la base del progetto e aggiunge ulteriori dettagli alla specifica dei
requisiti. Il documento di specifica dei requisiti software (SRS) costituisce il punto di convergenza di tre diversi
punti di vista: cliente, utente, sviluppatore. È un punto di riferimento per la validazione del prodotto finale: un
documento SRS di qualità è il pre -requisito per un software di alta qualità e riduce i costi di sviluppo.
- Validazione: l’SRS viene passato al progettista, che elabora una soluzione. Per controllare che i requisiti
specificati vengano elaborati realmente, i requisiti devono essere allocati alle parti della soluzione (allocazione
dei requisiti al sistema): ad esempio, se sto utilizzando il principio di modularità, bisogna specificare quali
requisiti vengono elaborati in ogni modulo. Successivamente si passa alla progettazione di basso livello,
scomponendo ogni componente fino ad arrivare a componenti sufficientemente piccoli da dominare la
complessità. Durante la scomposizione, è necessario continuare ad allocare ciascun requisito al sotto-
componente particolare. Si passa, poi, alla codifica, la stesura del programma: ciascun modulo software
implementa qualche componente, e grazie all’allocazione dei requisiti, di ciascun modulo software è possibile
conoscere i requisiti che esso deve realizzare. Quindi, la specifica dei requisiti viaggia lungo tutto il processo,
dall’analisi fino alla fase di test: tracciabilità dei requisiti.
- Gestione: tipicamente i requisiti sono soggetti a cambiamenti, che devono essere gestiti (requirements
management), non modificando semplicemente il documento SRS, ma anche considerando un impact
analysis (analisi d’impatto), ovvero la stima degli impatti del cambiamento sui requisiti e sulle attività, possibile
grazie alla tracciabilità. L’impatto di un cambiamento si può limitare con una buona modularità: se un utente
cambia un requisito ed è necessario modificare un’intera parte del progetto significa che la progettazione non
è stata realizzata correttamente.

L’ingegneria dei requisiti non si limita soltanto alla prima fase del processo di realizzazione di un sistema software, ma
al contrario interessa tutte le fasi: ciascun requisito va seguito almeno fino alla fase di realizzazione (collaudo e test),
motivo per cui il software test plan, documento che specifica quali saranno i test da effettuare, va steso subito dopo
l’SRS.

9. Diagrammi di interazione:
Mediante i diagrammi delle classi modelliamo le entità individuate nel dominio del problema (in fase di analisi) e nel
dominio della soluzione (in fase di progettazione). Nel diagramma dei casi d’uso modelliamo le funzionalità.
Verifichiamo infine la coerenza tra di essi mediante la navigabilità: navighiamo nel diagramma delle classi per vedere
se siamo in grado di compiere qualunque caso d’uso. Nella modellazione del diagramma delle classi aggiungiamo una
classe boundary per ciascun attore primario, che ne rappresenta l’interfaccia con il sistema, ed una classe gestore
control. Aggiungendo queste classi aggiungiamo nel diagramma delle classi le funzionalità: in ciascuna classe boundary
vanno tante operazioni quante le comunicazioni che l’attore svolge con i casi d’uso. Diagramma delle classi e
diagramma dei casi d’uso offrono una vista puramente statica del nostro modello (l’unica eccezione dinamica è la
navigabilità).

Con i diagrammi di interazione aggiungiamo la dinamica comportamentale agli oggetti individuati, istanze dei
classificatori. Modelliamo gli scenari dei casi d’uso tramite l’interazione tre le entità individuate.

In UML gli oggetti son istanze delle classi così come le classi sono istanze dei classificatori. Parliamo di modello ma
anche di metamodello. Il modello OO descrive la realtà composta da oggetti. In un DBMS i dati erano le tuple, queste
venivano organizzate in tabelle (l’intestazione della tabella, ad esempio nome, cognome e matricola, costituisce il
modello, lo schema). Quando creavamo il modello utilizzavamo il comando DDL create table, in questo modo il DBMS
definiva lo schema. Esso deve anche memorizzare gli schemi che crea e come sono costituiti. Il DBMS scrive allora lo
schema nel catalogo, che ne rappresenta il metamodello. Nel catalogo troviamo una tabella con tutti i nomi delle
tabelle della nostra base di dati e una tabella che fa corrispondere a ciascuna delle nostre tabelle tutti gli attributi
costituenti. Le tabelle sono quindi le istanze del metamodello.
Ragionando allo stesso modo il metamodello UML indica gli elementi che possono essere presenti nel modello UML.
Nel modello delle classi UML sono presenti classi e associazioni tra di esse, il metamodello è allora formato da Class e
Association, indicati in rettangoli: descriviamo il metamodello UML tramite lo stesso UML. La classe è un’istanza di
Class così come una specializzazione, ad esempio, è
un’istanza di Association. Il livello M0 è il modello degli
oggetti, M1 è il modello delle classi e M2 è il metamodello.
M0 è la realtà ed M1 è la sua rappresentazione, è il modello

della realtà. Il metamodello è la rappresentazione del


modello.

I diagrammi di interazione sono composti da


messaggi (ciascuno con due estremità, mittente
e destinatario) e linee di vita (life line) degli
oggetti. L’interazione è un comportamento e il
caso d’uso è un classificatore che ha un
comportamento. L’interazione mostra uno
scambio osservabile di informazioni. La
simulazione dei casi d’uso con messaggi può
rivelare l’esigenza di ulteriori classi non già
specificate nel diagramma delle classi, in questo
caso allora torneremo a modificarlo.

Tra i diagrammi di interazione abbiamo:

- Diagrammi di sequenza
- Diagrammi di comunicazione (precedentemente di collaborazione)
- Diagrammi di riepilogo di interazione
- Diagrammi di temporizzazione.
Diagrammi di sequenza:
Le life lines rappresentano i partecipanti all’interazione. Le linee evolvono verso il basso, il tempo scorre verso il basso.
Ogni oggetto è coinvolto in un’interazione. Le frecce rappresentano invece i messaggi che vengono scambiati, per
ciascuno di essi dobbiamo indicare il punto di partenza e di arrivo. In testa a ciascuna linea di vita troviamo un oggetto,
che non battezzeremo nel caso in cui l’interazione coinvolga tutti o più oggetti di una classe. In ciascuno caso d’uso è
coinvolto anche l’attore primario, che essendo colui che innesta il caso d’uso stesso, andrà sulla sinistra. Gli attori
secondari andranno invece più a destra.
La dinamica di un programma a oggetti si basa su una sequenza di messaggi che vengono scambiati. Notiamo che
quando arriva un messaggio vengono posti dei rettangoli sulle life lines, questi sono detti attivation box e delineano
la fase in cui l’oggetto è effettivamente attivo. Gli oggetti sono entità passive, fanno qualcosa solo nel momento in cui
gli viene effettivamente fatta una richiesta. Fino a quel momento l’oggetto è inattivo, quando arriva la richiesta (ovvero
il messaggio) l’oggetto si attiva, ovvero sta elaborando, quando avrà compiuto la sua operazione invierà un messaggio
di ritorno. Dal punto di vista implementativo allora il messaggio è l’invocazione di un metodo, il contenuto sono i
parametri. Ogni oggetto dunque interagisce con altri oggetti, quando questi esistono vi manda messaggi, quando non
esistono li crea. Possiamo anche rappresentare chi distrugge l’oggetto.

Quando l’oggetto riceve il messaggio può delegare parte


di ciò che deve fare magari perché non ha tutte le
informazioni per svolgere ciò che gli è stato richiesto (ciò
è alla base anche della modularità). Ciascun oggetto può
rispondere solo a richieste riguardo allo stato che esso
incapsula. Secondo il principio della coesione infatti le
operazioni dell’oggetto sono le operazioni che lavorano
sullo stato dell’oggetto stesso.
Un oggetto può anche auto-delegarsi, ovvero inviare un
messaggio a se stesso, in questo caso si genererà
un’attivazione innestata.

Il tempo descritto non è un tempo assoluto ma semplicemente un tempo relativo. Possiamo però indicare alcuni punti
con delle etichette ed esprimere tra di essi dei vincoli temporali.

L’intento dei diagrammi di interazione è modellare gli scenari dei casi d’uso ed in questi talvolta è possibile trovare
scritture del genere “se accade che…allora…”. Nel diagramma di sequenza è prevista la modellazione dei “se”,
attraverso dei rettangoli che prendono il nome di frammenti
combinati. In questo caso il frammento combinato esprime
un Option (opt). il servente (server) dovrà eseguire il
messaggio che riceve solo nel caso in cui la condizione di
guardia, specificata tra parentesi quadre, risulti essere vera.
L’Opt è una selezione a due vie, possiamo avere anche
selezione a più vie dette Alternatives (Alt). L’Alt è un
frammento combinato a più sezioni, separate da linee
tratteggiate. Ciascuna condizione avrà una guardia e si
eseguirà l’azione corrispondente alla condizione verificata,
altrimenti l’else.

Possiamo modellare anche l’iterazione, che in UML si indica


con la parola chiave loop. Possiamo indicare per ciascun loop il numero
minimo, il numero massimo di iterazione e una guardia. Possiamo anche
utilizzare il break per uscire dal ciclo, per questo motivo il rettangolo del break
inizia leggermente più a sinistra rispetto al frammento combinato in cui è
contenuto.

Nei diagrammi di sequenza possiamo esprimere i costrutti di sequenza,


selezione e iterazione. Secondo il teorema di Bohm-Jacopini abbiamo una
potenza espressiva tale da poter scrivere in questo stesso linguaggio anche gli algoritmi. Abbiamo regole ben precise
per passare da questi costrutti ad altri linguaggi di programmazione, possiamo allora eseguire il passaggio in maniera
automatizzata, se arriviamo ad una descrizione di dettaglio. Facendo ciò affermiamo di usare UML in fase di
implementazione, il codice verrà generato automaticamente (esattamente nello stesso modo in cui il codice macchina
viene automaticamente generato dal compilatore a partire dal codice oggetto). operando in questo modo effettuiamo
lo sviluppo guidato dai modelli, MDD (Model Driven Development). Ragionare per modelli è ciò che fanno gli ingegneri
ma ovviamente rispetto alla programmazione la modellazione richiede skills diverse, più elevate.

Diagrammi di comunicazione:
L’intento è analogo a quello dei diagrammi di sequenza ma varia la disposizione spaziale. Non vi sono più le life lines
per cui non è evidente il modo in cui scorre il tempo. L’ordinamento relativo degli eventi si evince dai numeri di
sequenza. Allo stesso modo anche qui possiamo avere
iterazione e selezione. L’iterazione si specifica mediante
il corrispondente specificatore * e una clausola
opzionale. Il ciclo for va tra parentesi quadre. Se
l’iterazione avviene in parallelo si usa //. La
ramificazione (selezione) è invece modellata
aggiungendo condizioni di guardia ai messaggi, che
verranno inviati solo se la condizione di guardia risulterà
vera. Le ramificazioni rendono difficile la lettura, sono
da limitare a casi semplici.

Possiamo modellare gli scenari sia con i diagrammi di comunicazione sia con i diagrammi di sequenza, la scelta
dipenderà dagli aspetti su cui ci vogliamo focalizzare:

- I diagrammi di sequenza enfatizzano la sequenza temporale relativa degli eventi. I diagrammi di


comunicazione rendono invece più esplicite le considerazioni strutturali degli scambi di messaggi. Tramite
questi ultimi risulta molto più semplice capire chi si rivolge a chi. I diagrammi di comunicazione sono quindi
più indicati per evidenziare tutti gli effetti su un dato oggetto.
- I diagrammi di sequenza rendono esplicito l’ordinamento temporale delle interazioni. Sono la scelta naturale
quando si dettaglia un modello dell’interazione a partire dai casi d’uso. I diagrammi di comunicazione sono
meno dettagliati e preferibili quando il punto di partenza è il diagramma delle classi, in particolare sono utili
per validarli.

10. Diagrammi di stato:


Un sistema in cui dato un ingresso l’uscita corrispondente è sempre la stessa è una macchina combinatoria. L’uscita
dipende solo dall’ingresso.

Un concetto fondamentale per i sistemi e le macchine è il concetto di stato. Lo stato è una condizione di
funzionamento. La macchina allora, ricevuto un ingresso, risponderà con una determinata uscita anche a seconda
dello stato in cui si trova. Se immaginiamo un ascensore se premiamo un tasto questo salirà o scenderà a seconda del
piano in cui si trova o meglio a seconda del tasto precedentemente premuto e di quello ancora prima e così via. Nel
far funzionare l’ascensore però non abbiamo bisogno di conoscere l’esatta sequenza degli ingressi precedenti, ci basta
conoscere il piano in cui esso si trova. Il piano rappresenta lo stato: astrae la storia passata. Possiamo allora affermare
che lo stato è una condizione di funzionamento di un sistema che astrae la storia passata.

Distinguiamo allora altri due tipi di macchine: gli automi di Mealy (in cui l’uscita dipende dalla coppia stato-ingresso)
e gli automi di Moore (in cui l’uscita dipende solo dallo stato). È stato in realtà dimostrato che questi sono equivalenti
ed è sempre possibile trasformare un automa di Mealy in uno di Moore che produca lo stesso risultato terminale.
Entrambi sono automi a stati finiti (ASF) e fanno parte degli automi deterministici. Abbiamo poi automi non
deterministici e automi probabilistici (possiamo conoscere l’uscita attesa in termini di probabilità).

Gli automi a stati finiti sono definiti dalla sestupla:

- Insieme finito e non vuoto degli stati


- Insieme degli ingressi
- Insieme delle uscite
- Funzione di trasformazione (passaggio da uno stato all’altro)
- Funzione di uscita
- Stato iniziale

Possiamo rappresentarli o tramite un grafo orientato (diagramma degli stati) o in forma tabellare.

Gli ASF sono un formalismo, un modello formale per descrivere gli stati in cui il sistema può trovarsi durante la sua
evoluzione e le trasformazioni che fanno evolvere il sistema tra questi stati. Sono un formalismo meno potente della
macchina di Turing in quanto tramite questa possiamo ridefinire il concetto di algoritmo stesso: un algoritmo è tutto
ciò che può essere eseguito attraverso una macchina di Turing.

Parliamo di modello operazionale in quanto il sistema viene descritto in termine di evoluzione tra gli stati.

Gli stati vengono modellati tramite dei rettangoli smussati, le transizioni tramite frecce. Possiamo inoltre etichettare
le transizioni con gli eventi che le innestano, l’azione che la macchina può fare e la guardia eventuale.

I diagrammi di stato vengono solitamente utilizzate per modellare il ciclo di vita di un’unica entità reattiva, contesto
del diagramma. Quest’entità risponde a eventi esterni (che nascono al di fuori del suo contesto), ha un ciclo di vita che
possiamo modellare come successione di stati, transizione ed eventi e ha un comportamento corrente che dipende
dai comportamenti precedenti. Solitamente utilizziamo le macchine a stati (e quindi i diagrammi degli stati) per
modellare il comportamento dinamico delle classi. Descriviamo come modello operazionale gli stati di un oggetto. Se
consideriamo un oggetto allora gli ingressi saranno le operazioni invocate su di lui, l’oggetto può infatti modificare il
suo stato tramite un’operazione che provocherà la transizione da uno stato all’altro. Le sollecitazioni, gli eventi,
possono essere anche di altro tipo, sono possibili:

- Eventi di chiamata
- Eventi di segnale
- Eventi di variazione
- Eventi temporali

Gli eventi di chiamata sono le invocazioni di operazioni. Essi possono essere eventi esterni, che ci faranno transitare
in un altro stato, o eventi interni, che ci faranno rimanere nello stesso stato. Possiamo infatti anche parlare di
transizioni interne, transizioni che modellano un evento che produce azioni senza alterare lo stato dell’oggetto.

Oltre a messaggi gli oggetti inviano segnali: pacchetti di informazione (strutture dati) inviati in modo asincrono da
un oggetto a un altro. Due operazioni sincrone sono due operazioni che avvengono nello stesso tempo, la chiamata a
un sottoprogramma è un esempio di operazione sincrona in quanto chiamante e chiamato sono attivi
contemporaneamente, il chiamante si blocca e attende che la funzione termini per riprendere de dove si è fermato. Il
programma interrompe solo la sua elaborazione, rimane sospeso ma è attivo. Un esempio di operazione asincrona è
l’invio di un sms, le due parti non devono infatti necessariamente essere attive in contemporanea. Nel diagramma di
sequenza rappresentiamo con le frecce operazioni sincrone e asincrone. Possiamo realizzare un’operazione sincrona
mediante due asincrone (proprio nel diagramma di sequenza infatti quando inviamo un messaggio abbiamo una
freccia tratteggiata che ritorna e che rappresenta proprio il messaggio di ritorno a fine operazione). La comunicazione
asincrona avviene con molteplicità 1 a 1, 1 a molti o molti a molti, per cui nel diagramma degli stati modelliamo la
possibilità di inviare i segnali a uno o più destinatari. Un’altra caratteristica della comunicazione è la simmetria: chi
conosce chi. La chiamata è un’operazione simmetrica, ognuno sa con chi sta parlando; guardare la televisione è
un’operazione asimmetrica, chi guarda sa cosa sta guardando, chi è guardato non sa chi stia guardando. La lettura del
giornale è un’operazione asimmetrica ma se introduciamo un abbonamento diventa simmetrica: i produttori del
giornale conoscono chi lo legge perché conoscono chi è abbonato. La simmetria è una caratteristica che riguarda il
flusso di informazione, come la molteplicità. Un’altra dimensione è il modo in cui avviene la consegna, se direttamente
o indirettamente. Nel caso dell’sms la consegna è diretta, nel caso dell’email c’è invece un deposito intermedio,
un’applicazione a cui il destinatario deve decidere volontariamente di accedere. Le modalità sono allora push e pull.
Nella modalità pull il destinatario va a prendere il giornale, ad esempio, che è stato lasciato al giornalaio, decide il
destinatario quando. Nella modalità push il mittente invece spinge, facendo arrivare il pacchetto direttamente al
destinatario (si parla, ad esempio, di notifiche push).

Quando una macchina invia un segnale (il simbolo è un rettangolo a freccia) da qualche parte vi sarà un’altra macchina
che lo riceverà. La ricezione di un segnale è modellata come un evento di segnale.

Gli eventi di variazione sono attivati solo da cambiamenti. Sono attivati quando la condizione booleana specificata
nella clausola quando, diventa vera dopo essere stata falsa. Perché venga nuovamente attivato l’evento la condizione
deve prima tornare a essere falsa e, quindi, tornare a essere nuovamente vera.

Gli eventi temporali sono specificati dalle parole chiave when e after. Specificano rispettivamente un determinato
momento nel tempo in cui verrà attivato l’evento o un intervallo di tempo al cui scadere verrà attivato l’evento.

All’interno di uno stato vi possono essere zero o più azioni o attività.


La differenza tra azione e attività è che la prima è istantanea, la
seconda richiede una quantità di tempo finita e può essere
interrotta. Ogni azione è associata ad una transizione interna
attivata da un evento. Esistono poi due azioni speciali, di ingresso e
d’uscita, relative ai due eventi speciali di ingresso e uscita. L’evento
ingresso avviene istantaneamente e automaticamente all’ingresso
dello stato, è la prima cosa che succede quando vi si entra e provoca
l’esecuzione dell’azione associata. L’evento uscita è l’ultimissima
cosa che succede istantaneamente e automaticamente all’uscita
dallo stato. L’esecuzione di un’attività richiede un intervallo di tempo finito non nullo. La parola chiave è esegui.

Gli stati possono essere composti, A può essere un macrostato composto dai sottostati A1 e A2. Possiamo decomporre
uno stato utilizzando una AND-relationship in sottostati concorrenti o una OR-relationship in sottostati mutuamente
esclusivi. Modellare sottostati concorrenti ci serve perché
la realtà stessa è concorrente, dunque dobbiamo poter fare
dei programmi con dei threads (flussi di esecuzione)
concorrenti. Ad esempio, se pensiamo a un lettore video è
importante che audio e video arrivino allo stesso tempo,
anche il minimo sfasamento turba lo spettatore, è
un’applicazione isocrona.

L’UML2 ci consente di utilizzare i diagrammi degli stati sia


per descrivere le macchine a stati di comportamento, che rappresentano il comportamento interno di un oggetto, sia
le macchine a stati del protocollo, con cui definiamo il protocollo di funzionamento dell’oggetto.

Consideriamo l’interfaccia implementata di una classe: oltre all’implementazione (che manca in quanto parliamo
dell’interfaccia del modulo e non del suo corpo) non si capisce chiaramente che moduli vengano utilizzati, qualcosa si
evince dalle direttive di inclusione ma il nostro modulo potrebbe includere moduli che allora volta includono altri
moduli. Sappiamo cosa il nostro modulo offre ma non cosa richiede. Nell’interfaccia sono note le operazioni che il
modulo può effettuare, tuttavia non si evince un eventuale ordinamento. Se consideriamo l’interfaccia di una classe
file le operazioni consentite possono essere open(), read(), write() e close(). Per specificare l’ordine possiamo utilizzare
le espressioni regolari oppure possiamo modellarlo tramite un automa a stati finiti. La corretta sequenza di utilizzo di
un file è quella che ci consenti di passare dallo stato iniziale chiuso allo stato aperto tramite l’operazione open()
(operazioni interne),con write() e read() rimaniamo all’interno dello stato aperto e ritransitiamo in chiuso tramite
l’operazione close(). Possiamo interpretare questo diagramma anche come un diagramma degli stati interni
dell’oggetto che completa l’interfaccia con la specifica del corretto ordine di invocazione delle operazioni consentite
sull’oggetto. Se non lo specificassimo sarebbe un comportamento corretto anche scrivere su un file non aperto, e via
dicendo. Definiamo allora le regole di utilizzo dell’oggetto: protocollo di utilizzo dell’oggetto. Utilizziamo le macchine
del protocollo (per cui troviamo {protocol} posto vicino al nome del diagramma) per rendere noto come va usato
l’oggetto ed è necessario perché siamo in fase di specifica, dobbiamo specificare tutto ciò che altrimenti rimarrebbe
vago o erroneamente implicito. È utile inoltre per produrre i casi di test. La prima prova allora sarà la sequenza corretta
minima (nel caso del file open() e close()), poi si testano le sequenze corrette via via più complicate (open() read()
close()…). La robustezza viene testata inverdendo l’ordine del protocollo. Possiamo quindi concludere che le macchine
a stati del protocollo sono definite a favore di chi le deve utilizzare e di chi deve definire i casi di test.

Le transizioni delle macchine del protocollo non contengono azioni ma solo precondizioni e postcondizioni delle
condizioni di guardia.

11. Diagrammi di attività:


I diagrammi di attività sono “diagrammi di flusso OO”. Ci consentono di modellare un processo come un insieme di
attività e transizione tra queste. Servono a descrivere work flows (flussi di lavoro), come si svolgono le attività. In
particolare, sono comodi per descrivere i processi di business.

Un’attività è una rete di noti connessi da archi. I nodi azione rappresentano unità di lavoro atomiche, non
interrompibili e istantanee. Possiamo poi avere nodi di decisione, di fusione, di biforcazione e di ricongiunzione (join).
Una decisione specifica precorsi alternativi basati su condizioni di guardia. In UML si rappresenta con un rombo,
utilizzato anche per la fusione laddove i percorsi alternativi si riunificano. I vari percorsi in uscita sono protetti da una
guardia. La parola chiave altrimenti indica il percorso preso nel caso in cui nessuna condizione di guardia si verifichi.
Bisogna fare attenzione a formulare le condizioni di guardia in modo che risultino mutuamente esclusive: in caso
contrario il diagramma risulterebbe indeterminato.

Attraverso la biforcazione possiamo modellare flussi concorrenti (attività parallele). In


UML la rappresentiamo come una linea orizzontale doppia da cui partono più frecce. Le
frecce rappresentano i flussi e i nodi le attività. Il nodo di ricongiunzione (linea
orizzontale doppia in cui incidono più frecce) è il punto in cui si risincronizzano i flussi
concorrenti, non è detto infatti che due attività che nascono in concorrenza abbiano la
stessa durata. Tramite il nodo di ricongiunzione indichiamo che possiamo proseguire
solo nel momento in cui tutte le attività concorrenti sono terminate: la condizione di
terminazione è un AND.

Possiamo esprimere anche in questo diagramma oltre alla sequenza e alle attività
concorrenti l’iterazione e la selezione (nodo di decisione), possiamo allora modellare
anche gli algoritmi. Possiamo altrimenti utilizzare il diagramma delle attività per
descrivere processi business o per capire quali processi vogliamo automatizzare e quali no. Modellando lo scenario di
un caso d’uso mediante il diagramma di sequenza ci focalizziamo sugli attori, gli oggetti coinvolti e gli scambi di
messaggi che avvengono tra questi, tralasciando l’algoritmo. Se vogliamo approfondire anche questo utilizziamo un
diagramma di attività.

Per facilitare la lettura possiamo partizionare le attività in corsie (swimlanes), in questo modo risulterà più evidente
chi faccia cosa. In realtà le corsie non hanno una semantica intrinseca ma viene decisa dal modellatore, possono
indicare unità organizzative così come componenti fisici del sistema, ruoli ma anche una distribuzione temporale.
Tramite le corsie possiamo rappresentare quali sono le classi responsabili per le attività, l’oggetto responsabile per
l’esecuzione di un’azione può essere rappresentato visualizzando la sua corsia del tempo di vita e posizionando le
azioni di cui è responsabile lungo questa.

Possiamo infine avere nodi oggetti, istanze delle nostre entità. Le frecce che entrano ed escono da questi sono dette
flussi di oggetti. Il diagramma delle attività ci permette di specificare anche l’output dell’attività, che comporta la
creazione di un oggetto o la modifica del suo stato. Possiamo infatti specificare lo stato in cui si deve trovare l’oggetto
al termine di un’azione, mantenendo coerenza con il diagramma degli stati: lo stato dev’essere uno degli stati previsti
e modellato nel diagramma degli stati. Tramite il diagramma delle attività siamo quindi anche in grado di descrivere
quale azione può modificare lo stato di un oggetto.

12. Diagrammi di deployment:


Un diagramma di allocazione (deployment)
descrive le scelte di allocazione dei componenti
software sviluppati (siano essi sottosistemi,
driver) sulle unità fisiche di elaborazione. Esso
può essere utilizzato anche per descrivere
l’architettura fisica e la distribuzione del sistema
(hardware e software). Nella pratica non si
modella un deployment esaustivo, ma
solamente gli elementi architetturalmente significativi: ad esempio, un sistema di tipo cliente-servente sarà
fisicamente realizzato come una serie di moduli software riguardanti la parte server e le parti client, tali moduli
dovranno essere chiaramente dislocati su unità d’elaborazione diverse.
Un nodo rappresenta un tipo di risorsa computazionale (ad esempio smartphone) su cui i manufatti (artifact) possono
essere dislocati per l’esecuzione. Un nodo è un classificatore strutturato e può avere
annidati altri nodi ed elementi. Un’associazione tra nodi rappresenta un canale di
comunicazione attraverso cui possono passare le informazioni.

Un’istanza di nodo rappresenta un hardware specifico ed identificabile. Un’istanza di


manufatto è una specifica istanza di un tipo software. Il deploymente è la messa in
esercizio, è il processo di assegnazione di artifact a nodi o di istanze di artifact ad istanze
di nodi.
Il linguaggio UML mette a disposizione due stereotipi per i nodi:
• <<device>> se il nodo rappresenta una periferica fisica, quale un PC o un server;
• <<execution environment>> se il nodo rappresenta un tipo di ambiente software di esecuzione, ad esempio
una Java Virtual Machine.

Il diagramma di deployment fa corrispondere l’architettura software creata nella progettazione a un’architettura del
sistema fisico che lo esegue. Esistono due forme:
- Forma di descrittore: contiene nodi, relazioni tra nodi e manufatti. Possono essere prodotti in progettazione
quando occorre decidere l’architettura hardware del sistema.
- Forma di istanza: contiene istanze di nodo, relazioni tra istanze di nodo e istanze di manufatto. È in forma di
istanza se il diagramma mi dice esplicitamente quale pc è. Ad esempio può servire quando si deve specificare
che un determinato software deve essere disponibile solo sul pc dell’amministratore e quindi di un utente
abilitato, pertanto a chi lo crea serve per fare dei controlli, mentre al compratore serve per capire dove
installarlo.
È possibile, tramite la forma di istanza, mostrare la configurazione del sistema a runtime (i componenti
software, processi ed oggetti implicati nell’esecuzione), di cosa consiste fisicamente il software che posso
mandare in esecuzione, dal punto di vista concettuale è la manifestazione concreta del software. Se invece
sono eseguibili, posso dire che l’artefatto che manifesta il mio programma è l’eseguibile, perciò do
informazioni a chi lo deve installare, che sia lo stesso cliente o l’installatore.
13. Progettazione:
La progettazione è l’attività ponte tra la specifica dei requisiti e l’implementazione. Lo scopo è definire l’architettura
del sistema software, trovare la soluzione al problema specificato in fase di analisi. La progettazione si occupa del
passaggio dall’identificazione di cosa si debba produrre all’identificazione di come si possa realizzare. Progettare
significa individuare una possibile soluzione al problema che ne realizzi i requisiti funzionali nel rispetto devi vincoli
imposti dai requisiti non funzionali (prestazionali, vincoli di affidabilità, disponibilità, safety e dunque vincoli sul
processo, vincoli sul budget). Per fare ciò il progettista deve dominare la complessità, scomponendo il sistema in
moduli e affidando a ciascuno di essi le proprie responsabilità. Il vantaggio di tale scomposizione è che le singole parti
avranno una complessità inferiore rispetto a quella globale, di conseguenza sarà più facile concentrarsi su di esse. La
prima fase di progettazione dunque definisce l’architettura software che mette in luce le parti del sistema e le
responsabilità assegnate a ciascuna di esse, ad esempio per quanto riguarda i requisiti funzionali. Si effettua
l’allocazione dei requisiti alle componenti del sistema, indicando esplicitamente quale, o quali, moduli sono i
responsabili di una determinata funzionalità.

Distinguiamo una progettazione ad alto livello ed una a basso livello. La progettazione HLD (High Level Design)
prescinde dalle scelte tecnologiche, dunque un progetto di questo livello potrà essere utilizzato per uno sviluppo su
più tecnologie, essendone indipendente. Successivamente sarà necessario prendere delle decisioni anche in merito
alle particolari tecnologie da utilizzare per cui si passerà alla progettazione LLD (Low Level Design), esempi di queste
scelte sono il particolare linguaggio di programmazione o l’utilizzo di determinate librerie.

In generale il progettista affronta una serie di problemi di design che prevedono più soluzioni alternative (design
options). Il progettista si muove in uno spazio di possibili progetti (design space), valuta le alternative e prende scelte
di progetto (design decisions) secondo determinati criteri. Le scelte possibili non sono uniche ma è compito del
progettista selezionare l’opzione che ritiene più valida, effettuando una scelta vincolante per le attività successive.
Il progettista, ad esempio, valuta se produrre un’applicazione monolitica o client/server, in particolare nel secondo
caso deciderà se thin client (più leggera dal punto di vista software) o fat client (più consistente e dunque pesante).

Esempio DropBox: possiamo utilizzare dropbox scaricando l’applicazione o accedendo via browser. Nel primo
caso dovremo installare un software, ovvero l’applicazione, nel secondo caso no, presumendo di aver già
installato il browser. Dal lato server nel caso di un sito web avremo una pagina html, un protocollo http. Se
invece progettiamo un’applicazione dobbiamo prendere delle decisioni affinché il cliente possa usufruirne.
Dobbiamo ad esempio decidere su quali dispositivi ne sarà possibile l’utilizzo. Attualmente DropBox può
funzionare su pc, tablet, smartphone ecc. se prevediamo invece che sarà possibile utilizzarlo su un solo
terminale sarà più thin lato client. Prendendo la decisione di accedervi solo tramite web allora non dovremo
incaricarci di sviluppare il lato client, sarà direttamente il browser.

Esempio bancomat: immaginiamo di lavorare per una banca. Ci viene commissionato un software per gli
sportelli bancomat. Un altro problema da porsi è relativo all’installazione. Non possiamo fare installare il
software manualmente da un tecnico se il progetto consta di 10.000 sportelli, dovremo allora prevedere che
sia possibile installarlo da remoto oppure eliminare il client e fare tutto lato server. Se decidiamo di installarlo
per via remota dovremo allora installare altro software con cui sia possibile l’installazione remota. Il client
diventa più fat e costoso. Le decisioni allora dipendono anche da vincoli di budget.

Un principio fondamentale in fase di progettazione è il design for change (abbiamo studiato il principio dell’IS di
anticipazione dei cambiamenti). Durante il ciclo di vita del prodotto saranno inevitabili cambiamenti, magari
tecnologici, che sarà necessario prevedere per ridurre i costi di adattamento. Si stima che il 60% dei costi del software
debba essere attribuito ai costi di manutenzione. Se infatti si necessita di una manutenzione (adattiva o perfettiva in
primo luogo) o di una qualunque modifica se si dovesse rifare tutto da capo oltre a maggiori costi diventerebbero
necessari tempi più lunghi, che farebbero perdere terreno rispetto alla concorrenza. Prevedere il cambiamento è allora
necessario e per esserne in grado bisogna essere aggiornati sulle evoluzioni della tecnologia e portarle in campo come
requisiti anche nei processi in corso di sviluppo.
Secondo il principio di modularità dobbiamo scomporre il sistema in
moduli con alta coesione e basso accoppiamento. Scomponendo
molto si hanno molti moduli, sempre più piccoli, la cui realizzazione
costerà poco. Aumenterà invece il costo dell’interazione tra di essi. Il
costo complessivo dunque prima scende e poi risale, formando una
curva: dobbiamo cercare di accostarci in un punto basso. Moduli
piccoli costano poco ma portano all’aumentare dei costi di
interfacciamento.

Sono necessari criteri di coesione, un criterio è la coesione funzionale: raggruppiamo nello stesso modulo moduli che
fanno cose simili (un esempio sono la libreria matematica o la libreria grafica). Possiamo poi avere una coesione di
livello, molte strutture software sono basate su un’architettura a strati, ad esempio i protocolli. Il livello più basso è il
livello fisico che in questo caso si occupa dell’accesso al mezzo di comunicazione, se consideriamo un computer questo
può essere il cavo ethernet. Successivamente c’è il livello di collegamento e poi quello di rete, che si occupa
dell’instradamento dei messaggi sulla rete (routing). Più in alto abbiamo il livello di trasporto che si occupa delle
sessioni (ad esempio sessioni di lavoro in cui viaggino pacchetti di dati trasmessi, nella chiamata la sessione è la
telefonoata), il periodo in cui si possono scambiare dati su un canale già aperto, dipende in generale dai protocolli di
trasporto. Sopra ancora abbiamo il livello applicazione. Nell’esempio visto precedentemente il browser è
un’applicazione che utilizza i protocolli http://. Le regole di un’architettura a livello prescrivono in primo luogo di porre
in ciascun strato cose che si trovano allo stesso livello di astrazione, in secondo luogo che ciascun livello può accedere
unicamente al livello immediatamente sottostante.
Altri criteri sono il criterio temporale: mettere in moduli o librerie i moduli che vengono usati nella stessa fase, come
i moduli che fanno inizializzazione o i moduli che vengono usati in fase di bootstrap. Molto spesso l’elaborazione è
sequenziale (facciamo prima un’operazione, poi un’altra e così via) allora potremmo mettere insieme tutti i moduli
elementari che sono coinvolti in ciascuna fase.
Possiamo mettere insieme moduli che svolgono svariate utilities, servizi diversi, extra che possono essere utili in
momenti diversi dell’esecuzione.

È fondamentale rispettare il principio di separazione degli interessi. Una tipica architettura software è quella in cui
distinguiamo presentation, business e dataaccess logic. Tener presente questo paradigma fa in modo che si possa
accedere al servizio tramite logiche di presentazione diverse, nel caso prima visto possiamo accedere a dropbox sia
tramite app sia tramite browser, le due modalità non devono richiedere due applicazioni diverse lato server se
abbiamo rispettato questa distinzione correttamente.

Il progettista produce diversi artefatti. In linea generale individua l’architettura del software, i componenti e le
interfacce tra di essi. Può essere richiesto un documento di specifica proprio riguardo le interfacce di ciascun
componente. Poi bisogna progettare le interfacce uomo-macchina (HMI: Human Machine Interface), l’accesso ai dati
(la base di dati se usiamo un DBMS ma anche in che modo le applicazioni salvano i dati, se vi accedono), gli algoritmi
e eventualmente i protocolli, nel caso in cui non siano sufficienti i protocolli rete offerti o altre librerie.

14. Diagrammi dei packages:


L’UML ci mette a disposizione diagrammi importanti in fase di progettazione, tra cui il diagramma dei packages e il
diagramma dei componenti.

Un package è un meccanismo generalizzato per raggruppare logicamente elementi (compresi package) e diagrammi.
L’icona con cui rappresentiamo un package è la cartella. I packages forniscono uno spazio di nomi incapsulato, al cui
interno tutti i nomi degli elementi devono essere univoci. Ci consentono di creare un modello navigabile e ben
strutturato, mediante il raggruppamento di elementi dotati di forte correlazione semantica. Possiamo ad esempio
raggruppare le classi in packages e fornire un diagramma delle classi per ciascun package, così che sia più leggibile. Il
raggruppamento in package si effettua secondo criteri di coesione.

Un package oltre a contenere classi ed elementi può contenere altri package. Si parla
infatti di package annidati e possiamo rappresentarlo con due notazioni differenti, o
tramite un grafico ad albero o inserendo graficamente un package nell’altro.

Un package annidato vede tutti gli elementi pubblici


del package ospite, possono accedervi senza
utilizzare un nome qualificato. Un package ospite
non vede nessuno degli elementi dei suo package
annidati, a meno che non vi sia una relazione di
accesso.

È un meccanismo analogo al namespace e come questo è gerarchico, non ci possono


essere due elementi con lo stesso nome in uno stesso package ma potrebbero esserci in
un package che lo contenga.

Misuriamo la coesione interna di un package con la metrica CCRC (Class Category Relational Cohesion): il rapporto tra
numero di link (tra package) e il numero di classi contenute deve appartenere al range nominale 150% - 350%.

I package possono essere collegati in più modi. In generale tra gli elementi UML abbiamo tre tipi di dipendenze, che
modelliamo con una freccia tratteggiata:

- Dipendenza d’uso: il cliente utilizza alcuni servizi del fornitore per implementare il proprio comportamento.
Abbiamo visto una di queste dipendenze tra la classe boundary e la classe control in particolare la classe
boundary usava la classe control. La dipendenza use indica semplicemente che il cliente fa un qualche uso del
fornitore, in maniera molto generica. La dipendenza call è una dipendenza tra operazioni, qualche operazione
del cliente chiama qualche operazione del fornitore. La dipendenza send descrive invece situazioni di questo
tipo: consideriamo un oggetto di una classe O o e un oggetto di una classe C c. o.m(c); vuol dire che stiamo
mandando ad o un messaggio il cui contenuto è c, ovvero stiamo invocando su o un metodo il cui parametro
è c. Rispetto ad o stiamo facendo una call ma in realtà stiamo mandando c come contenuto di un messaggio,
è una send: il cliente è un’operazione che invia il fornitore ad un terzo. Ciò accade quando si delegano attività
a un terzo oggetto su un dato scelto, in questo esempio deleghiamo ad o attività su c, dunque è o, e non c, a
utilizzare i metodi su c.

- Dipendenza d’astrazione:
modellano dipendenze tra
elementi del modello UML che si
trovano a diverso livello
d’astrazione. I livelli di astrazione
sono il livello di analisi, di
progettazione ad alto livello, di
progettazione a basso livello e di
implementazione. Una relazione
possibile è quella di trace ed indica
che il fornitore e il cliente
rappresentano lo stesso concetto
ma appartengono a modelli
diversi. Nel modello di analisi
abbiamo una classe C che è
presente anche nel modello di
progettazione. Su quest’ultimo
allora indicheremo che la classe C
traccia la classe del modello di analisi: è esattamente la stessa ma ad un altro livello di astrazione. Se facciamo
partire una freccia da un elemento a un altro con lo stereotipo refine indichiamo che l’entità raffina l’entità
indicata, precedentemente definita. Con la dipendenza derive specifichiamo che un elemento può essere
calcolato/derivato da un altro in qualche modo. Se consideriamo l’esempio del ContoBancario abbiamo visto
che il Saldo si può tradurre come variabile membro (con le corrispondenti operazioni di get/set) ma anche
come attributi derivato, calcolandolo ogni volta come risultato delle varie transazioni.

- Dipendenza di permesso: concerne la capacità di un elemento del modello di accedere a un altro elemento.
Tipicamente sono dipendenze tra package.

o Import: lo spazio dei nomi del fornitore viene incorporato in quello del cliente, gli elementi del
cliente possono accedere a tutti gli elementi del fornitore. Immaginiamo che nella figura la relazione
sia di tipo import: A importa B che a sua volta importa C. Nel package C ci sono delle classi, quando B
lo importa è come se le classi fossero dento B stesso. A può accedere alle classi di B, a sua volta le
importa dunque anche le classi di C sono viste da A.
o Access: gli elementi del cliente possono accedere a tutti gli elementi pubblici del fornitore ma lo
spazio dei nomi non viene incorporato. Nella figura A accede a B che accede a C: B può utilizzare le
classi di C ma non le importa, A dunque non le vede ma le utilizza indirettamente quando richiede
servizi a B che delegherà qualcosa a C.

Se ora cambia C nella seconda modalità A non ne viene impattato, B potrebbe perché vi accede. Questo
risultato è molto importante nelle analisi di impatto per eventuali manutenzioni. Se la dipendenza è di access
e dobbiamo effettuare cambiamenti dobbiamo cercare di capire chi ne sarà impattato. Il cambiamento di C
provocherà qualche cambiamento in B ma non deve riguardare la sua interfaccia, usata da A, non deve
cambiare il servizio offerto: in questo modo A non ne è impattato. La dipendenza access non è transitiva. Nel
primo caso invece in A troviamo le classi di B e di C. La dipendenza import è transitiva, indica una dipendenza
implicita di A da C ed è un aspetto da tener presente in fase di manutenzione.

Un meccanismo simile all’import in C++ avveniva con la derivazione in modalità public, gli utilizzatori della derivata
potevano infatti utilizzare i metodi della classe base. Un meccanismo simile all’access è la direttiva di include.

Tra packages possiamo poi avere relazioni gen/spec.

15. Componenti e diagrammi dei componenti:


La progettazione di alto livello è basata molto sulla progettazione
dei componenti e sulla loro interconnessione, per realizzare ciò
accuratamente bisogna definire le interfacce: un’interfaccia è un
insieme di funzionalità battezzato da un nome. È simile ad una
classe ma priva di alcun attributo, è composta solo da un nome e
da operazioni. L’idea sottostante è quella di separare le
specifiche riguardo cosa faccia un insieme di funzionalità dalla
sua implementazione. In C++ un meccanismo simile è quello della
classe astratta: definiamo una classe al vertice della gerarchia
che fornisca un’interfaccia, detti le regole riguardo a funzioni che
le classi sottostanti dovranno necessariamente implementare
con la stessa signature, per non diventare anch’esse classi astratte. La differenza con la classe astratta, come abbiamo
sottolineato, è che quest’ultima può anche essere dotata di attributi.
L’interfaccia è un contratto tra chi la implementa e chi la utilizza, in realtà non si utilizza direttamente l’interfaccia
ma una classe che la implementi. Ipotizziamo di avere una biblioteca, la biblioteca presta dei libri. Presta() sarà un
metodo di Libro. Supponiamo che ora si vogliano prestare anche CD, in CD avremo un metodo analogo che si chiamerà
Presta(). In generale diciamo che la Biblioteca presta “oggetti prestabili”, ciascuno dei quali avrà un’operazione di
Prestito() e una complementare di Restituzione(). Queste operazioni fanno parte della stessa interfaccia che
chiamiamo Prestito. In UML abbiamo due notazioni per rappresentare questa situazione: uilizziamo il simbolo della
classe con lo stereotipo <<interface>> (si noti che anche qui la classe non ha la sezione attributi ma solo la sezione
operazione); utilizziamo la notazione a palloncino (lollipop), in cui non si elencano le operazioni. L’interfaccia è
collegata alle classi libro e CD tramite relazione di realizzazione con cui indichiamo che le classi libro e CD realizzano
le interfacce. Le interfacce non descrivono infatti gli algoritmi dunque non hanno un comportamento. Le interfacce si
implementano tramite altre classi. Libro e CD implementano Prestito, hanno le operazioni e le forniscono, si parla
appunto di interfacce fornite. La biblioteca usa quelle operazione, le richiede per il proprio funzionamento: parliamo
di interfacce richieste.

In fase di progettazione definiamo i moduli del sistema, componenti anche tra loro articolati: progettiamo per
componenti. Dato che i componenti interagiscono tra loro, bisogna accuratamente esplicitare i servizi che offrono e
quelli che richiedono. Per farlo utilizziamo le interfacce, fornite e richieste. Ogni volta che un componente richiede
un’interfaccia questa sarà fornita da un altro componente, possiamo allora accoppiare richiesta e fornitura a patto che
abbiano la stessa interfaccia (quindi siano battezzate dallo stesso nome), lo indichiamo attraverso il connettore di
assemblaggio. Abbiamo la possibilità di raggruppare un insieme semanticamente coeso di interfacce fornite e richieste
in una porta, esse saranno probabilmente interfacce relative a funzionalità comuni. Le porte possono avere una
visibilità e una molteplicità: quando la porta è disegnata sovrapposta al limite del componente è pubblica, all’interno
è protetta o privata; con la molteplicità [n], specificata dopo il nome della porta, indichiamo il numero di istanze della
porta che ciascuna istanza del componente avrà. La connessione tra le porte semplifica il connettore di assemblaggio,
non colleghiamo un’interfaccia fornita alla relativa richiesta ma direttamente porta a porta, l’insieme di interfacce
richieste e fornite tra le varie porte.

Il componente è un concetto fondamentale a livello di progettazione, si tratta di elementi non più a grana fine (come
erano le classi) ma a grana grossa. Un componente è una parte modulare di un sistema la cui manifestazione è
sostituibile nel suo ambiente. Anche le classi, a grana fine, hanno un’interfaccia, la classe infatti offre dei servizi.
L’interfaccia di una classe non esprime cosa la classe richiede, cosa sia necessario al suo funzionamento. Un’altra
differenza, probabilmente la principale, è che le classi vengono compilate singolarmente e messe insieme a tempo di
collegamento. È ancora un legame statico. Quando pensiamo a un componente pensiamo invece a qualcosa che
possiamo gestire, nelle sue interconnessioni con gli altri, in particolar modo a tempo di esecuzione (o subito prima, a
tempo di assemblaggio). Dunque possiamo mettere insieme i componenti o stesso sul campo, un attimo prima della
messa in esecuzione o proprio durante l’esecuzione stessa (dinamicamente). Se pensiamo al sistema operativo o a
molti siti web sono caratterizzati da dinamicità, un sito web, ad esempio, si presenta e ci mostra cosa differenti a
seconda di ciò che facciamo e ad altre cose, è dinamico e non si comporta allo stesso modo con tutti. Possiamo quindi
affermare che la principale differenza tra componenti e classi è che i componenti sono moduli software che si vogliono
mettere insieme in fase di assemblaggio o addirittura di esecuzione, non prima del rilascio.

Quando progettiamo il componente dall’esterno dobbiamo esplicitare interfacce fornite e richieste, dunque i servizi
offerti e richiesti e, dato che l’interfaccia è un contratto, una volta rispettato ciò la sostituibilità può avvenire anche a
tempo di esecuzione. Nella definizione stessa di componente abbiamo detto che esso è sostituibile, possiamo
sostituire un componente con un altro compatibile, che abbia lo stesso comportamento esterno, a prescindere dalla
realizzazione interna. Ciò che varia è la manifestazione del componente, nel rettangolo che lo rappresenta abbiamo
infatti una parte, in basso, che indica come è
fisicamente realizzato. Il componente è allora una
forma, è un modulo la cui manifestazione è
sostituibile, nel suo ambiente. Il componente si
comporta come un black-box, il cui comportamento
esterno è definito da interfacce fornite e richieste.
Notiamo che la definizione UML non specifica che la sostituzione deve poter avvenire a tempo di esecuzione, è più
astratta. I package erano un altro elemento UML che effettuavano un raggruppamento secondo qualche criterio di
coesione, a differenza di un componente però questi effettuano solo un raggruppamento logico di elementi del
modello, non sono sostituibili all’interno del loro ambiente. Abbiamo osservato che sono un concetto più analogo a
quello del namespace C++, introducono uno spazio dei nomi.

I componenti sono classificatori strutturati, ovvero classificatori dotati di struttura interna. In UML i classificatori
strutturati sono i simboli come il rettangolo, indicano classi di elementi. La struttura interna indica le parti di cui è
composto il classificatore e che a loro volta possono essere connesse al loro interno, così come il classificatore può
essere connesso ad altri classificatori, mediante connettori. Una parte indica il ruolo che essa ricopre nel suo
partecipare al classificatore strutturato. Nel rettangolo che rappresenta un componente possiamo mostrare anche la
sua struttura interna. Quando progettiamo un componente dall’interno dobbiamo allora indicarne le parti, i
componenti interni che a loro volta avranno proprie interfacce (fornite e richieste): mediante il diagramma dei
componenti illustriamo la struttura interna di ciascun componente e affidiamo le responsabilità. Nel diagramma in
esempio A è costituito da B e C, tra loro interconnessi, fornisce l’interfaccia I1 e richiede l’interfaccia I2. In particolare
la realizzazione di I1 è delegata a un’istanza di B, che assume il ruolo chiamato “b”, così come la realizzazione di I2 è
delegata a C. B è responsabile di ciò che A offre, C di
ciò che richiede. Queste informazioni, oltre a essere
necessarie alla realizzazione sono utili in fase di
manutenzione, per effettuare le analisi di impatto. Se
si modifica un’interfaccia qualcosa cambia nel sistema
software, nel nostro caso se cambiamo I2 verrà
impattato A, ma nello specifico C e non B.

Un componente è un’unità a livello architetturale che può essere realizzata mediante uno o più artefatti (artefacts),
come classi, script, pagine html, file.java eccetera. Attraverso il diagramma dei componenti indichiamo come si
manifesterà ciascuna parte, la relazione tra un artefatto e il relativo componente è manifest, oppure utilizziamo un
componimento fisico sempre indicato dallo stereotipo. UML fornisce vari stereotipi per gli artefatti tra cui file,
document, library…

In realtà la definizione UML prevede che si possa indicare come componente qualcosa che non sia necessariamente
istanziato a tempo di esecuzione ma più simile a un costrutto puramente logico, istanziato in virtù del fatto che
vengono istanziate le sue parti.

In UML definiamo anche sottosistemi, parti di un sistema che a loro volta formano un sistema, più piccolo. È un
concetto che serve a raggruppare ciò che poi verrà messo in esecuzione. Anche in questo caso il sistema non è
direttamente eseguibile ma è istanziato il virtù delle parti che lo compongono. Un sistema è in componente che agisce
come unità di scomposizione per un sistema più grande. La principale differenza col componente è che quest’ultimo
è sostituibile nel suo ambiente, a tempo di esecuzione o di assemblaggio. Il sottosistema è invece una sorta di schema
a blocchi, unità di scomposizione. Li indichiamo sempre attraverso un rettangolo, con lo stereotipo sottosistema. Il
concetto di raggruppamento è simile a quello visto nei packages, la differenza è che ora non raggruppiamo elementi
del modello ma della soluzione. I componenti fanno parte dell’eseguibile quindi della soluzione: avranno un’istanza a
tempo di esecuzione, anche se tecnicamente UML prevede che possano essere istanziati anche solo in virtù del fatto
che vengono istanziate le sue parti.

La CBSE (Componenti Based Software Engineering) è l’ingegneria dei componenti, lo skill richiesto è quello di sapere
progettare per componenti, progettare per interfacce. È necessario lavorare come dei software architects (coloro
che definiscono le architetture del sistema software), è necessaria una visione d’insieme dell’architettura di tutto il
sistema.
Note: In Java il codice sorgente viene tradotto nel byte code (un linguaggio intermedio) e poi eseguito dalla
Java Virtual Machine, indipendentemente dal particolare processore. Una delle esigenze era infatti
programmare applicazioni che girassero su web. Quando si esegue il programma Java non tutto deve
necessariamente far parte dello stesso byte code, possiamo caricare qualcosa dinamicamente. Esistono
librerie dinamiche come il DLL (Dynamic Link Library) in quanto sarebbe inutile mettere nella libreria normale
qualcosa di utilizzato raramente o forse mai. Il concetto allora è che non carichiamo una classe in memoria a
prescindere, la carichiamo dinamicamente nel momento in cui la utilizziamo.

16. Pattern architetturali:


L’architetto Cristopher Alexander ideò il concetto di pattern: un pattern è la descrizione di un problema ricorrente e
della soluzione che possiamo mettere in atto per risolverlo. Esiste un vero e proprio catalogo dei patterns, ciascuno
di essi si presenta con la descrizione del problema e poi della relativa soluzione, tipicamente adottata. I pattern
esistono in ciascuna fase del ciclo di vita del software, dall’analisi alla codifica. I pattern di progettazione su più larga
scala sono detti pattern architetturali (o archetipi architetturali, architetture astratte riusabili), i pattern di
progettazione di dettaglio sono detti design patterns (soluzioni comprovate di problemi ricorrenti). Essi favoriscono il
riuso del software e delle sue componenti ma allo stesso tempo forniscono un vocabolario comune.

In fase di architettura organizziamo il sistema in componenti e, mappiamo le funzionalità sui moduli di cui si compone
il software. Oltre ai requisiti funzionali vanno mappati anche i requisiti non funzionali (allocazione dei requisiti ai
componenti del sistema). Mappare i requisiti non funzionali tendenzialmente è più difficile. Se dobbiamo garantire
l’affidabilità spesso una soluzione adottata è l’introduzione della ridondanza. Supponiamo di star progettando un
aereo, vi sarà un computer che indicherà cosa fare, ad esempio se frenare o meno. Se abbiamo una sola macchina e
questa si rompe avremo inevitabilmente una failure. Si parla di Single Point of Failure (SPOF), se vi è un solo punto di
fallimento allora quando esso si verificherà nulla sarà possibile per evitarlo. Per essere sicuri che non ci siano fallimenti
allora poniamo tre computer: introduciamo la ridondanza replicando dei moduli, in questo modo l’architettura
ammettere sistemi di tolleranza ai guasti (Fault Tolerance). A bordo di un aereo o di un treno tipicamente si utilizza
una ridondanza tripla, 3MR o TMR (Triple Modular Redundancy), si usa il numero triplo così da seguire la maggioranza.

Per decomporre un sistema in sottosistemi abbiamo due approcci tipici, l’architettura a strati (layer) o a partizioni. Le
architetture a strati sono gerarchie, ogni strato fornisce dei servizi, si presenta attraverso dei servizi e si manifesta
tramite operazioni che altri strati possono invocare. Il livello di interfaccia tra due strati è una API (Application
Programming Interface): l’interfaccia di uno strato che fornisce servizi ad altri strati che possono accedervi invocando
le operazioni definite nell’API. Tra le architetture a strati differenziamo le architetture chiuse da quelle aperte. In
un’architettura chiusa ogni strato può accedere solo a quello immediatamente sottostante. In un’architettura aperta
gli strati possono usufruire dei servizi offerti anche da strati più in basso. Una tipica architettura chiusa è quella dei
protocolli, un sistema operativo è invece aperto: c’è un nucleo, kernel, dove è descritta la schedulazione dei processi
e la gestione della memoria, ogni componente lo utilizza.
Progettare un’architettura chiusa fa sì che quando viene cambiato uno strato non cambia niente se l’API non viene
impattata, in caso contrario cambia solo lo strato immediatamente superiore, in quanto è l’unico a utilizzarla. Se
l’architettura è aperta nel momento in cui cambia un’API cambieranno tutti gli strati che vi hanno accesso.

L’architettura a partizioni organizza il sistema in moduli tra loro pari (peer). Solitamente si utilizza questo approccio
per raggruppare servizi secondo qualche criterio di coesione. Nelle architetture a strati il criterio era per lo più il
differente livello di astrazione. Nelle singole partizioni i servizi sono coesi tra loro ma diversi dagli altri.

• Pattern MCV (Model-View-Controller):

È un archetipo architetturale utilizzato in particolar modo in alcune tecnologie legate a Java. Questo pattern nasce
dalla nacessità di visualizzare dati generici mediante l’utilizzo di rappresentazioni diverse, diverse viste. Il sistema viene
strutturato in tre componenti che interagiscono tra loro: il Controller, la Vista e il Modello. Il modello incapsula i dati,
la vista si occupa della loro presentazione e il controller incapsula la business logic. L’utente riceve quello che mostra
la vista e imposta ciò che vuole vedere nello strato View. Se vi sono delle modifiche l’utente deve visualizzare i dati
modificati, quando l’utente richiede di vedere un dato la Vista lo deve prelevare dal Modello. Questo passaggio non
avviene direttamente tra la Vista e il Modello, altrimenti la View implementerebbe anche la business logic, avviene
tramite il Controller che trasforma gli input utente (nella View) in azioni del Model, comunicando al modello ciò che
dev’essere fornito alla vista per cambiare la visualizzazione dei dati.

L’MVC disaccoppia Vista e Modello secondo il modello publisher/subscriber. I dati si trovano nel modello e quando
cambiano occorre cambiarne la rappresentazione, per preservarne la coerenza. Se cambiano i dati di un foglio
elettronico (che si trovano nelle celle dei valori) va modificato un eventuale grafico. Quando il modello cambia avvisa
la vista tramite una notifica (modalità “push”), in tal caso il modello fa da notificatore e la vista dovrà aver effettuato
una sottoscrizione, richiedendo di essere aggiornata. Chi modifica il dato esegue l’operazione logica di pubblicare, è il
publisher. Nell’esempio già visto del giornale chi pubblica il giornale può sapere o meno chi lo leggerà, lo saprà se è
un giornale su abbonamento, i lettori avranno effettuato una sottoscrizione. È differente se pubblichiamo un articolo
online, in tal caso non sapremo chi lo leggerà e chi vorrà farlo accedere autonomamente alla pagina web per farlo
(modalità “pull”). Con questo modello allora disaccoppiamo publisher e subscriber, esistono sotto-modalità diverse.

Il modello incapsula lo stato dell’applicazione, cambia il dato in quanto cambia lo stato e dunque va modificata la
vista. La vista a sua volta interpreta il modello e può richiedere un refresh e può voler modificare un dato, ad esempio
quando l’utente immette un nuovo dato tramite la vista, così come può richiedere un nuovo stato. La Vista
comunicherà al Controller l’input ottenuto e questo comunicherà al Modello di cambiare lo stato: il Controller
rappresenta la logica business, la Vista la logica di presentazione e il Modello la logica dello stato.

Possiamo rappresentare l’interazione tramite un


diagramma di sequenza, gli oggetti coinvolti sono
il Controller, il Model e la View. L’utente chiede
alla Vista quale pagina visualizzare, la Vista a sua
volta richiede al Controller di elaborare la pagina e
restituisce la pagina da visualizzare. Se c’è un altro
input la vista lo comunica al controller che
richiederà al Model di elaborare lo stato.

• Pattern BCE (Boundary – Control – Entity):

Questo archetipo si basa sulla presenza di tre gruppi di classi, package: il package boundary contiene le classi che
gestiscono l’interfacciamento con l’utente e la presentation logic; il pakcage control contiene gli oggetti che
recepiscono gli eventi generati dall’utente, rappresentano le azioni dei casi d’uso, implementando la business logic; il
package entity raggruppa le classi che rappresentano i dati, entità del dominio applicativo, e si occupa
dell’interfacciamento tra questi e la base di dati in cui sono salvati (relazionale o no), implementa la data-access logic,
gestendo la persistenza dei dati.

Per certi versi è un pattern simile all’MVC ma nasce con obiettivi diversi. A differenza dell’MVC nasce nel contesto
della modellazione object-oriented, con l’obiettivo di assegnare a oggetti diversi responsabilità diverse. Siamo in fase
di progettazione perché stiamo modellando l’architettura del software, in analisi infatti la persistenza dei dati non è
un problema presente. Nel package entity del modello BCE vi saranno le entità che accedono ai dati e queste riflettono
le entità individuate nel dominio del problema, in fase di analisi. In fase di progettazione vengono prese decisioni,
molte a proposito della persistenza dei dati e dell’interfacciamento con l’utente. Come per ogni decisione il progettista
spazia in un design space, effettuando una scelta in uno spettro di soluzioni possibili. Anche nel momento in cui
fissiamo un paradigma di progettazione come l’object-oriented la soluzione non è unica. Ipotizziamo di avere n entità
tra cui ContoBancario, Mutuo, Saldo.. per ogni entità vi sono degli oggetti cb1, cb2, m1, m2, s1, s2 e così via. Abbiamo
dei dati che devono essere gestiti in un DBMS o in generale in archivi. I dati si trovano nell’applicazioni. Nella base di
dati avremo una tabella per ContoBancario, una per Mutuo, una per Saldo e via dicendo, all’interno di queste
troveremo le tuple contenenti lo stato dei singoli oggetti. Ciascun oggetto dovrà allora avere un’operazione “salva()”
tramite cui aprire una connessione con il DBMS e salvare lo stato in una tupla. Un estremo dello spettro delle soluzioni
è porre in ciascun oggetto un’operazione che provveda alla sua persistenza in memoria (“ciascuno salvi se stesso”).
L’estremo opposto sarà invece aggiungere un’entità con l’unica operazione “salva()”, che si occupi di salvare tutti gli
altri oggetti (“uno salva tutti”). Gli altri oggetti dovranno avere un’operazione “salvami()” con cui richiederanno
all’entità incaricata di salvarli, la differenza è che ora un solo oggetto accede al DBMS utilizzando la sua API (che in
questo caso è l’SQL), dunque se questa verrà cambiata ne sarà impattato un solo oggetto e non più tutti. Vediamo
allora come una decisione sulla persistenza abbia molte conseguenze anche in fasi successive, ad esempio dal punto
di vista della manutenibilità. Ovviamente tra i due estremi vi possono essere molte soluzioni intermedie, possiamo
avere pochi oggetti che salvino gli altri o ad esempio un oggetto per ciascuna classe che si occupi del salvataggio di
tutte le sue istanze. In fase di progettazione allora sceglieremo la soluzione più adatta in base al problema sottoposto.

Nel momento in cui un oggetto chiede a un altro di salvarlo gli deve inviare il suo stato, mandandogli un riferimento a
se stesso (in C++ il puntatore this). Abbiamo infatti la possibilità di passare come parametri di un metodo degli oggetti
(o.m(x);). Per lavorare su un oggetto dobbiamo usare i metodi definiti su di esso (un oggetto è fatto della struttura dati
e delle operazioni consentite su di esso), se ci troviamo nello stesso processo questo non è un particolare problema in
quanto le istruzioni si trovano nell’area codice, che è condivisa. Basterà allora inviare lo stato dell’oggetto (tramite un
riferimento a questo) in quanto anche chi riceve l’oggetto sa dove si trova il codice implementazione della classe
ricevuta (le operazioni delle classi si trovano in un’unica copia in area codice, accedono allo specifico oggetto ricevendo
come parametro in ingresso nascosto il puntatore this, in C++). Il problema allora si pone quando inviamo lo stato
dell’oggetto a un oggetto che si trova in un altro processo o nodo, non avrebbe senso mandare un puntatore in quanto
lo spazio di indirizzamento è diverso, bisogna linearizzare lo stato. Tuttavia, essendo su un altro processo chi riceve lo
stato non sa dove si trovi il codice del relativo oggetto, occorre mandargli anche questo. Bisogna effettuare
un’invocazione remota dei metodi. In Java abbiamo la possibilità di utilizzare il paradigma a oggetti RMI (Remote
Method Invocation), Java è pensato per applicazioni su web, che comunicano via rete. Occorre mandare il codice
perché l’eseguibile non basterebbe, ciò che è eseguibile su un processore non è detto lo sia su un altro. In Java
utilizziamo infatti il byte code e la macchina virtuale, per ovviare al problema della portabilità.

In particolare, se la persistenza si realizza tramite database si parla di pattern BCED, il package entity rappresenta le
classi individuate in analisi, nel package database avremo le classi che si occupano della gestione del database.
L’interfacciamento con il database viene gestito da quest’ultimo package. Nel progettare la soluzione dobbiamo
gestire l’apertura e la chiusura delle connessioni con il database, l’autenticazione, la sicurezza, il concetto di cursore
ecc..

Il pattern DAO sfrutta le classi DAO per accedere ai dati. Oltre alla classe ContoCorrente avremo un’entità
ContoCorrente DAO che si occuperà di salvarne lo stato sulla tupla corrispondente nel database e, analogamente, di
caricare lo stato della tupla all’oggetto corrispondente. In altri termini le classi DAO colmano il gap tra il modello di
programmazione object-oriented, in cui i dati sono lo stato dell’oggetto, e il modello di archiviazione del database
relazionale, dove i dati sono le tuple. Lo stato diventa la riga di una tabella e viceversa. La classe DAO è il duale di un
oggetto che si occupa di leggere la tupla e caricarne il contenuto nello stato e, allo stesso modo, leggere lo stato e
memorizzarlo nella tupla. L’oggetto non sa né si preoccupa della tabella su cui sarà memorizzato, se ne occupa
l’oggetto DAO.

C’è un altro modo per gestire l’accesso a db, mediante api che gestiscono servizi sofisticati (persistence framework).

• Pattern Broker:

Il Broker è un pattern architetturale con schema logico di tipo publish/subscribe. Se consideriamo un’applicazione
client/server il server sono coloro che rendono noti i servizi forniti (publisher) e i client utilizzano quei servizi
(subscriber). Client e server, ovvero chi richiede e chi offre, sono disaccoppiati da un intermediario, il broker. Se
dobbiamo connettere un server a n clienti tramite dei bus ci servono n fili. Il broker rappresenta invece un bus software
che permette ai produttori di porre tutti i dati sul bus, così da non dover comunicare direttamente con ciascun
consumatore. È una tecnica molto usata nelle architetture middlewhere, gli strati software sopra al sistema operativo
ma sotto alle applicazioni. Se modelliamo il pattern tramite un diagramma di sequenza gli oggetti coinvolti sono in
primis il client, il broker e il server. Il client invoca le API del broker, se vuole modificare un dato comunica col broker,
se invece riceve una notifica push, ad esempio, è il broker a comunicare con il client. Il broker si trova su un altro nodo,
il client vi interagisce mediante un processo locale che fa il rappresentante di ciò che sta nell’altro nodo, nel gergo
informatico parliamo di proxy. Il client non si rivolge direttamente al broker ma al client side proxy, uno strato
software addizionale tra di essi che permette al client di comunicare con un oggetto remoto, quale il broker, come se
fosse locale. Per comunicare direttamente col broker il client dovrebbe sapere impacchettare e trasferire i dati via
rete, dovrebbe conoscere dettagli in più a cui non è realmente interessato. Di tutti questi aspetti si occupa il proxy,
sollevando il client da molte responsabilità, così da fornirgli servizi ad un livello d’astrazione congruo rispetto a ciò che
deve fare. Nell’introdurre il proxy otteniamo molti vantaggi, tutti i pacchetti dati transitano attraverso esso. Se
effettuiamo la connessione a un server web straniero e importiamo il proxy http “unina”, il ricevente vedrà provenire
la richiesta dal proxy e non da noi. Impostando il proxy come intermediario anche all’estero possiamo accedere dei
servizi come dall’indirizzo IP dell’università. Tra le varie funzionalità il proxy effettua anche il precatching, se stiamo
leggendo una pagina web precarica la seconda ecc. questo fa risparmiare tempo. Se leggiamo una tabella i dati dal
disco vanno in memoria. Se il caricamento avvenisse per ogni singola tupla i tempi di caricamento sulla rete (nel caso
di un database remoto) e di accesso in memoria sarebbero lunghi.

I proxy sono gli intermediari che ci consentono di disaccoppiare il client dai dettagli della comunicazione con il broker,
allo stesso modo abbiamo un server side proxy che effettua la stessa funzione lato server. Dal diagramma di sequenza
vediamo che quando il client ha
bisogno di un servizio invia la
richiesta al proxy che impacchetta i
dati e inoltre la richiesta al broker. Il
broker trova il servizio e lo chiama
mandando la richiesta al proxy lato
server. Il proxy spacchetta i dati e fa
eseguire il servizio al server che
comunica il risultato al proxy. Il
proxy lato server impacchetta i dati,
manda il risultato al broker che
trova il cliente, invia al proxy, questo
spacchetta i dati e manda il risultato
al client.

Osserviamo quindi che dal punto di


vista degli oggetti componente il diagramma di sequenza si è complicato. Tuttavia, il broker è un componente
middlewhere che si compra, non va dunque implementato. Chi sarà incaricato di implementare il lato client dovrà
implementare effettivamente solo il client, che inoltre è stato sollevato da molti oneri, affidati invece al proxy (che
non va implementato). Dunque, tutti gli intermediari introdotti non vanno implementati, ci si occupa solo di client e
server.

Possono anche comunicare più broker tra di loro, in questo caso si useranno dei bridge.

• Pattern Pipe & Filter:

Pipe line è la catena di montaggio. Vediamo qualche esempio pratico: se vogliamo conoscere i file di una cartella da
linea di comando in Windows utilizziamo il comando dir, in Linux ls. In questo modo sappiamo che file ci sono ma li
leggiamo in maniera disordinata, se vogliamo ordinarli usiamo il comando sort. Se vogliamo passare alla schermata
successiva utilizziamo il comando more. Ognuno di questi programmi svolge un compito semplice, da questi possiamo
realizzare un compito più complesso. Facciamo in modo che ogni programma non lavori in realtà su file ma su
un’astrazione, lo standard input. Allo stesso modo produce i propri output sullo standard output. In questo modo
possiamo organizzare diversi programmi collegando lo std output dell’uno allo std input dell’altro. Abbiamo collegato
due programmi in catena, mettendo i parametri in pipe. Ls produce l’elenco dei file disordinato, sort ordina l’elenco.
Mettendoli in pipe otteniamo un programma in grado di produrre l’elenco ordinato (il pipe in Dos equivale allo slash:
dir/sort), un compito più complesso.

Molti programmi sono realizzati secondo questa tecnica. È una filosofia di progettazione.
Se i programmi sono diversi, dunque non prendono sempre dallo std input e producono sullo std output, risulta difficile
farli comunicare in maniera diretta, un programma dovrebbe accedere alle API dell’altro. introduciamo allora un livello
intermedio in cui l’uno deposita i dati che l’altro leggerà.

Un altro esempio di programma pipe&filter è il compilatore, le operazioni di analisi avvengono in sequenza: l’analisi
lessicale invia i simboli individuati all’analizzatore sintattico, che manda un purse tree (?) all’analizzatore semantico e
questo a sua volta lo manderà al generatore del codice.

Una generalizzazione di questo approccio è che le applicazioni possono avere dei dati condivisi. Non è detto che si
metta in comunicazione l’applicazione, ma si deve consentire l’accesso nel rispetto di certe regole. Un altro pattern è
allora lo shared data.

• Pattern Client/Server:

anche questo pattern ha lo scopo di disaccoppiare l’interazione tra due entità comunicanti. La differenza rispetto al
pattern Broker è che ora la comunicazione non è tra pari. Solo uno può iniziare la comunicazione, solo uno può essere
attivo: il client. Il server è del tutto passivo. È un modello analogo a quello che vige tra gli oggetti, entità passive. Nel
caso della programmazione parliamo di oggetti, nel senso di programmi o applicazioni distribuite su rete si parla di
client e server. I client server possono essere distribuiti su uno, due, o n livelli. Alla fine, com’è tipico delle applicazioni
gestionali, dobbiamo ottenere presentation, business e dataaccess logic, distribuite in modi diversi. Se mettiamo una
parte della logic sul client ovviamente alleggeriamo il server, se lasciamo tutti i dati sul server abbiamo un database
centralizzato, vi possono essere poi anche distributed database.

17. Design Patterns:


A differenza dei pattern architetturali sono pensati in fase di progettazione e non in fase di definizione dell’architettura
(progettazione ad alto livello). In molte circostanze ci possono essere aspetti più di dettagli in cui è possibile adottare
soluzioni comprovate di problemi ricorrenti, dunque dei pattern. Classifichiamo i pattern di design secondo due criti:
lo scopo (purpose) che riflette cosa fa il pattern e l’ambito (scope). L’ambito specifica se il pattern è relativo a classi o
ad oggetti. Lo scopo può essere invece:

- Creazionale: il pattern astrae il processo di creazione di oggetti (istanziazione).


- Strutturale: tratta la composizione di classi o oggetti in strutture più complesse in cui le classi cooperano per
uno scopo comune
- Comportamentale (behavorial): si occupa del modo in cui classi o oggetti interagiscono reciprocamente.
• Singleton:

Tra i pattern creazionali abbiamo il pattern singleton (crazionale di ambito a oggetti), utilizzato quando ci serve un
singolo oggetto di una classe. Sebbene tendenzialmente si crei una classe proprio quando serve un insieme di oggetti
dello stesso tipo, possiamo avere il caso in cui necessitiamo di una sola istanza. Tra le varie tecniche di programmazione
abbiamo visto che con la programmazione con oggetti (e non ad oggetti) possiamo creare un oggetto senza dover
definire un tipo di dato astratto né la classe. Un oggetto è la struttura dati unita alle operazioni consentite su di essa,
e nel momento in cui definiamo una classe esso è un’istanza della classe. A differenza del C++, Java non è un linguaggio
ibrido ma totalmente a oggetti, non possiamo allora adottare la tecnica di programmazione prima discussa, gli oggetti
sono solo istanze di una classe. Dunque, anche nel momento in cui vogliamo avere un singolo oggetto siamo costretti
a definire una classe: utilizziamo il pattern singleton. Un’altra differenza con il C++ è che in Java non si possono definire
variabili esterne ma solo variabili locali, definite all’interno di un blocco (parte di codice racchiusa tra parentesi graffe).
Se definiamo un oggetto in C++ come variabile esterne non controlliamo che ve ne sia effettivamente una sola istanza.
Il pattern singleton viene usato quando bisogna assicurarsi che vi sia una sola istanza di una classe, accessibile
globalmente e sottoclassabile. La realizzazione di un singleton prevede un’operazione di istanziazione, la struttura
dati consterà invece di un riferimento all’unica istanza, dunque un
riferimento a un oggetto della stessa classe. Notiamo che una
definizione di questo tipo non è ricorsiva perché non vi è un
oggetto singleton stesso ma un riferimento: un puntatore occupa
in memoria uno spazio che non dipende dal particolare oggetto a
cui punta (è solo un indirizzo), se vi fosse stato l’oggetto il
compilatore non avrebbe saputo quanto spazio in memoria
sarebbe stato necessario. Notiamo inoltre che il puntatore
dev’essere definito static, in questo modo ci assicuriamo che non
ve ne siano altri (una variabile membro static è una copia comune
accessibile a tutti gli oggetti della classe), perché l’oggetto
dev’essere unico.
Possiamo infine sottoclassare il singleton, definendo una classe
particularSingleton derivata da essa.

• Façade:

il pattern façade è strutturale, relativo a oggetti.

Nella figura di sx abbiamo alcuni oggetti collegati tra loro mediante relazioni di uso (frecce). In particolare, la classe
azzurra utilizza tre classi verdi, le altre due servono a fare ciò che è stato chiesto alle tre. La classe celeste dunque
utilizza tutto il gruppo verde, anche se solo alcune classi in maniera diretta. Le classi verdi formano allora un
sottosistema a cui la classe celeste accede per richiedere dei servizi. Dalle relazioni è evidente che le classi verdi siano
legate tra loro e non con il resto, c’è coesione tra di esse e non ce n’è con l’azzurra, realizziamo allora un sottosistema
o un componente tramite le classi verdi. L’accoppiamento tra la classe celeste e il gruppo verde è alto: vi sono tre
relazioni d’uso. Dunque, la situazione in figura non è preferibile. Introduciamo allora una nuova classe gialla (figura a
dx), apparentemente complichiamo le cose, passiamo infatti da 6 a 7 classi. La nuova classe però viene posta tra la
celeste e il sottosistema: la classe celeste ora ha una dipendenza d’uso solo con la classe gialla, abbiamo ridotto le
dipendenze, dunque l’accoppiamento, concordemente al principio di alta coesione e basso accoppiamento. La classe
gialla è ancora legata a tre delle verdi ma le frecce dell’azzurra si sono ridotte da tre a una sola freccia, che rappresenta
qualcosa di molto più astratto. Nella figura di sx la classe celeste non era ben disaccoppiata dal sottosistema e per fare
qualcosa di più complesso doveva utilizzare uno alla volta i tre servizi richiesti, ora invece potrà effettuare ciò
eseguendo un’unica richiesta, più astratta.

Esempio robot: se utilizziamo un robot per spostare un oggetto allora una telecamera ne localizzerà la
posizione, il braccio meccanico lo sposterà fisicamente e le chele si apriranno e chiuderanno per poterlo
afferrare e lasciare. Le operazioni allora che dobbiamo chiamare saranno 4: identificazione sulla camera,
spostamento sul braccio, apertura e chiusura sulle chele. Dobbiamo conoscere la struttura e il
comportamento. Introduciamo allora una façade che ci fornisce l’operazione moveObject a cui passiamo i
parametri e che si occuperà di chiamare per noi le operazioni sottostanti (le 4 operazioni saranno
l’implementazione della nuova operazione moveObject). In questo modo interagiamo solo con la façade senza
conoscere gli oggetti del sistema.

Allo stesso modo con l’introduzione della classe gialla la classe celeste non sa cosa vi sia nel sottosistema. Utilizza in
maniera più semplice una sola istruzione. Il pattern façade fornisce una vista semplice di un sottosistema complesso.

Ritornando all’esempio del robot potremmo voler cambiare il modo di localizzazione dell’oggetto, ad esempio tramite
un sensore, pur cambiano il metodo per identificare la posizione l’utilizzatore non deve cambiare nulla, non ne è
impattato e ciò è un vantaggio per la manutenibilità. Notiamo che il componente in più aggiunto allora è una facciata
(façade) del sottosistema verso gli utilizzatori. Realizziamo la façade quando vogliamo fornire un’interfaccia per un
insieme di funzionalità che, se ben usate, possono produrre un’operazione complessa ma che, per fare ciò, ne
richiederebbero all’utilizzatore la conoscenza. Dunque, l’utilizzatore conosce la façade che gli consente di richiedere
una funzionalità più astratta senza dover conoscere nel dettaglio i componenti di un sottosistema. La façade conosce
invece quali classi di un sottosistema sono responsabili di una richiesta così da delegare le richieste del client agli
oggetti appropriati.

Un altro esempio è il compilatore: il compilatore si occupa di eseguire l’analisi lessicale, sintattica e semantica e di
generare il codice, tuttavia quando dobbiamo compilare non chiamiamo l’analizzatore lessicale, sintattico, semantico
e il generatore di codice, chiamiamo direttamente il compilatore che è il façade di queste quattro cose.

• Adapter:

Un altro pattern strutturale è il pattern adapter. Nella vita quotidiana possiamo utilizzare un adattatore per la presa
corrente se ci serve alimentazione elettrica ma con una certa interfaccia, per quella spina. L’interfaccia in questo caso
sarà fatta da amperaggio, voltaggio e forma della presa. Tendenzialmente di questa interfaccia amperaggio e voltaggio
vanno bene ma la forma no. Il nostro fornitore non rispetta esattamente tutta l’interfaccia, riusiamo quello che va
bene tramite un adattatore. L’adattatore viene posto tra client e fornitore così da far vedere al client ciò che si aspetta
e al fornitore ciò che si aspetta gli venga richiesto. In progettazione utilizziamo il pattern adapter quando:

- Vogliamo utilizzare una classe esistente la cui interfaccia non risponde esattamente alle nostre necessità;
- Vogliamo realizzare una classe riusabile che cooperi con classi che hanno interfacce non troppo compatibili;
- Occorre utilizzare sottoclassi esistenti che non possono essere ulteriormente sottoclassabili (è possibile
indicare che una classe sia la “foglia” di una gerarchia, vincolando chi la utilizza, con il comando finally).

Il client si rivolge per un certo servizio a un target (che


definisce l’interfaccia specifica che il client utilizza),
l’adapter deriva da target e dalla classe adaptee
(l’interfaccia esistente che dev’essere adattata).
L’implementazione dell’operazione request() in adapter
invocherà l’operazione specificRequest() in adaptee.
Questo per un pattern in ambito di classi, tuttavia
l’adapter è anche un pattern in ambito di oggetti. In
quest’ultimo caso allora client si rivolge a target da cui
deriva adapter. Adapter contiene l’adattato e vi si
rivolge. Dunque, a differenza della prima soluzione in
questa non vi è la derivazione multipla.

la classe adapter non va bene se vogliamo adattare a loro


volta anche le sottoclassi.

Confronto façade – adapter: entrambi i pattern si interpongono tra client e fornitore, l’adapter per adattare e il façade
per disaccoppiare, riducendo le dipendenze del modulo cliente dai moduli che rappresentano il sistema. Il façade allora
ha un compito di semplificazione, l’adapter di conversione. Entrabi sono wrapper: entità che incapsulano altre entità.

• Composite:

Il pattern composite è un pattern strutturale che


consente di trattare un gruppo di oggetti come
fossero un singolo. Supponiamo di avere linee e
1: ambito di classe
cerchi, possiamo realizzare a partire da essi nuove
figure, come il rettangolo. Il rettangolo è composto
di 4 linee, dunque è composto da figure più
semplici. Lo scopo è poterli trattare in maniera
omogenea: vogliamo poter trattare gli oggetti
composti come gli oggetti componenti. Ciascuna
figura ha il metodo draw() per disegnarla, possiamo
disegnare il gruppo di figure (composto da più figure) 2: ambito di oggetto
invocando la sua funzione draw() che consisterà nella chiamata iterativa delle funzioni draw() delle figure componenti
(in un ciclo for). La gestione del gruppo
realizzata in questo modo ne comporta
comunque una trattazione differente dai
singoli componenti. Poniamo allora il
gruppo nella gerarchia di figura, in questo
modo possiamo affermare che “il gruppo
è una figura”. Tuttavia, il gruppo rimane
composto da figure, aggiungiamo anche
la composizione, ottenendo una
ricorsione: il gruppo è una figura
composta da figure. In questo modo
inoltre il gruppo può essere a sua volta
composto da altri gruppi. Così facendo il
pattern composite ci consente di trattare gli oggetti composti alla stregua dei semplici componenti. Il composto è fatto
di componenti, tra cui composti stessi in quanto anch’essi componenti.
I leaf sono le foglie e hanno il comportamento degli oggetti primitivi. In questo modo massimizziamo la flessibilità,
perché i composti sono utilizzabili come componenti, e
minimizziamo i problemi di sicurezza. Il problema che
invece sorge è che il gruppo e le figure hanno interfacce
diverse in quanto il gruppo ha anche l’operazione di add(),
che gli consente di aggiungere figure. Per risolvere il
problema possiamo inserire l’add sulle foglie ma
dev’essere diversa.
le foglie e i composite possono anche essere oggetti
astratti. Il client in questo modo manipola gli oggetti
attraverso l’interfaccia di component e in ogni punto in cui
il client si aspetta un oggetto primitivo possiamo passargli
anche un oggetto composto.

• Observer:

Il pattern observer ci permette di definire una dipendenza “uno a molti” tra oggetti, in modo che quando un oggetto
cambia stato tutti gli oggetti da esso dipendenti siano notificati e aggiornati automaticamente. È alla base di molti
sistemi ad eventi.

Se riceviamo un messaggio whatsapp su un gruppo è perché vi siamo iscritti, l’atto con cui ci registriamo a un oggetto
è la sottoscrizione e quando tale oggetto cambia stato riceviamo una notifica (secondo il modello publish/subribe).
C’è un forte disaccoppiamento tra chi produce e chi consuma eventi, in particolare tale pattern è ideale per i legami
tra produttori e consumatori di eventi di tipo molti a molti. La comunicazione avviene in maniera asincrona. Da un lato
vi sono gli oggetti dotati di stato e dall’altro i loro osservatori, interessati alle modifiche dello stato. Senza utilizzare il
pattern, ovvero senza ricevere automaticamente la notifica, bisognerebbe continuamente interrogato l’oggetto per
conoscerne lo stato (polling), il che provoca una perdita di tempo in quanto l’attesa è attiva (busy wait). Anche nel
caso della notifica le modalità sono push e pull: o con la notifica facciamo arrivare direttamente il nuovo stato (push)
o notifichiamo semplicemente che lo stato è cambiato, così che l’osservatore vada a prelevarlo (pull), se consideriamo
di nuovo whatsapp abbiamo la possibilità che la notifica ci mostri direttamente il messaggio o che ci segnali solamente
il suo arrivo. Un altro svantaggio del polling è che controllare troppo frequentemente lo stato dell’oggetto comporta
una perdita di tempo ma farlo sporadicamente può portare ad accorgersi troppo tardi di eventuali variazioni, in
particolare lo stato potrebbe anche essere cambiato più volte (il che porterebbe alla perdita di alcuni cambiamenti,
non osservati).

Dunque, gli osservatori si registrano, in questo modo quando avviene il cambiamento dello stato l’oggetto osservato
notifica l’osservatore. L’osservatore deve fornire un metodo all’osservato, che gli consenta la notifica. Tale
meccanismo è quello delle callback. L’oggetto notifica l’osservatore, inviandogli un metodo per la callback.
L’osservatore in quel momento non è attivo, può fare altro, l’attivation box si riattiva quando arriva la callback.

L’oggetto osservato deve fornire i metodi di register() e remove(), rispettivamente per effettuare e rimuovere la
sottoscrizione.

Questo pattern viene applicato quando l’astrazione ha due aspetti, l’uno dipendente dall’altro. Incapsulare questi
oggetti separati ci consente di variarli e riutilizzarli separatamente. Si applica quando un oggetto deve notificarne altri
senza conoscerli, dunque si vuole mantenere un disaccoppiamento tra osservato e osservatore.
L’oggetto osservato è il subject,
l’osservatore l’observer. Il
metodo di callback è l’update e
l’operazione di notify, nel
subject, invoche l’update di tutti
gli observer. In realtà abbiamo
subject e observer sono classi
astratte, di cui abbiamo le
sottoclassi concrete. Subject e
observer sono interfacce per gli
oggetti osservabili e osservatori
concreti.

17. Verifica e validazione:


Tramite la validazione constatiamo se il software rispetti le esigenze dell’utente, mediante la verifica i requisiti
specificati (output della fase di analisi e specifica dei requisiti). La prima attività risponde allora alla domanda are we
building the right software? e la seconda are we building the software right?

Un software è corretto se rispetta i requisiti specificati ma non è detto che un software corretto, che quindi supera la
verifica, risponda anche alle effettive esigenze dell’utente, superando anche la validazione. Il testing è un processo di
esecuzione del software al fine di scoprirne i malfunzionamenti (è differente dagli altri collaudi ingegneristici, spesso,
ad esempio, non è realizzato da terzi). Riprendendo la catena “fault-error-failure” il difetto nel codice, se attivato,
diventa un errore e, se questo si manifesta, un malfunzionamento. Secondo la letteratura più classica dell’ingegneria
del software tuttavia la catena sarebbe “error-fault-failure” dove con errore si intende l’errore umano che porta
all’introduzione del difetto nel codice. If(i=j) è un difetto del codice, se si attiva diviene un errore attivato e se si
manifesta una failure. Il difetto tuttavia è stato introdotto da un errore umano, dovuto a distrazione o ad ignoranza.
La causa ultima è la root cause.
Il testing fa manifestare i malfunzionamenti, dietro ai quali vi sono difetti che vanno rimossi. Per eseguire il testing il
sistema dev’essere disponibile ed eseguibile: il testing è una tecnica di programmazione dinamica. Talvolta può essere
invece necessario effettuare delle verifiche prima del tempo di esecuzione, per ispezione, peel review o con altre
tecniche non dinamiche. Tramite l’analisi statica del codice si prendono in esame le istruzioni del programma, senza
eseguirle e si analizzano. Si cercano violazione delle regole di programmazione, che possono essere indicative della
presenza di difetti. Viene analizzato un singolo modulo (che se invece volessimo eseguire dovremmo eseguire tutto il
programma), controlliamo che ogni variabile sia stata inizializzata, che non ci siano variabili dichiarate ma non definite
o definite ma mai usate. Un dato empirico dimostra che c’è correlazione tra la violazione delle regole di
programmazione e la presenza di difetti. Anche l’ispezione può essere applicata prima dell’eseguibile, possiamo
effettuarla sui requisiti, i documenti di progetto, codice sorgente..

Il debugging è il processo con cui, sapendo che vi soni malfunzionamento dovuti a difetti, andiamo a identificare dove
sia il difetto (fault localization) e a capire quale sia stato l’errore, per correggerlo. Letteralmente vuol dire “spulciare”
ed è un’attività distinta dal testing, così come lo sono tendenzialmente le persone addette.

Un test case è un insieme di input, condizione di esecuzione che devono verificarsi quando viene dato l’input e un
criterio pass/fail in base al quale stabiliamo se il test è passato con successo (pass) o meno (fail).

Se facciamo un test per cercare uno studente in un elenco l’input sarà la matricola, la condizione di esecuzione
sarà che lo cerchiamo tra gli studenti presenti (serve a chi fa il testing) e l’esito sarà pass se lo studente verrà
trovato.
Un test case specification è un requisito che dev’essere soddisfatto da uno o più casi di test. Un esempio potrebbe
essere una funzionalità di ricerca di uno studente tra molti altri: è logico che, essendovi più studenti, potremo cercarne
vari.

Una test suite è un insieme di casi di test.

La test execution è effettivamente il test, ovvero l’esecuzione di un caso di test (definito anche dopo aver definito i
requisiti) dopo l’implementazione. L’oracolo è la descrizione del comportamento atteso, che applichiamo a una test
execution per valutare, secondo il criterio pass/fail, se il comportamento osservato coincide con quello atteso.

Una test obligation è una specifica che richiede qualche proprietà ad alcuni casi di test che vogliamo effettuare.

Un criterio di adeguatezza è un predicato su una coppia (programma-test suite). Possiamo ad esempio voler fare un
test di una funzione che provi tutte le istruzioni, allora il criterio di adeguatezza è che bisogna essere sicuri di aver
eseguito tutte le istruzioni. Se non testiamo, ritornando all’esempio dell’if, l’istruzione if(i=j) non ci accorgeremo
dell’errore. Logicamente, non tutte le istruzioni vengano testate in un unico test ma di solito si esprime nella forma di
una regola per derivare un insieme di test obligation. Il criterio è soddisfatto se ogni test obligation è soddisfatta da
almeno un caso di test nella test suite.

Un test è buono se scopre un nuovo malfunzionamento. Utilizziamo il testing al fine di individuare i malfunzionamenti
del software, non basta che il un test buono individui un malfunzionamento qualunque perché se più test scoprono lo
stesso malfunzionamento oltre al primo risultano tutti inutili. Un test case è buono se ha un’elevata probabilità di
rilevare malfunzionamenti non ancora scoperti e dunque difetti alle loro spalle. In realtà quello che ha successo non è
il test case ma la test execution, tuttavia i test case vanno definiti a questo scopo.

Secondo la tesi di Dijkstra non si può dimostrare l’assenza totale di difetti in un software perché i test che
bisognerebbe eseguire sarebbero in numero troppo elevato, impossibile da eseguire, anche nel caso di un programma
semplice. Non esistono programmi senza difetti ma unicamente programmi di cui non abbiamo ancora scoperto
tutti i difetti.

Nonostante ciò il testing è importante in quanto dimostra indirettamente che il software rispetta le specifiche, dunque
è corretto, ed è la tecnica più usata per l’indicazione sull’affidabilità e la qualità globale del prodotto software.
Dobbiamo eseguire i test con le risorse di testing a disposizione: tempo, costo, numero di test case. Il costo è il costo
delle risorse umane necessarie. Il tempo è il costo del tempo dei tester ed essi eseguono i test definiti tra i test cases.
Dunque, la risorsa a disposizione è il costo, che nel caso di un software commissionato è pari al suo prezzo meno ciò
che si vuole guadagnare. Un manager addetto stabilirà le risorse o definendo un numero di casi di test o un numero di
giorni-uomo. La situazione tipica è questa in cui i test si stabiliscono sulla base delle risorse a disposizione ma non è
sempre la più adatta. Dalle risorse fissate deriva la qualità che si potrà ottenere ma molte volte è necessario procedere
all’inverso, partendo dalla qualità che si vuole ottenere e capire quali siano le risorse necessarie. Le risorse messe a
disposizione sono definite da un manager e si fa un best-effort, il meglio che si può per sfruttarle al massimo, tuttavia
ciò non è molto ingegneristico: si deve fare ciò che è necessario per raggiungere l’obiettivo fissato e non il meglio che
si può per poi vederne il risultato.

Empiricamente si stima che per ogni difetto rimosso viene introdotto circa un altro difetto, un difetto si rimuove
cambiando il codice ma facendo ciò è probabile creare un problema in un’altra parte. Il debugging (attività con cui si
localizza e rimuove il difetto) non è perfetto, solitamente. Lo è nel momento in cui rimuovendo un difetto non ne
introduce nessun altro.

Partendo dalle risorse fissate l’obiettivo è massimizzare i difetti trovati e rimossi: esporre quanti più difetti possibili,
definendo i casi di test così da scoprire quanti più nuovi difetti possibili e rimuoverli. Il problema può anche essere
visto come un problema di ottimizzazione: trovare il modo migliore di distribuzione delle risorse al fine di trovare
quanti più difetti possibili.
Se rappresentiamo nel tempo i difetti trovati (detected faults) dal testing la curva è la cosiddetta curva a ginocchio
(knee): a parità di sforzo nel primo intervallo vengono trovati sempre più difetti, la curva cresce drasticamente, poi
sempre meno fino ad arrivare ad una saturazione (è un andamento tipico di molti fenomeni: a parità di sforzo
diminuisce il guadagno marginale). Più test facciamo,
supponendo che ogni volta che troviamo errori questi
vengano rimossi, meno errori rimangono. Inoltre, più si
va avanti più difficile diventa trovare gli errori: i difetti
sono sempre più nascosti. La curva tende ad una
saturazione, la questione allora è quando fermarsi. Se
fissiamo le risorse fissiamo il costo e di conseguenza
fisseremo un tempo T, arrivati al quale ci fermeremo.
Dualmente se stabiliamo una qualità a cui vogliamo
arrivare la esprimeremo in termini della quantità di
difetti che vogliamo trovare (ad esempio il 90%), arrivati
alla quale ci fermeremo. Tuttavia, per la tesi di Dijkstra
non possiamo trovare tutti i difetti, dunque non
conosciamo il numero di difetti totale: come stabilire
quando siamo effettivamente al 90% se non conosciamo
i difetti rimanenti? Estrapoliamo la curva così da effettuare un’approssimazione sui difetti rimanenti, a partire dai
difetti trovati effettuiamo una stima su quelli residui, espressa con un valore e un grado di incertezza.

Gli istanti T1 e T2 sono le decisioni sul rilascio: release time. Possiamo prevedere il tempo necessario al testing ma è
difficile prevedere il tempo necessario al debugging, è difficile sapere quanto tempo serva a rimuovere i difetti (è
un’attività brain intensive). L’unico modo per prevederlo è tramite l’esperienza, registrando dati storici e analizzandoli.
I dati storici riguardano il processo e non il prodotto. I dati storici vengono archiviati sui defect management system,
anche detti issue o bug trackers, database sui quali vengono registrati i difetti, da chi sono stati trovati, la descrizione
del conseguente malfunzionamento e di come è stato rimosso. Ogni volta che il test trova un difetto deve infatti
documentarlo per poi comunicarlo all’addetto al debugging (che solitamente è un’altra persona). Un altro problema
quindi è quello di fissare il realease time (l’optimal release time) in funzione del numero di difetti che si vogliono
trovare, ovvero in funzione del numero di difetti residui. Per fare ciò si stima l’andamento della curva tramite modelli
matematici detti Software Reliability Growth Models (SRGM: modelli di crescita dell’affidabilità del software), che
rappresentano come l’affidabilità del sistema cambi man mano che i difetti sono scoperti e risolti. Questi modelli
estrapolano l’affidabilità in base ai dati correnti (sono necessari quindi dati statistici). La reliability (affidabilità) è la
probabilità che il software operi correttamente in un dato intervallo, ovvero la probabilità che non vi siano failure
durante l’intervallo: 1-Probabilità(fallimento). Il programma fallisce a fronte dei dati in input, il test case fa manifestare
un malfunzionamento, troviamo il difetto, lo rimuoviamo e se il debugging è perfetto quell’input non provocherà più
la failure: abbiamo ridotto i malfunzionamenti ed è cresciuta l’affidabilità.

Trovare pochi difetti in fase di test raramente è un bene, probabilmente i test fatti non erano adeguati. Decidere di
rilasciare il software solo dopo l’aver trovato una percentuale fissata dei difetti si fa, ad esempio, nei sistemi critici. In
generale in ogni caso i difetti non rimossi sono nel prodotto rilasciato, si manifesteranno quando il cliente lo utilizzerà
e se saranno molti non sarà soddisfatto.

Dunque, il processo del testing funziona in questo modo:


dopo l’analisi e la specifica dei requisiti vengono definiti i
casi di test; quando il prodotto è disponibile ed eseguibile
vengono effettuati i test e valutati i risultati a fronte di quelli
attesi. Ciò fornisce dati sul tasso degli errori per l’analisi
dell’affidabilità (che ce ne fornisce una stima) e gli errori
passano al debugging.
In generale la durata dell’esecuzione di un caso di test dipende dal software e da altre cose, per molti software buon
parte dei test può essere automatizzata, risparmiando molto tempo, per altri sistemi è più difficile. Tra l’altro per
eseguire i test spesso usiamo altri software, un problema molto rilevante è se questi hanno difetti. Nei sistemi critici è
necessario garantire che il software sia buono così come lo siano i software utilizzati per i test (ad esempio, serve un
compilatore certificato).

Tecniche di testing:

La prima distinzione che effettuiamo è tra tecniche black-box e white-box. Nelle tecniche black-box i casi di test sono
definiti a partire solo dalle specifiche funzionali. Nelle tecniche white-box i casi di test sono definiti a partire dalla
conoscenza del codice sorgente. Il test black-box è anche un test funzionale. Il white-box è anche detto strutturale ma
in realtà non sono esattamente sinonimi: il test strutturale si basa sulla struttura del codice e dunque è un test white-
box. Il test black-box viene condotto sulle interfacce software: vengono forniti dei dati in input, con le condizioni di
esecuzione, e si giudica se i dati output sono corretti in base al criterio pass/fail. Si realizza una tabella sulle cui righe
vengono posti i casi di test, sull’ultima colonna se questi sono passati o meno. I test black-box inoltre spesso riguardano
l’integrità dei dati persistenti.

L’output della progettazione è l’architettura del sistema software, fino ai moduli. Nel modello a cascata dopo la
progettazione abbiamo la fase di codifica e test dei moduli, poi l’integrazione e i test di integrazione. Parallelamente
abbiamo test di unità e test di integrazione. Il test di unità si effettua sul singolo modulo con l’obiettivo di trovare i
malfunzionamenti delle singole unità. Lo scopo del test di integrazione è invece scoprire i malfunzionamenti dovuti
all’interazione tra i moduli (può darsi che i moduli non interagiscano bene anche perché solitamente sono fatti da
diversi programmatori, che conoscono le interfacce ma non è detto che queste siano realizzate correttamente).
Tipicamente il test di unità viene fatto dal programmatore stesso dell’unità, quello di integrazione viene fatto quando
poi i moduli vengono messi assieme.

Il test di accettazione viene fatto in presenza del cliente, se c’è il committente (ad esempio, l’elettricista prova la
lampadina davanti al cliente) e se questo sarà soddisfatto pagherà. Se invece viene prodotto per il mercato lo farà
direttamente il cliente quando lo comprerà. Dunque viene fatto per verificare che il sistema risponda realmente alle
esigenze degli stackholders.

Prima del test di accettazione viene effettuato il test di sistema. Tramite l’integrazione vengono messi insieme i moduli
poi si effettua il test di integrazione, nel laboratorio. I computer dei laboratori saranno probabilmente veloci e potenti,
sennò vi sarebbe un problema di bassa produttività. Le macchine su cui dovrà effettivamente girare il software, dopo
il rilascio, avrà hardware, sistema operativo, memoria ecc. diversi e non sempre ciò è scritto sui requisiti, dunque
bisogna fare una prova anche di ciò. Potrebbe anche capitare che il software vada in conflitto con altri già installati
sulla macchina. L’integrazione viene fatta in laboratorio, l’accettazione sul campo ma per non fallire il test in presenza
del cliente è necessario il test di sistema. Il test di sistema viene effettuato nelle reali condizioni di esercizio o, se non
è possibile farvi accesso, in condizioni di esercizio riprodotte (magari anche in scala) e dunque realistiche.

• Test di unità:

Il test di unità viene effettuato al fine di rilevare i malfunzionamenti dovuti a difetti presenti nei singoli moduli. Il
modulo può essere un sottoprogramma, per testarlo scriviamo un programma chiamante che lo invochi. Ad esempio,
vogliamo testare una funzione di ordinamento, ci serve un programma che verifichi che lo effettui correttamente,
inserendo dei dati in ingresso: il driver. Il sottoprogramma potrebbe invocarne altri, tornando all’esempio
dell’ordinamento è probabile che invochi un’operazione di swap. Non è detto che i moduli chiamati da quello testato
siano disponibili, dovremo allora scrivere dei moduli fittizi che ne simulino il comportamento: gli stub. Dunque,
dobbiamo scrivere nuovo codice, driver e stub, che simuli rispettivamente i moduli chiamanti e il comportamento dei
moduli gerarchicamente inferiori. Nella programmazione a oggetti il modulo è tipicamente una classe, il driver
invocherà l’operazione esposta da una classe e i moduli stub saranno gli altri moduli, le altre classi, a cui la classe da
testare è collegata tramite una relazione d’uso.
Driver e stub vengono realizzati solo per effettuare il test dei moduli, non faranno parte del prodotto finale:
svilupparli è oneroso e non conviene sempre. Ci appoggiamo a questo strumento se abbiamo modularizzato il software
secondo il principio di alta coesione e basso accoppiamento, se l’accoppiamento è troppo alto gli stub sono molti e il
test sarebbe troppo oneroso, mentre se vengono rispettati entrambi i principi il numero di test case è minore, gli errori
sono facilmente individuabili e il numero di stub è minore. Tipicamente è il programmatore del modulo a testare lo
stesso ma vi sono anche altre soluzioni. Nelle agile methodologies (altro modello di ciclo di vita del software) i
programmatori lavorano a coppie (pair programming), come nello xtreme programming, in cui modulo per modulo i
programmatori si alternano, l’uno programma e l’altro esegue il test, in questo modo chi fa il test sarà sempre una
persona diversa dal programmatore della classe, ovvero il suo compagno nella pair programming.

In certi casi può non essere possibile effettuare il test di unità solo con l’ausilio di driver e stub ma è necessario qualcosa
di ancora non disponibile o la cui simulazione risulti troppo onerosa, il test di unità verrà posposto alla fase di
integrazione. Ad esempio, vogliamo testare un modulo che dovrà accedere ad archivi ancora non disponibili, o
rimandiamo il test all’integrazione o lo testiamo sulla memoria centrale ma in ogni caso, quando gli archivi saranno
disponibili, il test andrà rieseguito.

• Test di integrazione:

Il test di integrazione viene effettuato per individuare gli errori (in senso generico) relativi all’interazione tra i moduli.
Le strategie adottabili sono varie, il principio generale è quello di aggiungere un numero ridotto di moduli alla volta.
Faremo interagire un modulo, anziché con lo stub, con il modulo reale e vedremo se vi sono problemi nell’interazione:
lo scopo è trovare gli errori dovuti alla comunicazione effettiva tra i moduli (comunicazione che nei test di unità,
mediante gli stub, era fittizia). Gli eventuali problemi di comunicazione sono dovuti a problemi delle interfacce: un
modulo fornisce un servizio e chi lo riceve se lo aspetta diverso.

Un’altra cosa che si vuole scoprire, tramite questo test, sono i difetti legati agli effetti collaterali. L’interazione tra due
moduli può apparentemente avvenire senza problemi ma crearne altri che riscontreremo su altri moduli. Ciò capita
con l’utilizzo di variabili globali, ciò va limitato già in fase di codifica, limitandone l’uso e adottando opportune tecniche
di programmazione difensiva, rispettando l’information hiding e sfruttando l’incapsulamento. Se abbiamo un modulo
composto da più sottoprogrammi che comunicano tramite un’area dati comune dobbiamo preoccuparci di proteggerle
così che non siano utilizzati da altri.

Le strategie per effettuare i test di integrazione sono top-down o bottom-up, con varie alternative.

Secondo una metodologia top-down partiamo dal main, aggiungiamo M1, poi M2 e poi gli altri che si trovano allo
stesso livello, dunque sono direttamente chiamati dal main. Successivamente aggiungiamo i moduli invocati da M1,
ovvero M3 e M5, procediamo verso il basso ma un livello alla volta, secondo l’approccio breadth-first. Possiamo
altrimenti adottare l’approccio depth-first per cui procediamo dapprima verso il basso, integriamo M1, poi M3 e M4,
poi M5. Nel primo caso procediamo in ampiezza, ora in profondità. In generale la scelta dipende dalle necessità, è
comune anche un approccio misto. Il main verrà usato come driver, i moduli direttamente subordinati sono simulati
dagli stub e vengono man mano sostituiti con i moduli reali aggiunti. Gli stub sono gli stessi usati nel test di unità,
dunque non vanno scritti.

Esempio: per ordinare una lista di record dobbiamo effettuare l’overloading degli operatori di confronto e
assegnazione. Il programma ordinamento userà un programma di confronto e uno di scambio, li mettiamo
insieme e li integriamo in profondità, poi procederemo in ampiezza.

Se procediamo secondo una strategia bottom-up partiremo dal basso e metteremo insieme il programma principale
unendo i moduli. Nell’esempio dell’ordinamento partiremo dagli operatori (di confronto e assegnazione), passeremo
alle operazioni singole di confronto e scambio, infine arriveremo all’ordinamento.

Quando nell’approccio top-down sostituiamo di volta in volta un modulo con lo stub che lo simulava potrebbe essere
necessario effettuare un test di regressione. Se integriamo il main e M1 M2 sarà uno stub. Controlliamo il
funzionamento del main e di M1 e poi aggiungiamo M2 e testiamo l’interazione tra il main e M2. È necessario
effettuare il test tra il main ed M1 nuovamente, in quanto l’aggiunta di M2 potrebbe aver danneggiato l’interazione
tra i due. Il test di regressione è un test generale (fatto anche in casi diversi dall’integrazione) che mira a far verificare
il funzionamento dei moduli esistenti, a fronte delle funzionalità esistenti, quando modifichiamo o aggiungiamo una
funzionalità. Se rilasciamo la 9° versione di un software sarà dovuto a un cambiamento significativo rispetto all’8°,
come l’aggiunta di una nuova funzionalità, in generale vi sarà stata una manutenzione perfettiva. Testeremo
l’integrazione della funzionalità aggiunta con le preesistenti e poi faremo i test di regressione, ripetendo i test
effettuati per la versione precedente. Per effetto dell’aggiunta potrebbe esservi qualche vecchia funzionalità che ne
risenta. I casi di test che eseguiremo sono quelli già usati precedentemente. Tipicamente il test di regressione viene
effettuato quando si fanno nuovi rilasci.

Procedendo bottom-up mettiamo insieme i moduli di basso livello e poi risaliamo. Integreremo M4 e M3, poi M5 e
M1 perchè verosimilmente formano un sottoalbero che realizza qualcosa di coeso, ovvero una funzionalità (se
abbiamo usato l’astrazione sul controllo) o un’astrazione sui dati più complessa (se abbiamo usato l’astrazione sui
dati). Una lista di persone è fatta prima dalle persone e poi se ne fa una lista. Procedendo così non ci serve la
simulazione dei moduli gerarchicamente al di sotto ma dei moduli di livello sovrastante: i driver. Risalendo li
sostituiremo con gli effettivi moduli.

Volendo fare un confronto a livello generale il top-down ha lo svantaggio degli stub e il vantaggio di provare subito le
principali funzioni sul controllo; lo svantaggio del bottom-up è che il programma non esiste fin quando non viene
aggregato l’ultimo modulo (ovvero il main), il vantaggio è che i test case sono più semplici da progettare (sono in
numero ridotto).

Il test di integrazione fa notare inoltre gli effetti collaterali dovuti all’integrazione, ad esempio per l’uso di variabili
globali. Dichiariamo una variabile globale x condivisa da M1 e M5. Nel modulo M2 il programmatore definisce un’altra
variabile globale x, il problema verrà notato solo quando anche questo verrà integrato. Può capitare che M2 dovesse
utilizzare una variabile x definita in Mn, ma, omettendo la parola chiave extern, verrà chiamata la variabile definita in
M1, non protetta dalla parola chiave static. Dal test di integrazione notiamo questi errori. In realtà il difetto è interno
a M1 e Mn, avremmo dovuto capirlo anche prima, magari con il test di unità. Tuttavia, se il test è black-box non è a
conoscenza del codice, solo tramite un testo white-box scopriamo che in M1 vi è una variabile globale non protetta.
Potremmo altrimenti notarlo con un’altra verifica, tramite l’analisi statica del codice, anche tramite strumenti
automatici quali l’ASA (analisi statica automatica). Talvolta questa tecnica è preferibile in quanto vi sono alcune linee
di codice che, nel caso di una programmazione strutturale scorretta, potrebbe non essere mai raggiungibili. La
programmazione strutturale prescrive che ogni blocco sia one in one out, inserendo un break un’istruzione potrebbe
non venire mai eseguita. Per verificarlo è sufficiente utilizzare l’ASA. Ricordiamo che l’analisi, come il testing, fa sempre
parte delle attività di verifica. L’analisi è statica, il testing è dinamico.

In seguito al test di integrazione effettuiamo il test di sistema, per testare il sistema nelle reali o realistiche condizioni
d’esercizio in cui vi saranno altri elementi del sistema, come i dati reali che abbiamo migrato, ad esempio, dal vechcio
database. Useremo l’hardware target o una sua riproduzione. Solitamente non viene prodotto solo dall’ingegnere del
software ma anche da quello di sistema.
• Test di accettazione:

La produzione del software viene effettuata per un committente o per un mercato. Nel caso del committente
distinguiamo un committente esterno da uno interno (rispetto a chi produce il software). Il committente esterno è un
cliente, un committente interno è un’altra divisione della stessa azienda produttrice. Ciò che distingue profondamente
i due casi sono le modalità di interazione e dunque gli aspetti contrattuali, i tempi, le scadenze ecc. In seguito al test
di sistema eseguiamo il test di accettazione che, nel caso di una produzione per un committente, verrà effettuato in
sua presenza. Nel caso di produzione per il mercato non vi sarà alcun committente.

Il beta-test è un test di accettazione condotto dall’utente finale a cui lo sviluppatore, generalmente, non è presente.
Serve al produttore in quanto avrà un riscontro, a seguito del caso farà le opportune modifiche o meno, per poi
rilasciare il prodotto. Ovviamente il cliente che lo segue ha un costo, a fronte del quale gli vengono concessi dei
vantaggi. Questi possono essere o uno sconto sul prodotto finale o, se immaginiamo ad esempio di produrre il nostro
software per una casa automobilistica, il vantaggio d’anteprima: la macchina a avrà la funzionalità del bluetooth 6
mesi prima di tutte le altre.

Prima di fare ciò, dopo il test di sistema, facciamo utilizzare il software a uno dei nostri dipendenti, ottenendo altri
riscontri. Parliamo di alpha-test. E’ meno dispendioso in quanto il dipendente è pagato direttamente da noi. Avviene
presso lo sviluppatore, in ambiente controllato. Non siamo sempre in grado di eseguire un alpha-test, ad esempio se
stiamo producendo il software bluetooth per un auto, non potremo costruire un auto appositamente, se produciamo
un software come word sarà invece più fattibile.

A & T:

Le tecniche di analisi e testing (A & T) sono moltissime. Abbiamo la fase di planning, di verifica delle specifiche, di
generazione dei test, di esecuzione dei test e quella di miglioramento del processo. Nel processo abbiamo l’elicitazione
dei requisiti, la loro specifica, la progettazione di alto e basso livello, la codifica dell’unità, l’integrazione e rilascio e
infine la manutenzione:

- Planning: Pianifichiamo l’identificazione delle qualità e i test di accettazione all’inizio, i test di sistema invece
quando abbiamo specificato i requisiti. i test da fare sono stabiliti appena fatte le specifiche ma vengono
pensati già durante la specifica. I requisiti infatti devono essere testabili, se non siamo in grado di pianificare
un test non sono requisiti buoni. Pianifichiamo i test di unità e di integrazione durante la progettazione.
- Verifica delle specifiche: validiamo le specifiche durante la fase di specifica e anche un po’ oltre. L’ispezione
della progettazione architetturale viene fatta passo per passo. Ispezioniamo i documenti della progettazione
di basso livello, allo stesso modo, man mano che vengono prodotti. Controlliamo il codice mentre viene scritto.
- Generazione dei test: i test di sistema vengono generati dopo la specifica dei requisiti. In seguito alla
progettazione ad alto livello generiamo i test di integrazione mentre quelli di unità dopo la progettazione a
basso livello (riguardano infatti i singoli moduli, dunque più dettagliati).
Eseguiamo i test di unità appena dopo averli sviluppati. Gli oracoli vengono progettati appena dopo aver definito i test.
eseguiamo il test di integrità e dopo l’integrazione il test di sistema. Dunque, A & T accompagnano tutto il ciclo di vita
del software.

18. Test strutturale:


Nel test black-box testiamo le funzionalità a partire dai requisiti funzionali. Nel test white-box si parte dalla conoscenza
del codice, procedendo secondo qualche criterio. I due test sono l’uno il complementare dell’altro e non sono
alternativi: il test black-box non è infatti in grado di rilevare tutti gli errori.

Le funzionalità non hanno la stessa probabilità di essere eseguite o invocate, un criterio allora può essere testare
maggiormente le funzionalità più invocate. Per la tesi di Dijkstra, per quanto un programma sia breve, non è possibile
effettivamente eseguire tutti i percorsi, servirebbe un numero di casi di test che non siamo in grado di effettuare. Di
conseguenza, nel fare solo un sottoinsieme dei test possibili, alcune righe di codice potrebbero non essere mai
eseguite. Empiricamente si constata che la probabilità di trovare errori in un pezzo di codice è inversamente
proporzionale alla probabilità che quel percorso venga eseguito. Se un percorso è molto eseguito allora è più difficile
che vi siano errori (introdotti dal programmatore nel pensare l’algoritmo o nel codificarlo). Più sofisticata è una
funzionalità più complicata sarà pensarne l’algoritmo, inoltre sarà meno eseguita e testata. Ad esempio, se apriamo
un documento di Word, dovrà essere realizzata una funzionalità del tipo “file.open” ed è molto meno probabile che
contenga errori rispetto ad una funzionalità come la formattazione di un testo lungo, con simboli matematici. Tale
funzionalità ha un probabilità di invocazione minore rispetto all’apertura ma la possibilità che vi siano errori è
maggiore.

Per decidere quali test effettuare passiamo attraverso dei modelli del programma, il principale è il Control Flow Graph
(CFG): un grafo orientato che rappresenta il trasferimento del flusso di controllo tra blocchi di istruzioni. Il flusso di
controllo è realizzato dalle istruzioni di controllo (if, do-while, while-do, for, switch). Gli elementi del grafo sono:

- I nodi, che rappresentano i blocchi di istruzioni in sequenza, ove non vi sono istruzioni di controllo di flusso.
- Gli archi, che rappresentano il trasferimento del flusso di controllo, determinato dalle istruzioni di controllo di
flusso.
Ogni nodo rappresenta una componente one in one out, secondo i principi di una buona programmazione strutturale.
Nessuna decisione più interrompere la sequenza delle istruzioni incluse in un nodo.

Un altro tipo di grafo è il Call Graph (CG), in cui i nodi rappresentano i sottoprogrammi e gli archi le chiamate a
sottoprogramma.
Per ogni istruzione di controllo abbiamo un grafo:

- n if innestati: n+1 path


- n if in serie: 2n path
- n path in un ciclo ripetuto m volte: nm path

I percorsi saranno sempre troppi per essere completamente testati, dunque dobbiamo scegliere quali test fare e da
questo punto di vista il CFG è molto utile. Bisogna testare maggiormente i moduli più complessi, ma per individuarli è
necessaria una metrica di complessità. Introducendo il grafo possiamo scegliere come tale il numero di percorsi.
Possiamo decidere di fare i test in base ad un criterio, ad esempio eseguiamo un test che solleciti i nodi, i blocchi di
istruzione. Allo stesso modo possiamo decidere di testare gli archi.
La complessità risiede negli aspetti strutturali ma anche negli aspetti dinamici. Nel nostro caso, per la dinamica la
complessità riguarda il diagramma di sequenza, per gli aspetti strutturali quello delle classi. Un criterio che adotteremo
nel diagramma delle classi sarà quello di alta coesione e basso accoppiamento, se ci sono troppi archi non avremo
rispettato il principio di bassa coesione. Un altro criterio di complessità può essere considerato lo sforzo necessario a
capire qualcosa.

Nota: la dimensione spesso suggerisce la complessità ma non sempre ne è un indice. Il numero di righe di
codice, ad esempio, non va inteso come metrica di complessità ma come una metrica dimensionale del codice
sorgente.

Misurare la complessità è utile in quanto se un programma è più complesso è più probabile che ci sia un difetto e
questo dovrà essere messo in luce tramite l’attività di testing. Se definiamo i test a partire da risorse fissate il problema
è di ottimizzazione: cerchiamo di far manifestare il numero maggiore di malfunzionamenti mediante quelle risorse. Se
invece, al contrario, fissiamo la qualità da raggiungere il numero di test verrà di conseguenza. Cercheremo di fare più
test laddove la probabilità di scoprire un malfunzionamento sarà maggiore (ricordiamo infatti che un test è buono se
rileva un malfunzionamento non ancora osservato), non è detto che esso sia il modulo con più righe di codice.
In un intervallo finito [0,T] le funzionalità, dal punto di vista dell’invocazione, non sono equiprobabili, alcune saranno
chiamate più spesso. Oltre a ciò dobbiamo però considerare che anche la probabilità che ciascun difetto si manifesti è
differente e, soprattutto, può accadere che un difetto situato in una funzionalità con alta probabilità di invocazione
abbia una scarsa probabilità di occorrenza. L’affidabilità è 1-ProbabilitàDIFallimento e la probabilità di fallimento è
legata alla frequenza con cui invochiamo un’operazione ed è legata allo stesso modo all’inaffidabilità (1-affidabilità), a
cui potrebbe contribuire maggiormente un difetto con scarsa probabilità di essere invocato ma con altra probabilità
di occorrenza. Dobbiamo quindi pesare la probabilità di invocazione di una funzione con la probabilità di
manifestazione di un difetto al suo interno.

Criteri di copertura:

Il primo criterio è il criterio di copertura degli statements, per cui dobbiamo eseguire ogni istruzione almeno una
volta. L’if è una sola istruzione ma comporta due rami, per testarli entrambi ci servono due test case ma, secondo
questo criterio, essendo un’unica istruzione, serve un test. Se eseguissimo due test non staremmo applicando il criterio
di copertura delle istruzioni bensì dei rami.

Il secondo è il criterio di copertura delle decisioni: l’if ha una condizione booleana, le possibili decisioni possono essere
due: true o false, faremo due test. Ogni arco del controllo viene eseguito almeno una volta, si effettua un numero di
test pari al numero delle decisioni. La copertura delle decisioni implica quella degli statements. Se la condizione fosse
composta da più condizioni in ogni caso le possibili decisioni sarebbero due. Consideriamo il seguente esempio:

i test considerati verificano solo la condizione dell’y. Una condizione composta dall’OR è vera se è vera almeno una
delle condizioni componenti. in questo caso non viene mai effettuato il test per x=0, in ogni caso sono stati effettuati
i due test che verifichino il comportamento nel caso in cui la condizione globale sia vera e nel caso in cui sia falsa.

Il criterio di copertura delle condizioni richiede che ogni singola condizione debba essere sia vera sia falsa per i diversi
dati di test. In questo esempio allora una volta porremo in ingresso la x corretta e la y no e poi faremo il viceversa. In
questo modo però la condizione globale sarà in ogni caso versa, dunque, il criterio di copertura delle condizioni non
implica quello delle decisioni, sono da effettuarsi entrambi, abbiamo allora 2n casi di test, quante tutte le combinazioni
possibili: il numero di casi di test cresce esponenzialmente.

Il criterio di copertura delle condizioni e delle decisioni modificato (MC/DC) non è di facile applicazione ma è
vantaggioso in quanto riduciamo il numero di complessità rendendo lineare la crescita del numero di casi di test,
rispetto al numero di percorsi. Secondo questo criterio non testiamo tutte le condizioni ma solo quelle rilevanti, ovvero
le decisioni che influenzano indipendentemente l’esito di una condizione composta. Individuare tali condizioni è
un’attività brain-intensive, non automatizzabile, di conseguenza non è un criterio di applicazione immediata ma
permette di ridurre notevolmente il numero di casi di test. Tale criterio è implicato dal criterio delle condizioni
composte ed implica tutti gli altri. È dunque un buon compromesso tra completezza e numero di casi di test.

Esempio: Se a è vera, b può assumere qualsiasi valore affinché (a||b) risulti vera. Invece, c deve essere vero, d può
assumere qualsiasi valore ed e deve
essere vero, affinché tutta la
condizione risulti vera. Vado poi a
sottolineare nei primi due test case
quello che hanno contribuito ad una
novità: nel primo erano tutti essenziali,
nel secondo caso di test quello che
contribuisce è b, mentre c non è più
una novità. Questo significa che nel
secondo caso, affinché la condizione
risulti vera, non posso fare a meno di
b, e non di c. se volessi far dipendere la validità delle tre parentesi dall’or allora d deve essere vero, mentre a, b e c
possono assumere qualsiasi valore. Questo significa che nel terzo caso di test dovrei inserire tutti puntini sospensivi,
ma mettendo c come false considero una circostanza in più. Ecco perché alla fine bastano soltanto sei casi di test per
verificare questa espressione. La complessità è lineare: n+1 casi di test per n condizioni. Quest’attività è sicuramente
più brain intensive, ma in questo modo testo le condizioni rilevanti, dove ognuna influenza indipendentemente l’esito
della decisione. Ad esempio, in ambito avionico questo criterio è richiesto.
Per concludere il grafo è importante in quanto ci permette di definire una metrica di complessità: il numero
ciclomatico o numero di Mc Cabe, ovvero il numero dei cammini indipendenti nell’insieme di base di un programma.
Tale numero è legato al numero di test che dobbiamo effettuare, ovvero almeno quanti i cammini indipendenti.
Possiamo calcolarlo in tre modi:

- numero di regioni del grafo


- A-N+2C, dove A è il numero di archi, N il numero di nodi e C è il numero ciclomatico del nodo esploso.
- P+1, dove P è il numero dei nodi predicato (quelli in cui si valuta una condizione e da cui escono più archi).

N.B.: lo switch conta n-1 nodi predicato.

Il numero di Mc Cabe fornisce una metrica di complessità e individua un numero minimo di test da eseguire, tuttavia
non indica quali siano tali test.

Quando si verifica un’eccezione si interrompe lo stato del sistema, bisogna riportalo ad uno stato integro,
tramite la gestione delle eccezioni. Supponiamo di voler effettuare una transazione bancaria. Vi sono due conti
(probabilmente relativi a banche diverse) e un importo, il programma sarà:

CL=CL-I;

ES=ES+I;

La transazione deve rispettare le proprietà ACID (atomicità, consistenza, isolamento e persistenza). Per
l’atomicità una transazione dev’essere eseguita come un’unità atomica, non interrompibile. Se infatti si esegue
solo la prima istruzione e non la seconda CL versa un importo che ES non riceve. Se i due dati sono su database
diversi la consistenza dev’essere globale: la somma di CL e ES prima della transazione dev’essere pari a quella
finale. In generale ogni proprietà si esprime con un predicato (secondo l’algebra di Boole), nello scenario di un
caso d’uso verrà espresso con pre e post condizioni da verificarsi.
La proprietà di consistenza esprime il principio di conservazione, il sistema dev’essere a divergenza nulla,
altrimenti si creerebbe o perderebbe denaro. Se si creasse con l’accumulo continuo il sistema scoppierebbe.

In seguito a un’eccezione riportiamo il sistema in uno stato safe, tramite il blocco catch, in modo da non
perdere il flusso di controllo.

Un modulo dovrebbe avere un numero ciclomatico pari a 10/15. Il numero ciclomatico viene calcolato con strumenti
automatici, anche con alcuni open source. Ciò stabilisce quanti test si debbano effettuare ma non quali siano. Per
rispettare il principio di alta coesione e basso accoppiamento non dev’essere troppo complesso, altrimenti farà troppe
cose, dunque non sarà coeso.

Talvolta poi effettuiamo dei test di robustezza, inserendo in input dei dati scorretti, ad esempio proviamo a far lavorare
un’operazione che opera su un vettore di n elementi su uno di n+1 e vediamo cosa accade, ovviamente non possiamo
confrontare il risultato con nessun oracolo in quanto non vi sono risultati attesi specificati (altrimenti non sarebbe un
test di robustezza).

Testing dei cicli:

se abbiamo un ciclo cerchiamo di testare i casi in cui viene eseguito:

- 0 volte
- una sola volta
- m volte, con m<n (max iterazioni)
- n-1 volte
- n volte
- n+1 volte (per assicurarci che dopo il numero max di iterazione si concluda effettivamente)
I cicli possono inoltre essere innestati, se procedessimo come per i cicli singoli il numero di casi di test crescerebbe con
progressione geometrica. Iniziamo allora dal ciclo più interno, fissando gli altri al valore minimo, e effettuiamo i test
dei cicli semplici. Passiamo poi al ciclo immediatamente più esterno, lasciando gli altri al valore minimo e ponendo
quello interno, appena testa, ad un valore tipico (intermedio). Continuiamo così fino a quando non avremo testato
tutti i cicli.

19. Test funzionale:


Il test funzionale si basa sulle specifiche funzionali, descrizione tendenzialmente informale di ciò che è atteso dal
software. Tipicamente è il black-box testing o specification-based testing. Con funzionale non si intende che testi le
funzionalità ma che sia strutturato in base a ciò che è espresso dalle specifiche. È complementare al test white-box in
quanto ci permette di individuare errori che con esso non potremmo scoprire: errori dovuti alla logica mancante,
dunque parti di codice, relative a funzionalità, mancanti o incomplete (e dunque non testabili dal white-box).

È un test economico, in quanto non richiede la conoscenza del codice, ed è applicabile dal test di unità al test di sistema.
Possiamo definire i test funzionali immediatamente dopo le specifiche e sono un’ottima tecnica di raffinamento delle
stesse. L’analista specifica i requisiti e li passa a chi progetta i casi di test (se sono progettati immediatamente dopo e
non rimandati alla fase di codifica). Quanto più saranno accurate le specifiche tanto più potranno esserlo le test suite,
analogamente se non lo saranno il progettista dei casi di test troverà difficoltà e chiederà all’analista di rivedere i suoi
requisiti. Un buon requisito è infatti un requisito che sia testabile ma non sempre chi si occupa delle specifiche riesce
a pensare al testing, spesso non vengono fatti gli scenari o non vengono inserite precondizioni e postcondizioni, che
in particolare sono quelle che ci dettano un oracolo. La progettazione dei casi di test dopo le specifiche è quindi
importante in quanto, tramite un feedback, porta al miglioramento di esse.

Individuare un difetto è come “trovare un ago in un pagliaio”. Nella metafora l’ago è il difetto ed il pagliaio è l’insieme
di tutti i percorsi. Una prima tecnica può essere cercarlo a caso, tramite il random testing, ovviamente se siamo più
tester ci suddivideremo ed effettueremo la ricerca su zone diverse. individuiamo, nello spazio degli input, i dati da
fornire, quindi i test case, scegliendoli casualmente, secondo una distribuzione di probabilità uniforme. La probabilità
con cui verrà scelto un caso di test nello spazio degli input è la stessa per tutti i casi di test. Con l’assunzione che
effettivamente gli input all’interno del loro spazio siano equiprobabili questa tecnica di testing non è inferiore alle
altre. Nella pratica gli input non sono sempre equiprobabili ma, soprattutto, la distribuzione dei difetti non è uniforme.
I valori di fallimento non sono distribuiti uniformemente nello spazio degli input, anzi, tendenzialmente, essi si
trovano in intorni densi di punti.

Dobbiamo cercare di migliorare la probabilità di trovare il difetto, ovvero di esporre il malfunzionamento ed


individuarne il difetto. È necessario un
test più sistematico. Tramite il random
testing procediamo uniformemente,
evitando polarizzazioni: consideriamo
ogni input allo stesso modo trascurando
la conoscenza, che invece potrebbe
pilotare gli input così da aumentare la
probabilità di scovare il difetto. Il test
sistematico si basa sul partizionamento
degli input. Suddividiamo l’insieme degli
input in partizioni, ogni quadratino è un
insieme di dati che può o meno causare
un fallimento (in caso affermativo sono
colorati di nero). Scegliamo gli input in
base alle conoscenze dell’applicazione, i
difetti tendono ad addensarsi vicino ad alcuni punti dunque conviene scegliere i campioni di test (parliamo di campioni
in quanto, per la tesi di Dijkstra, non potremo testare l’intero insieme dei casi di test) oculatamente, così da
massimizzare i difetti trovati. Dunque, partizioniamo l’insieme “infinito” dei casi di test (che in realtà non è infinito,
ma in ogni caso è un numero tropo elevato) in un insieme finito di classi, a due a due disgiunte, la cui unione è l’intero
spazio. In realtà di parla di Quasi-Partition Testing, in quanto le classi tendono a sovrapporsi.

Effettuiamo le partizioni sulla base di qualche criterio, nell’ambito del test funzionali si parte dalle specifiche funzionali,
sulla cui base partizioneremo gli input: Specification-based Partition Testing. Il vantaggio è che se si ottengono
partizioni con un numero elevato di difetti, e provando ad effettuare almeno un test per partizione, con un solo caso
di test individueremo tutti i difetti presenti nella singola partizione. I test funzionali sono condotti sulle interfacce
software, verifichiamo che i dati di input siano accettati in modo adeguato, i dati di output siano corretti e che si siano
verificate le post-condizioni previste, come l’integrità delle informazioni esterne, in archivi di dati (persistenza).

Il dominio dei dati input viene suddiviso in classi che rappresentano un insieme di stati validi o non validi per le
condizioni in ingresso. Un test case ideale rileva da solo tutta la classe di errori legata alla classe dei dati in input, per
ogni classe di dati di input. In generale una condizione di input può essere un valore specifico, un intervallo di valori,
un insieme di valori o una condizione booleana.

A partire dalle specifiche funzionali individuiamo delle features (caratteristiche) testabili indipendentemente dalle
altre, specifichiamo i casi di test secondo diverse tecniche.

- Ci serviamo di un modello, dunque parliamo di testing funzionale model bases;


- Cerchiamo di individuare valori rappresentati degli ingressi per ogni feature.

In questo modo otteniamo solo la specifica dei casi di test, e non i casi di test stessi, essa è generale e può essere
soddisfatta da più casi di test, derivati da essa.

Esempio CAP: consideriamo un’applicazione di ricerca di codici postali. L’input è singolo, dunque non va
partizionato, l’output è l’elenco delle città (o località) caratterizzate dal codice postale inserito. Un codice
corretto è una combinazione di 5 cifre decimali, dunque abbiamo 105 possibili combinazione, testarle tutte è
inutile in quanto non tutte sono rappresentative. Valori rappresentativi dell’insieme dei valori corretti possono
essere: cap per cui esiste una sola località, cap per cui non ci sono località, cap per cui ve ne sono di più. Altri
valori rappresentativi fanno parte dell’insieme dei valori non corretti: stringa vuota, stringa con una cifra in
più, stringa con una o più cifre in meno, stringa con molte cifre, stringhe con caratteri che non siano cifre.
Dunque, abbiamo individuato due classi di input (corretto e non corretto), ciascuna con sottoclassi. Abbiamo
definito le specifiche dei casi di test, da cui definiremo i casi di test stessi.

Una volta individuati i valori rappresentativi degli ingressi (quindi data la specifica), possiamo procedere in due modi.
Se abbiamo più di un input è possibile che un input di valori possibili in un campo siano collegati ad un altro insieme
di valori possibili di un altro campo, ma che non tutte le combinazioni abbiano effettivamente senso. Cerchiamo allora
di ridurre il numero di casi di test fissando dei vincoli. Questa tecnica di definizione dei casi di test è detta category
partition testing. Un caso particolare è quello in cui proviamo i valori di input a coppie, considerando coppie di dati
input: si parla di pairwise testing.

Il category partition testing è un tecnica di testing combinatoriale in quanto struttura le specifiche di test in un insieme
di proprietà o attributi che possono essere sistematicamente variate in insiemi di valori. Un grosso vantaggio è che i
test possono essere automatizzati facilmente.

Esempio browser-OS:Supponiamo di avere un’applicazione browser che vogliamo testare su diversi ambienti,
indipendentementeda OS e browser. L’applicazione deve poter girare su Internet Explorer e Firefox, come
sistemi operativi su Windows (Vista e Xp) e Linux. Non tutte le combinazioni sono sensate: IE può girare solo
su Windows, non su Linux. Troviamo delle configurazioni e poi le testiamo indipendentemente, dunque in
questo caso la feature è la combinazione tra browser e sistema operativo.

Nel category partition testing individuiamo manualmente gli attributi caratterizzanti lo spazio degli input e
introduciamo dei vincoli così da eliminare le combinazioni non sensate, riducendo i casi di test. Come feature testabili
indipendentemente possiamo scegliere le singole funzionalità, possiamo testare ogni caso d’uso indipendentemente
dagli altri. Per ciascuno di essi individuiamo i parametri e gli elementi del sistema. i parametri descrivono la
funzionalità, gli elementi del sistema sono altre entità da cui dipende la funzionalità. Per ogni parametro e per ogni
elemento di sistema individuiamo le caratteristiche elementari (categorie) e di ciascuna di esse i le classi di valori
rappresentativi (valori normali, valori speciali, valori limite e valori errati). Infine, introduciamo vincoli sulle singole
categorie o sulle loro combinazioni.

Abbiamo un negozio di vendite di computer online che consente di comprare un computer assemblando i singoli pezzi.
Ovviamente bisogna controllare che i computer assemblati funzionino, bisogna imporre combinazioni funzionanti, in
quanto non tutte le combinazioni di input validi sono sensate. Ci sarà una funzionalità ControllaConfigurazione che si
assicurerà che la configurazione sia effettivamente valida. Di ogni funzionalità dobbiamo individuare parametri ed
elementi di sistema. i parametri saranno il modello del computer e l’insieme dei componenti. l’elemento di sistema da
cui dipende la funzionalità è il database su cui sono registrati i prodotti in vendita (non potremo provare a comprare
un prodotto non presente sul database). Dobbiamo poi individuare le caratteristiche di parametri ed elementi. L’app
prevede che le caratteristiche di ogni modello siano il numero di dischi, numero di slot necessari, numero di slot
opzionali ecc.. la funzionalità ControllaConfigurazione si informerà che se compriamo 3 dischi per un modello che ne
prevede 2 il terzo non potrà essere usato, altrimenti che non potremo usare il modello scelto se esso prevede un
numero di dischi e ne abbiamo selezionato un numero inferiore. Individuiamo poi le categorie dell’insieme dei
componenti, studiando la coppia (slot,componente). Ogni coppia è vincolata al modello che si desidera assemblare e
alle incompatibilità che possono esistere tra componenti diversi. Le categorie possono essere numero di componenti
richiesti, numero componenti opzionali, corrispondenza del componente con gli slot del modello.. . Definiamo infine
le categorie dell’elemento di sistema 8database): numero di modelli presenti, numero di componenti presenti.

Non esistono scelte predefinite/univoche per la scelta delle categorie.

A questo punto dobbiamo identificare i valori delle categorie (valori normali, speciali, di errore, di confine):

Tramite il calcolo combinatorio siamo in grado di contare quanti


test dobbiamo effettuare con il test combinatoriale, ovvero in
numero pari alla cardinalità del prodotto cartesiano dei valori delle
categorie. Nel nostro caso abbiamo 7 categorie con 3 classi di
valori, 2 con 6 classi e una con 4 classi: 37x62x4=314.928 casi di test.

È inutile però effettuare test che solo teoricamente potrebbero


avere senso ma non nella pratica. Introduciamo dei vincoli sugli
input, ad esempio, non ha senso combinare il caso 0 slot richiesti
del modello con il caso “componente incompatibile” dell’insieme dei componenti. introducendo i vincoli allora
riduciamo il numero di casi di test. Abbiamo 3 tipi di vincoli.

I vincoli error [error] indicano una classe di valori che da luogo ad un errore, ipotizziamo che sia sufficiente un solo
errore per combinazione per osservare un potenziale fallimento del sistema. se etichettiamo il caso numero modello
malformato con error vuol dire che, sebbene i valori malformati possibili siano moltissimi, testeremo un solo valore
malformato. Stiamo in un certo modo scommettendo che se il programma ha un difetto, che causa un
malfunzionamento, mettendo in ingresso un numero malformato si manifesterà un fallimento qualunque sia il
numero. Non è detto sia vero ma è probabile. Nel modello allora prima avevamo 3 classi per numero di modello, che
contribuivano a 37. Per il caso malformato ora però non faremo più tanti test quante le combinazioni con le altre
categorie, uno solo. Dunque, passeremo da 37 a 36 e aggiungeremo un caso di test. Con tutti i vincoli error esposti
passiamo a 2.711 casi di test, abbiamo ancora solo un sottoinsieme dei casi di test ma è scelto in maniera oculata, così
da rilevare i malfunzionamenti.

Il vincolo single [single] produce lo stesso effetto del vincolo error ma è concettualmente diverso, indica una classe di
valori che si desidera testare una sola volta e non su tutte le possibili combinazioni, per ridurre il numero di test case.
Ad esempio, il numero di modelli nel database pari a 1 non è una situazione scorretta ma è anomala, vogliamo testarla
solo una volta e la etichettiamo con single. Anche ora abbattiamo un fatto e aggiungiamo un caso di test.

Il vincolo property ([property],[if-property]) è un vincolo tra gli ingressi, ci consente di eliminare le combinazioni
invalide di valori delle categorie. Ad esempio, in numero di componenti richiesti il valore “= numero slot richiesti” ha
senso se il numero di slot richiesti vale molti. Introduciamo questo vincolo assegnando un nome alla proprietà RSMANY
con cui etichettiamo il valore “molti” in numero slot richiesti. Etichettiamo “=numero slot richiesti” con l’etichetta [if
RSMANY]. Riduciamo il caso di test perché ora il valore “=numero slot richiesti” non verrà provato con tutti i valori di
numero slot richiesti ma solo con “molti”.

Pairwise combinatorial testing: il pairwise è un altro tipo di testing combinatoriale. Tramite esso testiamo tutti i
possibili valori degli input, considerandoli a coppie (in generale anche a n-uple). Questo tipo di testing è reputato
efficace in quanto tipicamente possiamo osservare che alla base dei maggiori casi di fallimento ci sono combinazioni
di pochi input, gestiti male dal programma. Se consideriamo una funzionalità con 5 input i difetti raramente saranno
sul trattamento di un singolo input o di una particolare combinazione dei 5, più probabilmente il programmatore avrà
gestito male combinazioni di coppie di input, al più triple. Sotto questa assunzione risulta allora inutile andare a testare
varie combinazioni, si prova una volta ciascuna coppia.

Consideriamo un software che


permetta di impostare lo schermo.
Possiamo impostare i parametri in
figura, le possibili combinazioni
sono 432 test case (33x42), tuttavia
non tutte hanno senso. Non ha
senso impostare la modalità true-
color con display text-only.
Considerando tutte le
combinazioni ci sono coppie
che si presentano più volte,
il test pairwise ci richiede di
testarle solo una volta.
Ricaviamo allora, anche
automaticamente, 17 casi di
test, riducendone
notevolmente il numero.
Infine, anche in questo test
abbiamo la possibilità di
introdurre i vincoli
precedentemente visti.
20. Test model-based:
Un metodo di derivazione della specifica dei casi di test, alternativo al category e pairwise partition testing è il test
model based. Mediante questa tecnica verifichiamo il comportamento atteso del programma mandato in esecuzione
non più rispetto alle specifiche ma alla predizione fatta attraverso un modello formale. Consideriamo un’applicazione
orologio che ci consente di visualizzare l’ora in formato digitale o analogico. È facile modellare un programma del
genere tramite una macchina a stati.

Anche in questo caso sono diversi i criteri con cui decidiamo i casi di test da effettuare. Un criterio potrebbe essere
richiedere che tutti gli stati vengano testati, un altro che tutte le transizioni siano percorse. Un altro criterio può essere
eseguire una random walk, un cammino casuale lungo il diagramma, analogamente al random testing. Il principio su
cui si basa è che i difetti siano equiprobabili ma abbiamo visto che tendenzialmente non è così. Potremmo scegliere di
fare la sequenza più breve che ricopra tutti gli stati, diminuendo il numero di input così da ridurre il tempo del test, in
particolar modo se non viene svolto in maniera automatica.

Generare i casi di test da un modello è semplice, il costo è ridotto ed è un’operazione facilmente automatizzabile.
Stiamo confrontando il comportamento del sistema con quello descritto dal modello, dobbiamo allora assicurarci che
il modello rispecchi adeguatamente la realtà. Effettuiamo un test di conformità con cui verifichiamo che il modello
catturi correttamente il comportamento del sistema. Per validare il modello confrontiamo il comportamento con
quello del sistema reale, in casi noti. Nei casi non noti non sappiamo come si comporti il sistema, dunque non possiamo
determinare se il modello risponda adeguatamente. Se il modello viene costruito su una base numerosa di casi noti è
più probabile che rispecchi correttamente il sistema originario, anche nei casi non noti. Allo stesso modo se i casi noti
su cui si basa sono pochi è probabile che il modello si discosti dal sistema che vuole rappresentare per tutti gli altri
casi.

Come macchine a stati abbiamo visto i diagrammi di stato (del controllo e del protocollo), da questi è facile generare
automaticamente i test, a partire dal comportamento della macchina a stati.

21. Modelli di qualità del software:


Un modello di qualità del software definisce un insieme di attributi di un prodotto software, esaustivi e privi di cicli
di sovrapposizioni, e le dipendenze logiche esistenti tra di essi. È un modello gerarchico degli attributi di qualità del
software che, generalmente, possiamo differenziare a seconda che interessino chi compra o chi produce il software.
Tuttavia, gli attributi di qualità che riguardano il suo sviluppo influenzano gli attributi che interessano a chi utilizza il
software. Un esempio di attributo di qualità relativo a chi sviluppa il software è la manutenibilità, alla nascita di nuovi
requisiti (o alla modifica di requisiti preesistenti) se il software non è manutenibile dovremo effettuare lavori molto
costosi, che probabilmente non verranno commissionati.

Produrre in qualità vuol dire controllare il processo produttivo, avere skills, personale e regole con cui controllare cosa
fare, soprattutto quando gli esisti risultano negativi (non conformità). Qualunque sistema può essere messo in qualità,
dapprima definendo gli obiettivi e poi le politiche di gestione del controllo e delle non conformità. La qualità è
standardizzata dall’ISO 9000, una famiglia di standard riguardante la qualità della produzione di beni e servizi, di ogni
settore. Per il software esiste una sotto-famiglia specifica date le particolari caratteristiche, discendenti dalla propria
immaterialità. I modelli di qualità del software sono gerarchici, a n livelli.

- Il primo livello descrive un insieme di caratteristiche che rappresentano la qualità del prodotto secondo diversi
punti di vista (le caratteristiche sono astratte e qualitative);
- Il secondo livello descrive le sottocaratteristiche, quantitative e misurabili.

Ci servono metriche per valutare quanto un prodotto soddisfi le caratteristiche.

Le norme per il software sono:

1. ISO/IEC 9126-1: Definisce 6 caratteristiche di qualità principali e 27 sottocaratteristiche misurabili tramite


metriche fornite nel ISO/IEC 9126-2, 3 e 4.
- Part 1: caratteristiche e sottocaratteristiche
- Part 2: metriche per la misura della qualità esterna
- Part 3: metriche per la misura della qualità interna
- Part 4: metriche per la misura della qualità in uso.
2. ISO/IEC 9241: Definisce le caratteristiche dell’usabilità (che come abbiamo visto, essendo una qualità
soggettiva, non è facile da misurare);
3. ISO 12119: Definisce le caratteristiche di qualità di un software Commercial off-the-shelf” (COTS, come Office
o i sistemi operativi)

La norma 9126 definisce un modello a 4 livelli. Il primo riguarda i punti di vista delle qualità del software (esterno,
interno ed in uso). La qualità esterna esprime il comportamento dinamico del software nell’ambiente d’uso (non
necessariamente dal punto di vista dell’utente), la qualità in uso esprime l’efficacia ed efficienza con cui il software
serve le esigenze dell’utente ed è correlata alla sua percezione diretta (è efficace se soddisfa l’esigenza o meno,
soprattutto come; è efficiente se fa un uso adeguato delle risorse). Gli attributi interni sono invece statici, sono visti
indipendentemente dall’esecuzione
del software, sono qualità intrinseche
del prodotto. Nel secondo livello
vengono descritti i principali attributi,
secondo i 3 punti di vista; nel terzo le
sottocaratteristiche e nel quarto le
metriche per misurarle.

Qualità interne, esterne e in uso


riguardano il prodotto. Infine, abbiamo
qualità sul processo, che a loro volta
influenzano quelle sul prodotto. Allo
stesso modo la qualità interna influenza
quella esterne che influenza quella in uso.
Durante il processo per una produzione di
qualità dobbiamo confrontare ciò,
raccogliendo dati, effettuando misurazioni
e controllando punti di non conformità ed
eventuali sprechi. Più il processo sarà
controllato maggiore sarà la probabilità di
produrre un software di livello.
La qualità del processo contribuisce a conferire i livelli di qualità desiderati nel prodotto ed interessa di per sé chi
produce e alcuni stackholders esterni (come chi commissione o chi certifica). La certificazione è fondamentale in alcuni
sistemi, come quelli critici. Gli standard relativi a tali sistemi prevedono attività da svolgere all’interno del processo:
bisogna eseguire combinazioni di attività tramite cui si da evidenza del raggiungimento di certi livelli di qualità. È
fondamentale dare evidenza dell’aver seguito un processo i cui prodotti siano stati atti a dare un ragionevole, o più,
livello di confidenza sulla qualità finale del prodotto, che nei sistemi critici riguarda in particolare l’affidabilità
(reliability, tolleranz ai guasti, dependability, capacità di ripristino ecc..). Dunque, la qualità va monitorata e misurata
lungo tutto il processo, interessa chi certifica ma anche chi produce così da controllare il proprio processo in maniera
quantitativa e prestare attenzione ai punti di forza e di debolezza. In questo modo si è in grado di definire azioni per
ottimizzare il processo. Il produttore è interessato a migliorare la qualità del proprio processo per ridurne il costo:
rimuovere molti difetti è costoso, soprattutto se individuati troppo dopo l’introduzione, e può far andare in perdita.
Oltre al costo viene influenzato il tempo, altro fattore importante.

Vi sono varie tecniche di verifica del processo e del prodotto, in particolare nei sistemi critici le norme non dicono le
attività da svolgere in maniera circostanziata ma impongono di dare determinate evidenze attraverso una
combinazione di attività scelte. Ad esempio, in fase di codifica scriviamo il codice sorgente, quello che verrà poi
effettivamente fatto girare sarà il codice eseguibile ma come assicurarsi che, pur avendo rimosso tutti i difetti, ciò che
viene eseguito corrisponda esattamente a ciò che pensiamo di aver scritto? La traduzione è affidata al compilatore,
dipenderà allora da questo e bisogna dare evidenza che accada correttamente. Utilizziamo un compilatore per cui è
certificato che l’output prodotto corrisponda all’input fornito. Altrimenti possiamo utilizzare un compilatore che,
sebbene non sia certificato per ogni situazione, sappiamo produca un output corretto nel nostro caso. Infine, possiamo
controllare manualmente l’eseguibile rispetto al file sorgente. Non importa la scelta effettuata, l’importante è dare
evidenza del fatto che l’output effettivamente prodotto corrisponda a ciò che è stato scritto nel codice sorgente. Gli
std non definiscono le attività da svolgere necessariamente ma le categorie di attività accettabili per ottenere un certo
livello di qualità, nel caso di sistemi critici parliamo di Safety Integrity Level (SIL).

Vediamo il modello concettuale dell’ISO 9126: il modello di qualità si compone di fattori di qualità, ovvero
caratteristiche, sottocaratteristiche e
attributi (la gerarchia è completa e
disgiunta). Le caratteristiche sono
composte da sottocaratteristiche,
che a loro volta possono essere
sottocaratteristiche basic (composte
da attributi) e sottocaratteristiche
derivate (composte da altre
sottocaratteristiche). Gli attributi
possono essere basic o derivati
(composti da altri attributi).

L’usabilità (capacità di gestire bene l’interazione con l’utente) è composta da learnability e understandability, la prima
riguarda la capacità di apprendimento, la seconda la comprensibilità. Poterle misurare è importante e per farlo
consideriamo un campione significativo di utenti e analizziamo la facilità di comprensione, il primo utilizzo è
significativo per la learnability. Se decidiamo di comprare per la nostra azienda un software e il costo per imparare a
utilizzarlo è elevato dovremo considerarlo come un costo aggiuntivo a quello di acquisto. Potremmo allora scegliere
un software la cui curva di apprendimento sia più semplice, così che i dipendenti possano usarlo più velocemente e la
produttività non venga persa.
L’affidabilità è la capacità di mantenere le prestazioni stabilite nelle condizioni e nei tempi fissati (il software reagisce
bene a variazioni esterne). Le sue sottocaratteristiche sono la maturità (robustezza: capacità di evitare
malfunzionamenti a seguito di condizioni di guasto dovute ad errori), la tolleranza (capacità di mantenere certi livelli
di prestazioni in caso di malfunzionamenti) e recuperabilità (capacità di ripristinare certi livelli di prestazione
predeterminati e di recuperare i dati).

Gli std non sono semplici da leggere, esistono altri standard che descrivono come leggerli, interpretarli e consultarli.
Lo std 598 guida la valutazione della qualità del software.

Metriche del software:

Dobbiamo essere in grado di monitorare il prodotto e l’avanzamento del processo. Ci servono allora tecniche che ci
consentano di misurare al fine di rispondere a domande quali: ci sono ritardi nel processo? Il software rispetterà la
qualità desiderata? Si è consumato o si sta consumando di più del previsto, in termini di risorse? Misurare il processo
è fondamentale in quanto esso influenza costi e tempi. Non possiamo accorgerci di un eventuale ritardo solo alla
consegna, bisogna essere in grado di saperlo prima così da intervenire o abbandonare. Allo stesso modo alcune qualità,
come le prestazioni, saranno visibili solo dopo aver messo in esercizio il software ma dobbiamo poter controllare e,
eventualmente, intervenire se non è stato tenuto conto del modo tramite cui ottenere i requisiti prestazionali. Spesso
inoltre vogliamo effettuare delle stime a priori, ad esempio dei costi, così da determinare il prezzo finale del prodotto.
Queste tecniche nell’ingegneria del software non seguono metodi scientifici ma sono basate su basi empiriche.

Una metrica caratterizza in modo quantitativo e misurabile attributi semplici di un’entità. La misura è l’assegnazione
di un valore a un attributo, in base a una metrica.

Distinguiamo metriche:

• Del prodotto
• Del processo
• Di qualità

Inoltre, le distinguiamo tra:

• Funzionali
• Strutturali (tra cui dimensionali).

Le due classificazioni non sono alternative. Le metriche sono uno strumento con cui prediciamo, misuriamo il processo,
il prodotto e le risorse. In generale le metriche sono associate agli attributi interni, da cui dipendono anche gli esterni
(più complessi).

Metriche dimensionali: tendenzialmente come metrica dimensionale di un’applicazione non consideriamo la


dimensione del file eseguibile, in quanto dipende dal particolare compilatore, ma il numero di linee di codice del file
sorgente. Se, ad esempio, consideriamo un file di 100 righe, ognuna da 80 byte le dimensioni del codice sorgente non
saranno semplicemente 8000 ma dovremo tener conto del carattere ‘a capo’ che, a seconda del sistema operativo,
occuperà 1 o 2 byte. In ASCI occupa due caratteri (CR e LF, ovvero Carriage Return e Line Feed), a parità di righe quindi
uno un codice sorgente avrà 100 byte in più dell’altro. Generalmente viene quindi considerato significativo il numero
di linee di codice LOC, anche se non è detto sia fondamentale. Inoltre non c’è accordo sulle righe di commento, che
aumentano il LOC ma sono fondamentali per la documentazione del programma (in particolare ricordiamo che in Java,
tramite JavaDoc, utilizzando i commenti siamo in grado di generare automaticamente la documentazione) e questa è
importante per la manutenibilità, il riuso ecc.. un’altra metrica è la NCNB (No-Comment-No-Blank, anche detta NLOC),
tramite cui calcoliamo tutte le righe di codice tranne linee vuote e commenti. Allo stesso modo possiamo considerare
gli statement eseguibili: il programma è fatto da istruzioni e dati, che vanno dichiarati e definiti ma queste non sono
istruzioni eseguibili (il programma comincia dalla prima istruzione eseguibile). CLOC sono le linee di commento.

Se vogliamo misurare il rapporto tra le linee di codice e i commenti consideriamo CP=CLOC/LOC (ricordiamo che
LOC=NLOC+CLOC, contiene tutte le righe di codice, anche non eseguibili), un programma senza commenti ha lo
svantaggio di essere poco documentato, cosa che creerà difficoltà in fase di eventuale manutenzione o correzione, un
programma troppo commentato risulta difficile da leggere agevolmente. Un buon rapporto è sul 30%.

Tramite la LOC possiamo misurare la produttività P=LOC/M dove M è il tempo necessario a sviluppare il programma,
espresso solitamente in mesi/uomo. Per misurarlo o facciamo una stima al termine di ogni giornata o alla fine del
processo, considerando l’inizio, la fine, il numero di programmatori e il tempo di lavoro.

Possiamo anche misurare, tramite la LOC, la qualità, come numero di righe di codice sul numero di errori rilevati LOC/E.
Non è facile però determinare se sia da considerarsi migliore un programma in cui siano stati rilevati pochi errori,
rispetto alla LOC, o molti. Potrebbero essere stato buona la codifica oppure meno il testing. Allo stesso modo se
rileviamo molti difetti non è detto che il testing sia ottimo, il programma potrebbe esser stato scritto male e
potrebbero esservi altri errori.

Misuriamo il livello di documentazione come numero di pagine di documentazione su LOC, ovvero PD/LOC.

La LOC è una metrica dimensionale e non di complessità. Ci permette di effettuare misure a posteriori (per misurare
le qualità di un programma) o stime a priori (per verificare il corretto svolgimento delle attività di sviluppo).

Metriche di complessità: la più famosa metrica di complessità vista è il numero ciclomatico.

Un’altra metrica è il flusso di informazione, fan-in e fan-out sono i flussi entrati e uscenti (nel caso di un
sottoprogramma saranno pari a quanti sottoprogrammi/programmi lo invocano e a quanti ne invoca).

C = Loc * (Fan-in * Fan-out)2

È una metrica di complessità in quanto dovremmo avere alta coesione e basso accoppiamento (principio di
modularità), se i flussi entranti sono molti allora il modulo è molto riusato, è una metrica per il riuso. Se ci sono molti
flussi uscenti allora l’accoppiamento è alto, dipende da troppo moduli.

Metriche Object-Oriented: sono state pensate metriche specifiche per i programmi OO. Queste si focalizzano sulla
struttura interna o sulla complessità esterna di un oggetto. Possiamo considerare come complessità il numero delle
classi, il numero delle classi chiavi e di quelle a loro supporto, ossia quante realizzano la business logic e quante sono
funzionali a queste. Se conosciamo il tempo medio impiegato dai programmatori destinati al progetto per sviluppare
una classe e sappiamo quante classi dobbiamo implementare siamo in grado di stabilire quanti giorni/uomo saranno
necessari per sviluppare il software, dunque è una metrica utile anche per pianificare il processo.

In questo ambito sono importanti le metriche di Chidamber e Kemerer:

1. Weighted Method per Class: possiamo definirla come numero di metodi per classe o come somma di tutte le
complessità ciclomatiche dei metodi costituenti la classe (dato che, per ogni metodo, siamo in grado di
calcolare il numero di McCabe);
2. Response for a class: è la cardinalità dell’insieme di tutti i metodi che possono essere chiamati in risposta ad
un messaggio ricevuto da una classe. Una classe riceve un messaggio quando viene invocato un metodo su di
essa e per svolgerlo può delegare parte del lavoro ad altri oggetti, invocandone i metodi. Questo numero ci
aiuta quindi a misurare anche l’accoppiamento;
3. Lack of cohesion: è la misura della mancanza di coesione, misurando la diversità dei metodi in relazione alle
variabili usate. I metodi sono più simili se operano sugli stessi attributi;
4. Coupling between object classes: è la misura dell’accoppiamento in base al numero di classi da cui la classe
ha una dipendenza di qualche tipo;
5. Depth of inheritance tree: rappresenta la profondità di un albero in una gerarchia di classi;
6. Number of children: è il numero di sottoclassi immediatamente subordinate a una certa classe della gerarchia.
Maggiore è tale numero, maggiore è il riuso. Tuttavia, maggiore è tale numero maggiore è la probabilità di
aver realizzato un’astrazione errata.

22. Stima dei costi:


La stima dei costi è fondamentale in fase di pianificazione per stimare il prezzo del prodotto finale, dimensionare il
team di sviluppo ad altre ragioni di management del processo di produzione. È applicabile anche ai prodotti software
già esistenti, nel momento in cui dobbiamo valutare se effettuare una manutenzione (correttiva, adattativa o
perfettiva, a seconda dell’esigenza) sia effettivamente più conveniente di rifare completamente il prodotto (in termini
di risorse). I metodi non sono molti e sono tutti basati su principi empirici, il principale è l’analisi dei punti di funzione.
Possiamo aggiustare i parametri della stima, rendendola più precisa, sulla base dell’istanziazione specifica che abbiamo
nell’azienda, integrando conoscenza pregressa all’interno di essa.
La stima, per quanto generica, è influenzata dalla specifica azienda, in particolare dal grado di preparazione del
personale, dalla seniority e anche dal tipo di software che si vuole sviluppare. In generale la stima dei costi è influenzata
dalla produttività degli sviluppatori, serve una metrica di produttività. La metrica ideale dovrebbe misurare quale sia
il valore prodotto per unità di sforzo, ad esempio, se per unità di sforzo consideriamo il mese/uomo il costo dipende
da cose note, come gli stipendi da pagare, ma il valore è diverso. Tendenzialmente vengono richiesti software per la
realizzazione di funzionalità che realizzino il business, il valore allora sarà il valore delle funzionalità prodotte. I punti
funzione (function points) sono una metrica per la stima della quantità di funzionalità del software, il punto f
corrisponde al valore della funzionalità. Calcolati tali punti siamo in grado di derivare la quantità di sforzo richiesto per
realizzarlo, che non dipende dalla tecnologia. Possiamo allora stimare i costi e andare a migliorare alcuni fattori
aggiuntivi. Il metodo di analisi dei function points è vantaggioso in quanto, basandosi sui requisiti funzionali, è
applicabile fin dalla prima fase del ciclo di vita, dallo studio di fattibilità o dall’analisi dei requisiti.

Tramite l’analisi dei punti di funzione (FTA) ci serviamo dei punti funzione per quantificare le funzionalità del sistema.
In base alle entrate e alle uscite valutiamo quanto sia necessario alla realizzazione di ciascuna di esse. Una funzionalità
per cui dobbiamo elaborare molti dati e produrre molte uscite peserà di più di una in cui dovremo elaborare pochi dati
e produrre poche uscite. Un altro dato da tenere in considerazione è la quantità di informazioni diverse da
memorizzare. Se consideriamo Amazon la funzionalità di gestione dell’ordine dovrà gestire esso e il carrello e
consentire la memorizzazione dei dati.

I function point sono standardizzati dall’ISO e dal gruppo di lavoro IFPUG (International Function Point User Group). Il
metodo 4.1 è l’UFPM (il metodo degli unadjusted function points) e si applica in due stati:
1. Calcoliamo la stima delle funzionalità, senza aggiustamenti
2. Applichiamo dei valori correttivi in base alla letteratura dell’ingegneria del software o all’esperienza nella
nostra realtà specifica.

È importante perché può essere certificato.

A partire dai requisiti funzionali determiniamo quante sono le funzionalità da realizzare (cosa che siamo in grado di
evincere già dai casi d’uso) e il peso di ciascuna di esse, ovvero la complessità in base ai dati in ingresso, in uscita e ai
dati da produrre e memorizzare.

• Passo 1 - valutazione degli unadjusted function points:


1. Determinazione del tipo di conteggio;
2. Identificazione dell’ambito di conteggio e dei confini applicativi;
3. Identificazione delle funzioni dati;
4. Identificazione delle funzioni transazionali;
5. Conteggio degli UFP;

al termine di questa fase applicheremo i fattori di aggiustamento e otterremo i function points.

Per prima cosa dobbiamo individuare il tipo di conteggio tra:

• Conteggio per sviluppo di progetto, nel caso in cui stiamo sviluppando un nuovo software;
• Conteggio per manutenzione evolutiva, viene applicato per misurare le modifiche ad un’applicazione esistente nel
momento in cui vengono presentati requisiti aggiuntivi;
• Conteggio applicativo, misura un’applicazione già esistente (può essere utile per scegliere se rifare il prodotto ex-
novo o effettuare una manutenzione).

Successivamente identifichiamo il confine applicativo, la linea di demarcazione tra l’utente (o altri sistemi) e
l’applicazione da utilizzare. Tale individuazione va effettuata dalla percezione dell’utente in merito alle funzionalità,
dunque in base ad aspetti di business e non tecnologici. Dobbiamo individuare gli input provenienti dall’utente
esterno, gli output ad esso forniti e
gli external inquiry (input e output
scambiati con applicazioni esterne).
Avremo uno scambio di file con
altre applicazioni ma anche file
interni, non intesi come file fisici ma
come file logici.

Nello step di identificazione delle funzioni dati distinguiamo tra:

• Internal logical file (ILF): aggregati logici di dati generati, gestiti e usati internamente dal sistema (ad esempio,
tabelle di database, file di errore, password trattenute dal sistema ecc.). Affinchè dei file siano tali devono
essere identificabili all’utente, dunque i file temporanei non sono ILF. Nell’esempio prima visto su Amazon
nella gestione del carrello dovremo memorizzare l’acquisto con le sue informazioni (costo, data, quantità ecc.),
queste sono dati generati e gestiti internamente al sistema ma visibili al suo utilizzatore.
• External interface file (EIF): aggregati logici di dati, scambiati con altre applicazioni. Anche questi per essere
tali devono essere identificabili dall’utente. Spesso per ottenere l’interoperabilità tra due applicazioni l’una
scrive nel deposito dati dell’altra, in generale gli ELF di un’applicazione sono ILF di un’altra.

Nella fase di identificazione delle funzioni transazioni invece:

• External input (EI): informazioni distinte fornite dall’utente o da altre applicazioni, usate come dati di ingresso.
Generalmente alterano gli ILF o agiscono sul funzionamento del sistema. Non sono da considerarsi tali schemi
di menù usati per navigare all’interno dell’applicazione, in quanto non alternao gli ILF, richieste di input da
parte di una query o meccanismi per il refresh delle schermate a video.
• External output (EO): output distinti che il sistema restituisce all’utente come risultato delle proprie
elaborazioni. Lo scopo è restituire i risultati provenienti da elaborazioni (e non da semplici recuperi di dati da
un ILF), per fare ciò dev’esservi una formula o una funzione di calcolo applicata a un ILF.
• External Inquiry (EQ): interrogazioni in linea che producono una risposta immediata del sistema, senza
necessità di elaborazione. Sono EQ le query al database, tramite cui vengono dati risultati all’utente
prelevando dati da un ILF o un EIF. Avranno un peso mine rispetto agli EO proprio per la mancata elaborazione.
Non sono EQ le schermate di menù, i messaggi di errore e i messaggi di conferma.

Per effettuare il conteggio degli UFP contiamo il numero degli ILF, EIF, EI, EO e EQ (rispettivamente NILF, NEIF, NEI,
NEO e NEQ) e li pesiamo per un fattore, associato ad una stima di complessità (semplice, media o complessa). Gli UFP
li otteniamo come sommatoria dei ViPi.
I dati ottenuti provengono da stime
empiriche, possiamo aggiustarli
aggiungendo la nostra conoscenza.
Notiamo che, a parità di complessità, il
numero di internal logical file (ILF) pesa
meno degli EQ. Questo perché se NIFl è
il file di scrittura del record
corrispondente al prodotto comprato su
Amazon, l’EQ conterrà una query che restituisca quel dato e, pur non compiendo un’elaborazione, verrà effettuata
almeno la ricerca. Tanto più è complessa la scrittura tanto più sarà complessa la query. Al crescere della complessità
il rapporto tra NIFL e NEQ rimane pressoché invariato. Il parametro più pesante è il NEO, in quanto richiede
un’elaborazione.

• Passo 2 - applicazione dei fattori correttivi:

Distinguiamo 14 fattori correttivi, il cui valore varia tra 0 e 5:

- 0 ininfluente
- 1 incidenza scarsa
- 2 incidenza moderata
- 3 incidenza media
- 4 incidenza significativa
- 5 incidenza essenziale

Il parametro prestazioni pesa gli obiettivi prestazionali dell’operazione. Se non


vi sono tali vincoli sarà un fattore ininfluente (0). Se i vincoli non funzionali
impongono il rispetto dei tempi ma in maniera non troppo preoccupante varrà
1. Se i tempi di risposta sono critici durante le ore di picco 2, se lo sono durante
l’intera giornata 3. C’è infatti differenza se sono critici solo in presenza di traffico
o sempre, gestiremo le situazioni differentemente, in un caso va gestito il caso
medio, nell’altro il picco (nei sistemi real time, ad esempio, distinguevamo gli hard real time e i soft real time, a seconda
che la scadenza fosse mandatoria o da rispettare in termini probabilistici). Se l’ottimizzazione delle prestazioni riveste
estrema criticità varrà 5.

Efficienza per l’utente finale: esprime il grado di considerazione per i fattori umani e la facilità di utilizzo per l’utente.
Alcuni requisiti possono riguardare la possibilità di fornire gli help, la documentazione online, il menù, lo scrolling ecc.
Ciò che cambia è la logica di interfacciamento e i pesi vengono stabili in base a quanti di questi elementi (sopracitati)
sono importanti.

Riusabilità: tendenzialmente non è un fattore che interessa l’utente, può in situazioni come le pubbliche
amministrazioni. Pesiamo tale fattore in base alla quantità di moduli che potranno essere riutilizzati.
Il valore finale del conteggio FP è il prodotto tra il conteggio UFP e l’aliquota introdotta dai fattori correttivi, secondo
la formula:

Tramite gli FP possiamo calcolare il costo del progetto. Ad esempio, se


dobbiamo realizzare 12 function point e ciascuno costa 1000 il costo finale
allora sarà banalmente 12000. Per calcolare il costo di ciascun function point
applichiamo delle stime tramite le tabelle, i valori dipenderanno da fattori come
il linguaggio di programmazione adoperato, il numero di linee di codice (che
varia di linguaggio in linguaggio). Più è elevato il livello di astrazione è meno
saranno le linee di codice necessarie tra i rispettivi linguaggi.

23. Ciclo di vita del software:


Nella produzione di un sistema software sono coinvolte molte attività, come la fase di specifica, di progettazione, di
verifica. L’ordine in cui queste vengono affrontate definisce il ciclo di vita del software. in generale, il processo di
produzione del software è il processo che seguiamo per costruire, consegnare ai clienti e far evolvere il prodotto
software, dalla nascita dell’idea fino alla consegna ed al ritiro del prodotto quando diviene obsoleto.
Esaminiamo diversi modelli che cercano di catturare l’essenza di questo processo, tenendo ben presenti due cose: la
produzione del software è un’attività prevalentemente intellettuale, quindi non facilmente automatizzabile; in
secondo luogo il software è caratterizzato da un alto grado di instabilità (i requisiti cambiano in continuazione e quindi
i prodotti devono essere evolvibili).

Un modello del ciclo di vita del software (CVS) è una caratterizzazione descrittiva o prescrittiva di come un sistema
software viene o dovrebbe essere sviluppato. Gli obiettivi sono:
- determinare l’ordine delle attività nello sviluppo e nell’evoluzione del software;
- stabilire criteri di transizione per progredire da uno stadio di lavorazione al successivo, dato che molto spesso
è difficile separare nettamente le diverse fasi.

Code and fix


Un approccio primitivo alla produzione del software, tipico del programmatore singolo, consiste nello scrivere codice
ed aggiustarlo (per correggere errori, migliorarne le funzionalità o per aggiungere nuove caratteristiche): code and fix.
Questo modello è stato causa di molte difficoltà e carenze, in primis dovute al fatto che, dopo una serie di
cambiamenti, il codice diventava disorganizzato rendendo le modifiche successive più difficili.
Oggi, inoltre, il software non viene sviluppato per uso personale, ma per utenti che in generale non possiedono un
background informatico. Diventano, quindi, importanti questione economiche, organizzative e psicologiche; è
cresciuta la domanda per livelli qualitativi molto più elevati e i requisiti di affidabilità sono diventati più stringenti.
Un’altra differenza sostanziale con il passato è che lo sviluppo del software è diventato un’attività di gruppo.
Appare chiaro che il modello code and fix sia inadeguato per lo sviluppo odierno del software:
- non c’è una gestione pianificata della complessità (modularità, separazione degli interessi, anticipazione del
cambiamento);
- la gestione del personale risulta difficile (problemi di ricambio del personale per mancata documentazione,
che del resto è importante proprio per comunicare. La conoscenza deve essere scritta, anche medianti artefatti
software, purché opportunamente documentata, per stabilire il linguaggio di comunicazione, in termini di
metodologie, glossario ecc.);
- risulta difficile aggiustare/modificare/ristrutturare il codice;
- interpretazione sbagliata dei requisiti utente;
- processo non prevedibile (tempi/costi, manutenibilità), qualità non misurabile.
Le tre caratteristiche fondamentali di un sistema software sono qualità, tempi e costi (triangolo delle qualità).

Questi problemi portano a riconoscere la necessità di processi predicibili e controllabili, dunque strutturati. La qualità
del processo influenza: la qualità dei prodotti, i tempi per portare il prodotto sul mercato, i costi, le prestazioni sui
diversi progetti.

Attività principali della produzione del software


La produzione di software può essere scomposta in diverse attività specifiche. Il modo in cui queste vengono
organizzate può variare a seconda del modello di processo scelto.

1. Studio di fattibilità
Questa attività viene eseguita prima che inizi il processo di produzione, in modo da poter decidere se debba essere
intrapreso un nuovo sviluppo. Il suo scopo è fornire un documento di studio di fattibilità, il quale presenti diversi scenari
e soluzioni alternative, insieme ad una discussione dei compromessi in termini di costi e benefici (valutazione delle
risorse finanziare ed umane). Prima di tutto, quando il cliente pone una richiesta, è necessario definire il problema, in
quanto spesso nemmeno il cliente/committente è in grado di dettagliarlo in tutti i suoi aspetti. Più un problema è
compreso a fondo più è facile trovare soluzioni alternative con costi e benefici diversi.
Sulla base della definizione del problema durante l’analisi preliminare, gli sviluppatori identificano le soluzioni
alternative: per ognuna di queste vengono stimati i costi e le possibili date di consegna, influenzate sicuramente dalle
risorse.
La fase di studio di fattibilità consiste quindi in una simulazione del processo di sviluppo futuro mediante la quale
dedurre informazioni utili per decidere se valga la pena avviare il progetto. Il rischio, però, è quello di “vincolare” lo
sviluppo futuro mediante decisioni premature, prese quando le informazioni disponibili sono ancora incomplete e
magari confuse.

2. Acquisizione, analisi e specifica dei requisiti


Questa è una delle attività più critiche del processo di ingegnerizzazione del software: se si pensa al software come
passaggio da qualcosa di informale a qualcosa di estremamente formale, è proprio questa la fase in cui deve avvenire
la transizione.
L’ingegneria dei requisiti sviluppa metodi per raccogliere (estrarre l’informazione dal committente, che non
necessariamente corrisponde ad una persona fisica, ma potrebbe anche essere il mercato), documentare, classificare
ed analizzare i requisiti. C’è la necessità di elicitare i requisiti latenti, oltre ai requisiti funzionali. In questa fase bisogna
svincolare il “cosa” dal “come”: un esempio potrebbe essere la separazione degli interessi, specificando le funzionalità
e le qualità che il software deve possedere, senza vincolare la progettazione e l’implementazione. I compiti
dell’analista sono: identificare i portatori di interesse (stakeholders), esplicitare i requisiti, conciliare i vari punti di vista,
specificare i requisiti.
Il risultato di questa fase è la produzione di un Documento di specifica dei Requisiti (DSR), un glossario comune
comprensibile, preciso, completo, coerente, non ambiguo, modificabile. D’altro canto, abbiamo detto che la linea di
demarcazione tra correttezza e robustezza del software è proprio la specifica.
Il DSR contiene:
- descrizione del dominio. Le domande principali a cui rispondere sono: chi sono gli utenti interessati e quali
sono i loro obiettivi e le loro aspettative? Quali sono le principali entità che caratterizzano il dominio? Quali
sono le principali relazioni? Che influssi ha il sistema che si vuole sviluppare su di esse?
- requisiti funzionali: descrivono che cosa dovrà fare il prodotto, usando notazioni informali, semiformali o
formali. Un esempio che vediamo è lo standard UML.
- requisiti non funzionali: affidabilità, disponibilità, integrità, sicurezza, ecc.
- requisiti del processo di sviluppo-manutenzione: procedure per il controllo della qualità (procedure di test del
sistema), possibili cambiamenti del sistema.
Quando si specificano i requisiti, a fronte di essi, vanno specificate anche le modalità di test, mediante un Piano di
Test di Sistema (PTS). È una modalità per ottenere feedback: se non si riesce a capire come testare i requisiti, significa
che questi sono scritti male, non in maniera testabile. Bisogna poter confrontare l’output esterno con l’output atteso.

Attenzione: la scelta in questa fase di utilizzare uno specifico linguaggio di programmazione costituisce un vero e
proprio vincolo, anzi può essere dannosa in quanto viene a mancare il principio di differimento delle decisioni (le
decisioni vanno prese solamente a tempo debito). Potrebbe essere però un vincolo se necessito di garantire alcuni
fattori come la portabilità (ad esempio sviluppare in Java garantisce una buona portabilità del sistema) o se
esplicitamente richiesto dal committente. Come ciascuna fase, l’analisi presenta come input una raccolta di requisiti,
che devono essere recuperati interfacciandosi con il committente che può essere una particolare figura professionale
all’interno di un’azienda, un responsabile di reparto interno alla stessa azienda che lavora al software o una legge, ma
raramente si tratta dell’utilizzatore finale. Per la comprensione dei requisiti, l’analista deve capire il dominio
applicativo, identificare gli stakeholders, ovvero tutti coloro che hanno un interesse nel sistema e che saranno
responsabili della sua accettazione. Per la raccolta dei requisiti, c’è un’attività di elicitazione dei requisiti, che abbiamo
trattato nel capitolo sull’Ingegneria dei requisiti.

3. Progettazione
La progettazione è l’attività attraverso la quale i progettisti strutturano l’applicazione a diversi livelli di dettaglio. È
possibile cominciare ad un livello alto definendo un’architettura che separi le funzionalità tra i componenti client e
server. Si può procedere poi con una progettazione dettagliata che si occupi della scomposizione di client e server in
componenti modulari, definendo le loro interfacce in maniera precisa. Ogni componente può essere a sua volta
scomposto in ulteriori sottocomponenti.
Il risultato è un documento di specifica di progetto (DSP) che contiene una descrizione dell’architettura software:
descrive i componenti, le loro interfacce, le relazioni tra di loro; registra le decisioni significative e ne spiega le
motivazioni. Una documentazione di questo tipo è importante in vista di possibili richieste future di cambiamenti che
potrebbero comportare modifiche all’architettura. La forma esatta del documento di specifica di progetto è
solitamente definita come parte degli standard adottati dall’azienda.

4. Codifica, test e rilascio


Questa fase si articola nella produzione del codice in un determinato linguaggio di programmazione e nel testing, in
particolare si distinguono tre tipologie di test: test di unità, test di integrazione e di sistema, test di accettazione.
Dopo aver completato lo sviluppo di un applicativo, rimangono da portare a termine diverse attività post-sviluppo. In
primis, il software deve essere consegnato ai clienti. Ciò avviene, in genere, in due fasi: nella prima l’applicazione viene
distribuita ad un gruppo selezionato di clienti prima del suo rilascio. Lo scopo è quello di eseguire una sorta di
esperimento controllato per determinare, sulla base del feedback ricevuto dagli utenti, se sono necessari cambiamenti
al software prima del rilascio: beta test.
Nella seconda fase, invece, il prodotto viene rilasciato a tutti i clienti.
Infine, la manutenzione può essere definita come l’insieme di attività svolte per modificare il sistema dopo che è stato
rilasciato al cliente. La manutenzione può essere adattiva, correttiva o perfettiva.

Tutte le attività fin qui viste contengono alcuni compiti comuni, come la documentazione, la verifica e la gestione.
La documentazione è un risultato necessario in qualsiasi attività. Può essere realizzata mediante l’uso di diagrammi
UML oppure una descrizione narrativa che fornisce una motivazione delle diverse scelte progettuali.
La verifica è parte dell’attività di sviluppo del software. Si distingue la validazione, ovvero la valutazione di come il
prodotto risponde alle necessità dei clienti, e la verifica, lo studio della correttezza interna del processo, quando
l’architettura software corrisponde ai requisiti specificati.
La gestione delle attività è importante al fine di controllare adeguatamente ognuna di esse, insieme alle sottoattività
comprese.

Modello a cascata – Waterfall Model


Nel modello a cascata, il processo struttura le attività come
una cascata lineare di fasi in cui l’output di una fase diventa
l’input della seguente. Ogni fase, a sua volta, è strutturata,
come un insieme di sottoattività che possono essere svolte da
diverse persone in maniera concorrente. La fine di ogni fase è
una milestone, l’output di ogni fase è detto deliverable, il
quale deve essere ben definito per poter misurare la qualità
del progetto.
Il modello ha fornito due importanti contributi alla
comprensione dei processi software: il processo di sviluppo
del software deve essere soggetto a disciplina, pianificazione
e gestione; l’implementazione del prodotto dovrebbe essere
rimandata fino a quando non sono perfettamente chiari gli
obiettivi.
Visto che è un modello ideale, il modello a cascata può essere solo approssimato nella pratica. È possibile
caratterizzarlo mediante tre proprietà: linearità, rigidità e monoliticità.
Il modello a cascata si basa sull’assunzione che lo sviluppo del software proceda linearmente dall’analisi alla
produzione di codice. Nella pratica ciò non può succedere ed è necessario prevedere forme disciplinate di cicli di
feedback. Lo scopo dell’alpha e del beta testing è infatti quello di fornire feedback alle fasi precedenti. La pianificazione
del progetto è basata su un’assunzione di linearità e qualsiasi deviazione dalla progettazione lineare è sconsigliata in
quanto rappresenta una deviazione dal piano originale e richiede una nuova pianificazione.
Un’altra assunzione alla base del modello a cascata è quello della rigidità delle fasi, ovvero il fatto che i risultati di ogni
fase vengono congelati prima di procedere alla fase successiva. Il modello, di conseguenza, assume che i requisiti e le
specifiche di progetto possono essere congelati nelle prime fasi di sviluppo, quando le conoscenze dell’area applicativa
e l’esperienza sono ancora preliminari e soggette a cambiamento. Questa assunzione non riconosce la necessità di
un’interazione tra i clienti e gli sviluppatori in modo da far evolvere i requisiti durante il ciclo di vita.
Infine, il modello a cascata è monolitico, nel senso che tutta la pianificazione è orientata ad una singola data di rilascio.
Tutta l’analisi viene completata prima che cominci la progettazione, e il prodotto viene rilasciato mesi o addirittura
anni dopo che i requisiti sono stati raccolti, analizzati e specificati. Se eventuali errori commessi in questa fase che non
sono subito scoperti, verranno identificati solo dopo che il sistema è stato rilasciato ai clienti. Inoltre, siccome il
processo di sviluppo può durare a lungo, il prodotto potrebbe essere rilasciato quando ormai i requisiti dei clienti sono
cambiati e richiedere subito altri adattamenti.
Il modello a cascata soffre di punti deboli: non permette una visibilità sufficiente del prodotto in sviluppo fino a quando
il prodotto completo non è pienamente implementato.

Vediamo alcuni dei problemi associati alla rigidità del modello:


- è difficile stimare le risorse in maniera accurata quando sono disponibili solo informazioni limitate. La stima
dei costi e la pianificazione del progetto, con il modello a cascata, sono spesso possibili solo dopo che è stata
effettuata una prima fase di analisi;
- la specifica dei requisiti produce un documento scritto che guida e vincola il prodotto da sviluppare.
Indipendentemente dalla leggibilità, esso rimane pur sempre un documento inanimato, ben diverso dallo
strumento attivo che verrà rilasciato;
- l’utente spesso non conosce i requisiti esatti dell’applicazione, in alcuni casi non può conoscerli.
- non sottolinea sufficientemente il bisogno di anticipare possibili cambiamenti. Al contrario, la filosofia di base
del modello è che si debba puntare alla linearità congelando più cose possibili nelle prime fasi.
- il processo è document driven, guidato dai documenti, obbligano a standard pesantemente basati sulla
produzione di una data documentazione in determinati momenti.

Modello V&V

Il modello a cascata è meno utilizzato per via della sua eccessiva rigorosità, basandosi sull’assunzione che lo sviluppo
del software proceda linearmente
dall’analisi alla produzione del codice.
Nella pratica ciò non può succedere ed è
necessario prevedere forme disciplinate
di feedback. Lo scopo, infatti, dell’alpha
e del beta testing è quello di fornire
feedback alle fasi precedenti.
Un modello di questo tipo permette un
feedback disciplinato ed esplicito,
limitando i cicli di feedback da una fase a
quella immediatamente precedente,
minimizzando la quantità di lavoro da
rifare nel ripercorrere le fasi precedenti.
Il fondamento logico + che si dovrebbe cercare di ottenere la linearità del ciclo di vita in modo da mantenere il processo
il più possibile prevedibile e facile da monitorare.
Questo viene chiamato modello a cascata verification&validation, con retroazione: con verifica si intende la
corrispondenza tra prodotto software e specifica, mentre con validazione si intende l’appropriatezza di un prodotto
software rispetto ai requisiti utente.
E’ una variante del modello a cascata: introduce un feedback in ogni fase, si possono così rilevare errori prima del
rilascio, ma resta comunque un modello che non anticipa i possibili cambiamenti, quindi può essere utile quando a
priori si sa che il sistema sarà poco soggetto ai cambiamenti.

Modello a V

Nel modello a V tutte le attività del ramo di sinistra sono


collegate con quelle del ramo a destra: durante le attività
di sinistra, vengono progettati i test della fase a destra
corrispondente (ad esempio dalla specifica dei requisiti
corrisponde la progettazione dei test di accettazione). Se
si trova un errore in una fase a destra (per esempio nel
testing di sistema) si riesegue la fase a sinistra collegata.
Ciò si può iterare migliorando requisiti, progetto e codice.

Nello sviluppo di sistemi HW/SW (Hardware/Software), di


cui il software è una parte, si prevede l’utilizzo di un
modello a V: dapprima si esaminano i requisiti utente di sistema (sia funzionali, sia non funzionali), poi si identificano
le unità del sistema assegnando requisiti alle unità (allocazione dei requisiti). L’analisi dei requisiti hardware/software
esamina le risorse di ogni unità, decomponendole in CSCI (Computer Software Configuration Items) e HCI (Hardware
Configuration Items).

Le varianti del modello a cascata sono adatte soprattutto quando si prevede che il sistema (e/o l’ambiente) sarà poco
soggetto a cambiamenti, oppure vi sono requisiti chiari e completi sin da subito con poca possibilità di cambiare (ad
esempio sistemi non interattivi).

Modelli evolutivi

I modelli prototipali utilizzano i prototipi, quando i requisiti non sono chiari o possono cambiare (abbiamo visto che
nel modello a V e nei modelli a cascata i requisiti sono congelati), con l’obiettivo di raffinare i requisiti. Il prototipo
non è il prodotto finale, ma una versione inziale, utilizzata solo temporaneamente, fino a quando fornisce al progettista
un feedback sufficiente. Quindi, si realizza un prototipo, un modello dell’applicazione, con lo scopo di validare i
requisiti, ricevendo feedback dagli stakeholders e/o committenti.
Si finisce di utilizzare il prototipo quando sono stati chiariti i requisiti: ad esempio, un prototipo potrebbe essere
un’interfaccia utente dell’applicazione, in cui non è necessario che le funzionalità siano effettivamente implementate,
ma è utile per stabilire come si deve presentare l’applicazione (erano i requisiti di interfacciamento uomo-macchina).
Logicamente un prototipo deve costare poco, facile dea realizzare, ad esempio mediante applicazioni web, interfacce
come ambienti di programmazioni visuali (Visual Basic).
Il prototipo può essere usa e getta (throw-away): la prima versione dell’applicazione viene cestinata, la seconda
versione può poi essere sviluppata seguendo un modello di processo a cascata.
Questo approccio (ispirato al principio “Do it twice”) fornisce una soluzione parziale ad alcuni problemi discussi
precedentemente, come l’eliminazione di errori nei requisiti. Non elimina comunque la distanza temporale che esiste
tra la definizione dei requisiti e il rilascio dell’applicazione e non sottolinea la necessità di anticipare i cambiamenti.
Se invece il prototipo porta progressivamente al prodotto finale si parla di prototipo evolutivo: il prototipo è uno
strumento di identificazione dei requisiti di utente; è incompleto, approssimativo, realizzato utilizzando parti già
possedute o routines stub.
Nell’approccio prototipale la distanza tra la specifica dei requisiti ed il rilascio è ancora invariata, ma non è adatto a
gestire i cambiamenti. E’ comunque un passo verso modelli di sviluppo flessibili, chiamati approcci incrementali.
Bohem definisce il modello di processo evolutivo come “un modello le cui fasi consistono in versioni incrementali di un
prodotto software operazionale con una direzione evolutiva determinata dall’esperienza pratica”. Un approccio
incrementale consiste in uno sviluppo in cui alcune parti di una fase possono essere rimandate in modo da produrre
prima possibile un insieme utile di funzionalità. In altre parole, in uno sviluppo evolutivo incrementale alcune parti di
una fase possono essere rimandate al fine di riprodurre prima possibile un insieme utile di funzionalità.
Un incremento rilasciabile è un’unità software funzionale auto-contenuta che esegue una funzione utile al cliente
accompagnata da: specifica dei requisiti, piani di test e casi di test, manuale utente.

Il modello evolutivo per implementazioni incrementali è un processo a cascata fino all’implementazione, solo
l’implementazione è rilasciata incrementalmente, le fasi precedenti sono come da modello a cascata. Permangono,
perciò, problemi tipici del processo a cascata come l’eccessiva distanza tra l’analisi dei requisiti dell’intero sistema al
primo rilascio, il “congelamento” dei requisiti (la specifica dei requisiti soggetta a possibile successiva invalidazione).
Il modello incrementale più generale si ha quando l’approccio incrementale è esteso a tutte le fasi del ciclo di sviluppo
si parla di modello a rilasci incrementali: si inizia con una fase che copre gli obiettivi del sistema complessivo e
l’architettura complessiva, si procede con l’analisi dei requisiti di un incremento. Ogni incremento è progettato,
implementato, testato, integrato e rilasciato. Gli incrementi tengono conto dei feedback (affinamento dei requisiti). È
sicuramente più facile pianificare le risorse necessarie, dato che si procede per un incremento alla volta. In questo
modo si può far fronte alle esigenze del cliente che almeno riesce ad avere subito a disposizione alcune funzionalità,
ed è utile quando lo sviluppatore viene pagato a rate.
In un modello evolutivo, la manutenzione scompare come fase del ciclo di vita, il ciclo di vita diventa un’evoluzione
continua.
Nel modello a cascata il cambiamento si manifesta come un’attività post-sviluppo (la manutenzione) ed è
costoso perché il processo non anticipa i cambiamenti.
Nel modello evolutivo, i cambiamenti sono gestiti durante lo sviluppo (design for change) ed è più semplice
incorporare i cambiamenti in modo disciplinato. Inoltre, il modello evolutivo favorisce rilasci tempestivi sul
mercato rivelandosi adatto alle attuali esigenze dell’industria del software.
Il cambiamento non va considerato come qualcosa di eccezionale, un’anomalia, deve rientrare nella normalità:
i requisiti cambiano o possono non essere chiari, in tal caso si chiariscono proprio proseguendo nella
realizzazione del progetto. Ad esempio, se consideriamo un ristorante, inizialmente il cameriere prendeva le
comande con “carta e penna”, oggi vengo prese utilizzando il tablet, tuttavia nei diversi reparti continuano ad
arrivare cartacee. Cambiamo nuovamente i requisiti, ed introduciamo una stampante in cucina, gestendo il
passaggio da una piattaforma all’altra. Addirittura, potremmo pensare che il cliente possa decidere di ordinare
direttamente al tavolo con un tablet. L’introduzione del software cambia il processo di business e il modo in cui
si svolge: non cambia il processo nella sostanza (il ristorante rimane tale), fondamentalmente le cose fatte
sono le stesse, ma cambia il modo in cui si svolge.
Modello trasformazionale
Lo sviluppo del software avviene come una progressione di passi. Una descrizione formale viene trasformata in una
descrizione meno astratta. Si specifica il software in
maniera formale, si passa ai semi-lavorati successivi
del ciclo di vita per “trasformazione” del semi-
lavorato del passo precedente. Il modello
trasformazionale si basa su due concetti:
prototipazione e formalizzazione, ottenendo un
codice eseguibile di basso livello. Si specificano i
requisiti formalmente (devono essere convalidate
prima di essere trasformata), si procede
trasformando man mano la descrizione formale in
una meno astratta e più dettagliata, fino a che
diviene eseguibile da un processo astratto. Le
specifiche eseguibili possono essere viste come un
prototipo evolutivo.

Un esempio è il compilatore: nella sotto-fase del ciclo di vita del software (implementazione/programmazione)
stiamo attuando un modello trasformazionale. Deve essere eseguito il codice eseguibile, ma in realtà quello
che è stato scritto è il codice sorgente.

Le trasformazioni possono essere eseguite manualmente o supportate da appositi strumenti. Uno dei vantaggi del
processo di trasformazioni è la riusabilità: i componenti sono riusabili, del resto abbiamo detto che otteniamo un semi-
lavorato di livello i+1 da uno di livello i.
Attualmente non è un paradigma praticabile, ma è guardato con interesse per l’approccio formale allo sviluppo del
software. Per esempio, fatto un modello di analisi e considerate delle regole di trasformazione, sarebbe semplice
ottenere la realizzazione del progetto mediante i principi di questo modello, ma questo implicherebbe una formalità
dei requisiti. La formalità è molto onerosa.

Metodologie agili

Le metodologie agili sono un sottoinsieme dei modelli evolutive, nate in alternativa alle metodologie tradizionali. Sono
nati come modelli di sviluppo leggeri (lightweight): più adattivi che predittivi, nel senso che non si cerca di
programmare lo sviluppo nel dettaglio ed in modo da soddisfare tutte le specifiche, ma si progettano programmi
pensati per cambiare nel tempo.
Negli approcci tradizionali, come quello a cascata, un aspetto delicato è proprio la pianificazione: dall’inizio dobbiamo
cercare di sapere con precisione cosa e come vogliamo fare, sia come funzionalità che come fattori di qualità, poi
pianifichiamo il processo di lavorazione, fino al rilascio ed alla manutenzione successiva. La pianificazione riguarda
anche le risorse, il personale, le skills del personale, monitorare la produzione.
Con le metodologie agili, si cerca di limitare l’attività di pianificazione del processo, anzi si scrive e si documenta il
meno possibile, mentre si sviluppa, anche per piccoli incrementi, il più rapidamente possibile. Sono metodologie
people-oriented anziché process-oriented, (dove per people si considerano anche gli stessi sviluppatori) l’approccio
prevede di adattare il processo alla natura dell’uomo. Lo sviluppo software deve diventare un’attività piacevole per
chi lo opera.
Anche per le metodologie agile l’ambito d’uso è che i requisiti siano pochi chiari o instabili, addirittura variabili nel
tempo. L’approccio è flessibile (quindi è un modello evolutivo) e “leggero”, poiché si parte dal presupposto che le
numerose prescrizioni da seguire, la quantità di documenti richiesta e un’eccessiva rigidità rendono “pesante” il
processo di sviluppo, aumentando il rischio di fallimento.

Un gruppo di circa 20 persone ha scritto il “Manifesto for Agile Software Development”: bisogna dare più valore alle
persone e all’interazione, al software che funziona e presto piuttosto che ad una documentazione chiara ed esaustiva
(ma non è la documentazione che realizziamo!), collaborazione con il cliente piuttosto che negoziazione di contratti
(nel modello a cascata si interagisce solo in fase di analisi con il cliente, al massimo nel test di accettazione, con la
negoziazione del contratto), rispondere al cambiamento, adattandosi dinamicamente ad esso, invece di seguire il
piano.
Queste metodologie possono rilasciare il software anche in maniera incrementale attraverso finestre temporali molto
piccole.
Si comincia a lavorare ad un prodotto quando si ha la commessa (nel caso del committente), si lavora nel proprio
laboratorio e poi il software viene messo in esecuzione sul campo. Ambiente di sviluppo e ambiente di esercizio sono
momenti e luoghi differenti. Ad esempio, netflix ha rimosso l’ambiente di prova, il software viene messo direttamente
in esercizio: in questo modo si riducono i tempi e dallo sviluppo si passa direttamente all’esercizio. A volte ci sono
addirittura più rilasci in un giorno. In questo caso le finestre temporali sono ore, non c’è tempo di effettuare
pianificazione, documentazione, ma dai requisiti si passa direttamente al codice.

La priorità massima delle metodologie agili è soddisfare il cliente, pertanto il cambiamento dei requisiti è ben accetto.
Le persone del business e gli sviluppatori devono lavorare insieme giorno per giorno. In certi ambiti, avere il cliente
vicino può convenire piuttosto che fare “interviste”, pianificare, e solo dopo produrre e rilasciare.
Un aspetto tipico è lavorare a coppie (pair programming), uno programma e l’altro fa il test, poi il contrario: in questo
modo il test si fa subito e viene fatto da un occhio diverso rispetto al programmatore.

Ci sono varie metodologie agili, tra le più diffuse ci sono XP e Scrum, DevOps.

XP (eXtreme Programming) è di Kent Beck (uno degli autori del manifesto) ed è la metodologia agile più diffusa. Si
sviluppa per iterazioni molto veloci, che rilasciano piccoli incrementi, magari fatti con pair programming, mettendo
più coppie a lavorare. Il committente partecipa attivamente al team sviluppo, il software viene testato continuamente
per verificare cosa funziona e cosa no. Il risultato è un processo che combina disciplina e adattività.
Le pratiche sono: planning game (determinare obiettivo e tempi della prossima release; durano poco), small releases
(rilasciare velocemente un piccolo incremento, quindi saranno necessari programmatori esperti), simple design (il
sistema è concepito nel modo più semplice possibile, non perdendo tempo nelle grosse formalizzazioni del design),
refactoring (ristrutturare il sistema senza cambiarne il comportamento), testing. Si parla infatti di Test Driven
Development (TDD): lo sviluppo è guidato dal test, in pratica se si lavora in tempo reale comincia addirittura prima
quello che fa il test, ancora prima di chi sviluppa (pair programming). Un’altra pratica è il collective ownership:
chiunque può cambiare qualsiasi parte del codice quando vuole, il software non viene visto come un prodotto
artigianale. Tramite l’Ingengeria del Software si passa dal software come artigianato al software come industria. Del
resto, il software non è brevettabile, gli algoritmi non sono brevettabili ma sono solo soggetti a copyright, dato che ci
si chiede se esso sia effettivamente frutto dell’ingegno.

Scrum è un’altra metodologia agile, dove scrum significa mischia nel rugby. E’ un modello iterativo ed incrementale,
adottato negli anni 90, per lo sviluppo e gestione di ogni tipologia di prodotto. E’ suddiviso in tre fasi:
- il pregame: si fa il planning e si stabilisce l’architettura del rilascio, in maniera rapida;
- development (game): si sviluppa tramite piccole applicazioni, dette sprint;
- post-game: contiene la chiusura definitiva della release.
Lo sprint è un ciclo iterativo in cui vengono sviluppate o migliorate una serie di funzionalità. Ogni sprint include
tradizionali fasi di sviluppo del software e può durare da una settimana ad un mese. Uno sprint è gestito da uno scrum
master (ogni gruppo ha il suo gestore). Esiste anche il ruolo del product owner, che rappresenta gli stakeholders, e
infine c’è il team, ovvero il gruppo che esegue analisi, progettazione, implementazione e test.
Giornalmente si fanno i meeting e durano al
massimo un quarto d’ora.
La backlog è la coda di cose da svolgere, questa coda
viene assegnata, sequenzialmente o secondo altri
criteri, a dei team che fanno gli sprint e vengono
monitorati ogni 24 ore. Ogni sprint non deve durare
più di 30 giorni.

DevOps è la frontiera più recente, sta per development operations. Fa parte delle metodologie evolutive, più che delle
agili. Mettere in esercizio il software comporta un mondo di problematiche dovuto al numero di utenti e di richieste:
tutto ciò non attiene allo sviluppo ma all’operation. Spesso chi sviluppa non tiene conto di queste problematiche, come
abbiamo visto nell’esempio del bancomat. DevOps nasce proprio per tenere insieme queste scelte: c’è enfasi sul
feedback della fase operazionale, la fase della messa in esercizio. C’è continua interazione, sviluppo e rilascio. Oggi si
usa molto per le tecnologie cloud, di containers o di virtualizzazioni. Su un nodo possiamo mettere più macchine
virtuali: se però un cloud fallisse su un nodo, sarà causato il fallimento anche degli altri nodi. Dato che non possiamo
permettere ciò, ogni software utente va confinato in una macchina virtuale, se il software fallisce, è la macchina
virtuale a fallire ma non il nodo, quindi l’altra macchina virtuale non sarà toccata. Un altro vantaggio è la sicurezza:
dobbiamo garantire che i dati immessi non siano visti dagli altri clienti del nodo. Ciò si fa con tecnologie di
virtualizzazione e di contenitore. Ognuno ha un contenitore, un ambiente che contenga sul nodo. In questo modo
utilizziamo la macchina virtuale isolatamente dal resto.

Potrebbero piacerti anche