Sei sulla pagina 1di 34

INGEGNERIA DEL SOFTWARE

SLIDES 1: INTRODUZIONE
Ciclo di vita del software:
- Raccolta dei requisiti
- Analisi/specifica dei requisiti
- System Design
- Object Design
- Implementazione
- Testing
Il software engineering si occupa di problem solving. Precisamente, risoluzione dei problemi di un cliente
mediante i mezzi della computer science.
Un software ha costi elevati. Il costo è espresso in manpower (mesi/uomo).
La manutenzione costa più dello sviluppo del software in se: in un ciclo di vita di un software, il 75% è
dedicato alla manutenzione, il 25% restante allo sviluppo (il cui tempo è diviso 50% per analisi,
progettazione e coding, l’altro 50% in testing).
La qualità del software può essere misurata
(da federica unina..)
Metodologie di sviluppo: modelli processo
L’approccio Tayloristico è stato per decenni il fondamento dell’industria mondiale. Prevedeva:
- gestione gerarchica
- passi fissi, non fluidi
- separare nettamente le postazioni di lavoro
- il lavoro una volta suddiviso, diventa un singolo task
- processo orientato al prodotto e non al cliente
- processo ripetibile
Una prima applicazione di tale approccio fu il modello a cascata.
- progressione lineare di fasi, senza ricicli
- nessuna overlap tra fasi
- uscite intermedie
- consente un controllo del processo
contro: interazione con l’utente solo all’inizio e alla fine del processo. Il sistema diventa installabile solo a
fine processo.
pro: facilmente comprensibile ed applicabile.
Così si è pensato di introdurre prototipi usa e getta ai fini di avere una prima implementazione da
considerare come prova con lo scopo di verificare l’accettabilità del prodotto. Prototipi usa e getta: mockup,
breadboards.
Quindi passa ad un modello di prototipazione evolutiva, costituito da poche fasi che si ripetono:
- realizzazione di un artefatto
- consegna al cliente
- feedback
- modificare il progetto in base a tali feedback
Si passa al modello a spirale (applicazione del modello evolutivo al modello a cascata).
Ogni fase inizia con un obiettivo di design e termina con un’interazione con il cliente.
Ogni ciclo di spirale si compone di quattro fasi. Il raggio rappresenta il costo accumulato e la dimensione
angolare rappresenta il progresso nel processo.

SLIDES 2: REQUIREMENT ENGINEERING


Processo/metodo per sviluppare software. Suddiviso in fasi: Understand, Develop, Validate.
Fase 1: Studio di fattibilità (valutazione costi, stabilire se avviare un progetto o meno. OUTPUT: definizione
preliminare del problema, strategie di risoluzione, costi e tempi per ogni strategia.)
Lo studio di fattibilità rientra nella Raccolta – Analisi/specifiche dei requisiti.
Punto chiave di questa prima fase: CHE COSA? In questo caso vanno descritte le caratteristiche di qualità
che l’applicazione deve soddisfare. Non si bada ancora a COME sviluppare queste caratteristiche.
OUTPUT: documento di specifica dei requisiti.
Fase 2: Progettazione (System/Object design):
- definizione di una struttura del software
- scomposizione del problema in moduli
- distinzione tra System design (struttura modulare complessiva) e Object design (dettagli interni di ciascun
modulo).
Punto chiave di questa seconda fase: COME?
OUTPUT: documento di specifica di progetto (UML)
Fase 3: Implementazione/testing/installazione/manutenzione.
La prima fase è una fase molto creativa e ricca di ambiguità. I requisiti riguardano Cosa deve fare il sistema
+ Vincoli operativi. Raccolti i requisiti, si passa alla creazione di una documentazione che poi devono essere
validati dal cliente.
La raccolta dei requisiti richiede la collaborazione tra più gruppi partecipanti con diversi background. In
particolare si distingue la figura dello stakeholder: “insieme dei soggetti che hanno un interesse nei
confronti di un’organizzazione e che con il loro comportamento possono influenzarne l’attività”. Sono figure
necessarie per far sì che lo sviluppatore possa avere una conoscenza del dominio di applicazione (ambiente
degli utenti finali).
Gli errori introdotti in questa fase sono difficili e costosi da risolvere durante il ciclo di vita del software.
Tipi di requisiti:
- Requisiti utente: servizi richiesti dal sistema, scritti in linguaggio naturale, espressi dal cliente.
- Requisiti di sistema: formulazione dettagliata e strutturata di servizi e vincoli, scritti in linguaggio naturale,
espressi dallo sviluppatore (che può utilizzarli con il cliente, quindi non si entra strettamente in un linguaggio
tecnico).
- Requisiti funzionali: servizi o funzioni offerti dal sistema (es. quando l’utente richiede la visualizzazione
del conto, allora il sistema deve..). Descrivono le interazioni tra sistema e il suo ambiente.
Devono essere completi (devono indicare tutti i servizi richiesti dal cliente) e coerenti.
- Requisiti non funzionali: vincoli sui servizi offerti dal sistema (es. la visualizzazione dell’estratto conto
deve avvenire entro 4 secondi dalla sua richiesta).
I requisiti non funzionali dovrebbero sempre essere verificabili.
Descrivono gli aspetti del sistema che non sono direttamente legati al comportamento del sistema.
- Requisiti di dominio: caratteristiche del sistema che derivano da caratteristiche generali del dominio
applicativo.
Modello FURPS:
Funcionality (requisiti funzionali)
Usability – usabilità (requisiti non funzionali)
- facilità per l’utente di imparare ad utilizzare il sistema e a capire il suo funzionamento.
Reliability – affidabilità (requisiti non funzionali)
- capacità del sistema ad offrire un servizio sotto certe condizioni e per un periodo di tempo.
Performance (requisiti non funzionali)
- attributi quantificabili del sistema come tempi di risposta.
Supportability – supportabilità (requisiti non funzionali)
- semplicità di fare modifiche dopo lo sviluppo: adattabilità ad altri sistemi e mantenibilità.
La validazione dei requisiti è un passo critico nel processo di sviluppo. I requisiti vanno continuamente
validati dal cliente e dagli utenti. La validazione riguarda il controllo di correttezza, completezza, coerenza,
chiarezza, realismo, verificabilità, tracciabilità.

GLI USE CASE DIAGRAMS


Contiene:
- attori
- casi d’uso
- relazioni d’associazione, dipendenza e generalizzazione
Usato per modellare il contesto di un sistema:
- gli attori sono esterni al sistema
- i casi d’uso sono all’interno del sistema
Viene usato per modellare i requisiti funzionali. Mostra le modalità di utilizzo del sistema, chi le usa (attori)
e le relazioni tra attori e casi d’uso.
Attore: è qualcuno o qualcosa che:
- controlla le funzionalità
- fornisce input o riceve output dal sistema
- un attore modella un’entità esterna che comunica col sistema
- ogni attore ha un nome univoco
Use case: è un’unità funzionale parte del sistema.
Un caso d’uso rappresenta una funzionalità offerta dal sistema.
Relazioni principali:
Association: relazioni semplici tra attori e casi d’uso.
Include: fattorizza proprietà comuni
Extend: identifica comportamenti simili. A può essere visto come variante di B.
Generalization: si applica sia ad attori che a use case. A eredita il comportamento di B. A può essere
sostituito con ogni occorrenza di B.

Obiettivo dello UC: ciò che il sistema dovrebbe fare secondo gli utenti.
Passi per la costruzione di uno Use Case Diagram:
- identificazione degli attori
- identificazione dei casi d’uso
- definizione delle associazioni tra attori e casi d’uso
- descrizione dei casi d’uso
- strutturazione dei casi d’uso
Per ogni Use case è necessaria una descrizione testuale dettagliata, presente successivamente nel documento
dei requisiti software. L’obiettivo è di specificare in ogni aspetto l’interazione attore/i sistema. Una di queste
rappresentazioni è il template di Cockburn.
NB: LO USE CASE DIAGRAM è UN DIAGRAMMA SEMPLICE. UN UC COMPLESSO è INDICE DI
UN CATTIVO ANALISTA.

SLIDES 4: I CLASS DIAGRAMS


Il class diagram descrive le classi e le relazioni tra esse. È il cuore della progettazione di un sistema.
Una classe ha due proprietà: attributi, che descrivono lo stato, e metodi, che descrivono il comportamento.
I termini oggetto oppure istanza di classe sono interscambiabili.
Le operazioni (metodi) di una classe sono di due tipi: operazioni di selezione (che accedono allo stato
dell’oggetto senza alterarlo) e operazioni di modifica (che alterano lo stato di un oggetto).
Operazioni di base per un oggetto: costruttore e distruttore.
Una Responsibility è un contratto o una obbligazione di una classe. Può essere indicata in maniera testuale o
con linguaggi formali come OCL.
E’ possibile modificare la visibilità di attributi e operazioni:
+, public: qualsiasi altra classe con visibilità alla classe data può usare l’attributo/operazione.
#, protected: qualsiasi classe discendente della classe data può usare l’attributo/operazione.
-, private: solo la classe data può usare l’attributo/operazione.
E’ possibile definire una “stringa di proprietà” per specificare particolari caratteristiche delle features di una
classe. Si indica tra parentesi graffe affianco alla definizione della feature.
Per gli attributi si hanno questi tre valori: changeable, addOnly, readOnly.
Per i metodi: isQuery, sequential, guarded, concurrent (le ultime tre vengono usate per sistemi
multithread).
es. - titolo: String[1] = “sw engineering” {readOnly}
+ getSaldo (data: Date): double {isQuery}
In UML le connessioni tra classi avvengono mediante le relazioni.
Queste sono di tre tipi: associazioni, generalizzazioni/specializzazioni, dipendenze.
Quando un’istanza di classe passa un messaggio ad un’istanza di un’altra classe, si sottintende l’esistenza di
un’associazione tra le due classi.
Associazioni:
possono essere riflessive o n-arie. La molteplicità di un’associazione indica se questa è obbligatoria oppure
no e indica il numero minimo e massimo di oggetti che possono essere relazionati ad un altro oggetto.
Un ruolo può definire meglio la posizione di una classe in un’associazione.
Aggregazioni:
è la relazione “parte di”. Gli elementi aggregati hanno una loro esistenza a prescindere dall’aggregazione.
Composizioni:
è una relazione più forte. Tutte le parti hanno lo stesso ciclo di vita dell’insieme (all’atto della distruzione
della relazione si ha la propagazione della distruzione degli oggetti che ne fanno parte).
Ereditarietà: generalizzazioni/specializzazioni:
la classe generale è detta superclasse, ogni classe specializzata viene detta sottoclasse.
Tramite l’ereditarietà, può avvenire una condivisione delle proprietà degli oggetti seguendo un ordine
gerarchico. Tutte le features di una superclasse sono ereditate dalle sottoclassi (anche le relazioni).
Dipendenze:
relazione secondo cui un cambiamento di una classe indipendente può influenzare la classe dipendente.
Altri elementi: interfacce e classi abstract.
Un’interfaccia viene modellata allo stesso modo in cui viene modellato il comportamento di una classe e
definisce una serie di operazioni che una classe offre ad altre classi.
Un’interfaccia non ha attributi ma soltanto operazioni.
La relazione tra classe e interfaccia viene chiamata realizzazione, e viene indicata con una linea tratteggiata.
Una classe abstract definisce un comportamento “generico”. Può essere implementate parzialmente il
comportamento, ma gran parte della classe è lasciato indefinito. Vengono indicate con il nome in corsivo.
Euristica THREE-OBJECT-TYPE:
Secondo tale euristica, il modello ad oggetti di analisi classifica gli oggetti in Entity, Boundary e Control.
- Entity: rappresentano i concetti del dominio che sono persistenti. Sono concettualmente simili alle entità
identificate nella progettazione di un database. (es. Biglietti, Clienti, Prenotazioni etc).
- Boundary: sono responsabili di gestire le interazioni tra attori e sistema. (es. Button, Form, Report etc).
- Control: modellano la logica che si occupa di utilizzare gli use case (in genere, uno per caso d’uso).
L’approccio three-object-type porta a modelli più flessibili e facili da modificare (l’interfaccia al sistema,
rappresentata da oggetti boundary, è più soggetta a cambiamenti rispetto alle funzionalità, rappresentate da
entity e control).
Identificare gli oggetti Boundary:
tali oggetti rappresentano l’interfaccia del sistema con gli attori. In ogni use case, ogni attore interagisce
almeno con un oggetto Boundary. Gli oggetti Boundary collezionano informazioni dagli attori e le traducono
in una forma che può essere usata sia dagli oggetti Entity che Control. Gli oggetti Boundary poi mostrano
informazioni in output agli attori.
Tali oggetti modellano l’interfaccia senza descriverne gli aspetti visuali (scroll bar, menu item etc).
Linee guida per l’identificazione:
- Identificare i controlli della UI di cui l’utente ha bisogno per iniziare lo use case.
- Identificare i form di cui l’utente ha bisogno per inserire dati nel sistema.
- Identificare avvisi e messaggi che il sistema usa per rispondere all’utente.
- Non modellare aspetti visuali della UI con oggetti Boundary.
- Usare sempre i termini dell’utente finale per descrivere l’interfaccia, non usare termini del dominio
d’implementazione.
Identificare gli oggetti Control:
tali oggetti sono responsabili del coordinamento degli oggetti Boundary ed Entity. Si preoccupano di
collezionare informazioni dagli oggetti Boundary e di inviarle agli oggetti Entity.
Non hanno solitamente una controparte nel mondo reale: modellano la logica di funzionamento di un caso
d’uso.
In uno use case, un oggetto Control è creato all’inizio dello use case e cessa di esistere alla fine.
Linee guida per l’identificazione:
- Identificare almeno un oggetto Control per ogni use case.
- La vita di un oggetto Control dovrebbe corrispondere alla durata di uno use case o di una sessione utente.
Se è difficile identificare l’inizio e la fine dell’attivazione di un oggetto Control, il corrispondente use case
probabilmente non ha delle entry e exit condition ben definite.

SLIDES 5: I SEQUENCE DIAGRAMS


Mostra la sequenza temporale dei messaggi che due oggetti si scambiano per portare a termine una
funzionalità.
In fase di analisi sono utilizzati per individuare nuovi oggetti e comportamenti mancanti
La ricezione di un messaggio determina l’attivazione di un metodo (rappresentata da un rettangolo sulla linea
della vita). Il tempo procede dall’alto verso il basso. La creazione di un oggetto è contraddistinta dal
messaggio <<create>>.
In generale, un messaggio rappresenta il trasferimento del controllo da un oggetto all’altro.
E’ possibile determinare cicli (si indicano con una cornice intorno alla parte del sequence che è soggetta
all’opzione).
Rappresentazioni:
if -> OPT [condition]
if/else -> ALT [condition], separati da una linea orizzontale tratteggiata
cicli -> LOOP [condition o items su cui ciclare].
Se un sequence è troppo articolato o fa riferimento ad un altro sequence, si può utilizzare il REF.
Convenzioni per utilizzare i sequence con diagrammi three-object-type in fase di analisi:
- la colonna più a sinistra rappresenta l’attore che inizia lo use case.
- la seconda colonna rappresenta l’oggetto Boundary con cui l’attore interagisce per iniziare lo use case.
- la terza colonna rappresenta l’oggetto Control che gestisce il resto dello use case.
- le altre colonne possono rappresentare qualsiasi altro oggetto che interagisce con lo use case.
Gli oggetti Control accedono ad oggetti Entity e Boundary.
Gli oggetti Entity non accedono mai agli oggetti Control e Boundary.

SLIDES 6: ACTIVITY DIAGRAMS


Forniscono la sequenza di operazioni che definiscono un’attività più complessa.
Permettono di rappresentare processi paralleli e la loro sincronizzazione.
Può essere associato ad uno use case, ad una classe o all’implementazione di un’operazione.
Utili per modellare:
- comportamenti sequenziali
- non determinismo
- concorrenza
- sistemi distribuiti
- business workflow
- operazioni
Da cosa è composto:
Activity: esecuzione non atomica in uno state machine.
Transition: flusso di controllo tra due action successive.
Guard expression: espressione booleana che dev’essere verificata per attivare una transition.
Branch: percorsi alternativi in base a espressioni booleane.
Synchronization bar: flussi concorrenti (fork, join).
Swimlanes (tradotto, corsie di nuoto): costrutto grafico rappresentante un insieme partizionato di
action/activity. Identificano le responsabilità relative alle diverse operazioni (parti di un oggetto, oggetti
diversi).

SLIDES 7: STATECHARTS
Diagramma utilizzato per descrivere il comportamento di un artefatto composto da un numero finito di stati.
E’ composto da stati rilevanti dell’entità e transizioni che determinano passaggi di stati.
Vantaggio principale: grande impatto visivo e grande leggibilità.
Le transizioni sono generalmente lanciate da eventi.
Vengono usati principalmente per descrivere il comportamento di:
- Use Cases
- Classi
Ogni oggetto ha interazioni, durante il suo ciclo di vita, con oggetti vicini. Deve quindi rispondere sempre a
stimoli asincroni. Esistono inoltre oggetti il cui comportamento è determinato dallo stato dei suoi attributi.
Per descrivere queste situazioni, viene utilizzato lo Statechart: è un diagramma chiave per guidare i
programmatori nell’implementazione di sistema.
Un evento è un avvenimento significativo per un artefatto.
Uno stato è una condizione di un oggetto compreso in un intervallo di tempo compreso tra due eventi.
Una transizione è una relazione che lega uno stato di partenza ad uno di arrivo.
Uno stato è rappresentato da un rettangolo arrotondato.
Le transizioni sono etichettate con tre elementi, tutte opzionali: Evento [condizione] / Azione.
Evento: trigger. Stimolo per innescare una transizione tra stati.
Condizione (guard): espressione booleana.
Azione: è associata alla transizione, è considerata un processo rapido, atomico.
Uno stato può opzionalmente avere le seguenti azioni:
- Entry: azione che viene eseguita ogni volta che si entra nello stato.
- Exit: azione che viene eseguita ogni volta che si esce dallo stato.
- Do: associato allo stato, non atomica.
Esistono transizioni senza trigger, chiamate appunto transizioni triggerless. Al termine dell’azione del primo
stato, si passa automaticamente al secondo stato.
Uno stato può contenere sottostati.
Uno stato può essere decomposto ortogonalmente (in sottostati mutualmente esclusivi), concorrentemente (in
sottostati concorrenti). Sono ammesse anche transizioni che entrano in uno stato interno.
Quando si entra in uno stato composito, la macchina a stati inizia dal suo stato iniziale. Esistono situazioni in
cui vorremmo poter memorizzare quale sottostato era attivo nel momento in cui si abbandona uno stato
composito, così da poter riprendere le attività da quel punto (una volta rientrati nello stato composito).
Si parla di History states.
Esistono due stati, H e H*.
Lo stato H si chiama history state superficiale. Nel caso di macchina a più livelli, H ricorda soltanto lo stato
del primo livello.
Lo stato H* è detto history state profondo, utile per ricordare lo stato di ogni livello.
Linee guida:
- il nome dello stato deve riflettere un intervallo di tempo reale.
- dev’essere possibili uscire da ogni stato.
- non confondere evento (causa) con azione (effetto).
- nomi non ambigui.
- condizioni sugli eventi hanno valore booleano.
- azioni e condizioni sono opzionali: da usare solo se necessari.
- check finale: abbiamo descritto tutti i possibili comportamenti del sistema?
ARCHITETTURE SOFTWARE
Un’architettura software è la struttura del sistema.
Definire un’architettura significa mappare funzionalità su moduli (es. modulo di interfaccia utente, modulo
di accesso a db, modulo di gestione della sicurezza etc…).

La definizione dell’architettura logica di un grande sistema è ottenuta decomponendolo in sottosistemi,


usando layer e partizioni.
- un layer è uno strato che fornisce servizi (es. interfaccia utente, accesso al DB). È un raggruppamento di
sottosistemi che forniscono servizi correlati. Può dipendere solo dal layer più basso e non ha conoscenza dei
layer più alti.
Macchina virtuale: prima formalizzazione di architettura a layers.
Architettura chiusa: ogni layer può accedere solo al layer immediatamente sotto di esso. Comporta alta
mantenibilità e portabilità.
Architettura aperta: ogni layer può accedere anche ai layer di livello più basso. Comporta efficienza a run
time.
- una partizione è un’organizzazione di moduli (es. funzionalità carrello di un sito di commercio).
Suddivisione del sistema in sottosistemi paritari (peer) fra loro, ognuno responsabile di servizi.
In generale, una decomposizione in sottosistemi è risultato di un’attività di layering e partitioning.

Principali architetture definite:


- Repository Architecture: i sottosistemi accedono ad una singola struttura dati, chiamata Repository.
Vantaggi: molto efficiente per condividere grandi moli di dati. Un sottosistema non si deve preoccupare si
come i dati sono prodotti/usati da ogni altro sottosistema. Gestione centralizzata di backup, security etc…
Svantaggi: I sottosistemi devono concordare su un modello di dati compromesso -> minori performance.
l’adozione di un nuovo modello di dati è difficile e costosa. Diversi sottosistemi possono avere diversi
requisiti di backup,security etc… non supportati.
- Client/Server Architecture: è un’architettura distribuita dove dati ed elaborazione sono distribuiti su una
reta di nodi di due tipi: i server che offrono servizi specifici e i client che utilizzano tali servizi.
- Peer-To-Peer Architecture: ogni sottosistema può agire sia da server che da client.
Strati funzionali dell’architettura software
Suddivisione three-tier:
Ogni applicazione software è suddivisa logicamente in tre parti:
- presentazione che gestisce l’interfaccia utente
- logica applicativa vera e propria
- gestione di dati persistenti su database
Svantaggio: problemi ad integrare Legacy Software.
Vantaggi: ricerca di bug più mirata, alta mantenibilità, l’aggiunta di funzionalità è mirata ad uno solo dei tre
strati.
Suddivisione n-tier:
elementi fondamentali:
- interfaccia utente
- presentation logic
- business logic
- infrastructure services

SLIDES 8 – SYSTEM DESIGN


L’ultimo obiettivo del system/object design è di definire un nuovo documento, scritto in linguaggio
tecnico/formale, da dare ai programmatori affinchè questi siano in grado di implementare il software
descritto nel documento dei requisiti software.
Si passa quindi dal momento di analisi al modello di design del sistema.
Scopo del system design:
- definire gli obiettivi di design del progetto
- definire l’architettura del progetto, decomponendolo in sottosistemi più piccoli che possono essere
realizzati da team individuali
- Selezionare le strategie per costruire il sistema (strategie hardware/software, strategie relative alla gestione
dei dati persistenti etc.)
Scopo dell’object design:
- data l’architettura del sistema, specificare il dettaglio realizzativo dei sottosistemi più piccoli
- selezionare le strategie ottimali per la realizzazione (design pattern)
OUTPUT DEL SYSTEM DESIGN: Documentazione UML del sistema e dei sottosistemi.
Il system design
In questa fase non abbiamo più a che fare con il cliente, ma bensì con i programmatori che implementeranno
il software (quindi si passa ad un linguaggio più tecnico visto che avremo a che fare con informatici).
È suddiviso in tre attività principali:
- identificazione degli obiettivi di design.
- progettazione della decomposizione del sistema in sottosistemi.
- raffinamento della decomposizione in sottosistemi per rispettare gli obiettivi di design.
Il primo passo è molto importante, serve ad identificare le qualità su cui è focalizzato il sistema. Molti design
goal possono essere ricavati dai requisiti non funzionali o dal dominio di applicazione. È importante
formalizzarli esplicitamente poiché ogni decisione di design dev’essere fatta seguendo lo stesso insieme di
criteri.
I criteri possono essere riassunti in 5 punti:
- Performance
- Affidabilità
- Costi
- Manutenzione
- Usabilità
Solitamente si finisce a rispettare a pieno uno di questi 5 gruppi di criteri, visto che molto spesso vanno in
contrasto.
Concetti di system design
Molti criteri di design sono strettamente dipendenti dal problema trattato (la scelta del miglior criterio viene
affidata poi all’analista che opera in base alla propria esperienza/sensibilità). Esistono però dei criteri di
design che valgono per qualunque software object oriented.
Quando progettiamo una classe dobbiamo sforzarci a pensare a quali cambiamenti potranno essere richiesti
in futuro. Grazie a queste scelte, facilitiamo l’evoluzione futura del software.
Per ridurre la complessità del sistema, decomponiamolo in sottosistemi (un sottosistema corrisponde
tipicamente alla parte di lavoro che può essere svolta in maniera autonoma da un team di sviluppo).
Nel caso di sottosistemi complessi, li possiamo ancora decomporre in sottosistemi più semplici.
Un sottosistema è caratterizzato da servizi che offre ad altri sottosistemi. Il system design si focalizza sulla
definizione dei servizi forniti dai sottosistemi in termini di operazione, loro parametri e loro comportamento
ad alto livello.
In questo esempio abbiamo ridotto le relazioni tra classi ma abbiamo aumentato la complessità.
Qual è il giusto numero di componenti?
La suddivisione del sistema in sottosistemi può essere guidata da due fattori qualitativi fondamentali:
- coesione
- accoppiamento
L’obiettivo è l’indipendenza funzionale dei sottosistemi che si realizza con alta coesione e basso
accoppiamento.

La coesione è una misura di quanto siano fortemente relate e mirate le responsabilità di un modulo.
Se ciascuna unità è responsabile di un singolo compito, parleremo di alta coesione.
Un’alta coesione è una proprietà desiderabile del codice poiché permette:
- di comprendere meglio i ruoli di una classe
- di riutilizzare una classe
- di manutenere una classe
- di limitare e focalizzare i cambiamenti del codice
- utilizzare nomi efficaci.
Coesione dei metodi: un metodo dovrebbe essere responsabile di un solo compito.
Coesione delle classi: ogni classe dovrebbe rappresentare un solo concetto ben definito.
Indicatori di alta coesione di una classe:
- ha delle responsabilità moderate, limitate ad una singola area funzionale.
- collabora con altre classe per completare dei tasks.
- ha un numero limitato di metodi, cioè di funzionalità altamente legate tra loro.
- tutti i metodi sembrano appartenere ad uno stesso insieme, con un obiettivo globale simile.
- la classe è facile da comprendere.

L’accoppiamento è una misura di quanto fortemente una classe è connessa con altre classi e di quanto si
basa su di esse.
Due o più classi si dicono accoppiate quando è impossibile modificare una senza dover modificare l’altra.
Se due classi dipendono strettamente l’una dall’altra, si parlerà di forte accoppiamento.
Per un codice di qualità dobbiamo puntare ad un basso accoppiamento.
Un basso accoppiamento è una proprietà desiderabile del codice poiché permette di:
- capire il codice di una classe senza dover leggere i dettagli delle altre.
- modificare una classe senza che le modifiche comportino conseguenze sulle altre classi.
- riutilizzare una classe senza dover importare necessariamente anche altre classi.
Un basso accoppiamento migliora la manutenibilità del software.
Linea guida: legge di Demetra.
Concetto base: spingere al massimo l’information hiding.
Un metodo M di un oggetto O dovrebbe soltanto mandare messaggi a:
- il proprio oggetto
- un parametro del metodo
- una variabile di istanza dell’oggetto contenente
- un oggetto creato all’interno del metodo
Come ottenere alta coesione e basso accoppiamento?
Basta pensare alla progettazione di un sistema in termini di classe, responsabilità e collaborazioni.
Ogni classe deve avere delle responsibility e dei ruoli ben precisi (alta coesione).
La classe che possiede i dati dev’essere responsabile di processarli (basso accoppiamento).
NB:
- se non riesco a definire chiaramente le responsabilità di una classe, c’è un errore di design.
- se una classe ha bisogno di troppe classi collaboratrici, c’è un errore di design.
- una classe è troppo complessa se rappresenta più di un concetto.
- un metodo è troppo complesso se è responsabile di più di un compito logico.

SLIDES 10 - PROJECT MANAGEMENT


Un progetto è una serie di attività che devono essere svolte in un intervallo temporale definito, finalizzate al
raggiungimento di un obiettivo e di un risultato specifico, a cui sono assegnate risorse limitate.
Punti chiave:
- definire un obiettivo da raggiungere.
- pianificare il modo con cui ottenere il risultato.
- definire le risorse disponibili/necessarie per raggiungere il risultato.
- predefinire checkpoint.
- effettuare controlli periodici correggendo i gap rispetto a quanto pianificato.
- effettuare la valutazione del risultato raggiunto.
Condizioni di successo:
- Condizioni organizzative razionali (definizione degli obiettivi, competenze professionali, delle risorse
tecniche e strumentali dei tempi e dei costi).
- Condizioni organizzative relazionali (dinamiche interne al gruppo di progetto, relazioni tra il gruppo e
attori esterni ad esso, dinamiche organizzative che influenzano il contesto operativo).
Ciclo di vita:
Fase iniziale: ideazione, pianificazione.
Fase intermedia: esecuzione.
Fase finale: chiusura e valutazione.
Il project manager non ha potere gerarchico. È una figura che deve possedere tre tipologie di competenze:
specialistiche, gestionali, relazionali.
Il PM:
- coordina e supervisiona la definizione del progetto.
- valuta l’andamento del progetto suggerendo eventuali azioni correttive e garantisce l’espletamento di tutte
le attività.
- ottiene il consenso e il giusto coinvolgimento di ogni partecipante al progetto.
- mantiene aggiornato il management sull’andamento del progetto.
- garantisce il rispetto del livello di qualità.
Le attività utili alla realizzazione dell’obiettivo vengono definite attraverso uno strumento chiamato WBS
(Work Breakdown Structure).
È una struttura ad albero che dall’obiettivo finale (radice) procede per suddivisioni successive in sotto tasks
fino al livello del “work package” (unità minima di attività).
Definisce le attività da svolgere nel dettaglio. Alimenta di informazioni i diagrammi e i reticoli del progetto.
Sintetizza i costi e lo stato di un programma per successivi livelli gestionali. Il “work package”, è l’insieme
di attività necessarie per svolgere un compito specifico o un processo.
Regole per la costruzione di un WBS:
un WBE (Work Breakdown Element) rappresenta un’attività per la quale deve essere possibile definire:
- una precisa descrizione del lavoro da compiere.
- la durata.
- le risorse che realizzeranno l’attività e la responsabilità dell’esecuzione.
- il costo.
- le rilevazioni di avanzamento lavori.
Ogni WBE è collegato ad uno e uno solo degli elementi di livello superiore.
La WBS deve includere il 100% del lavoro definito dal progetto e includere tutto il necessario –
interno, esterno e appaltato – alla realizzazione del progetto, inclusa la gestione del progetto stesso (la
regola va applicata ad ogni sotto albero: la somma del lavoro dei livelli “figli” dev’essere uguale al 100% del
lavoro del livello “padre”).
La WBS mira a definire i work package. Un WP deve contenere le seguenti informazioni:
- cosa fare.
- responsabile e committente.
- costo e tempi.
- prodotti input e output.
- task interni.
Task: unità di lavoro atomica.
Specifica di un task:
- nome e descrizione del lavoro che dev’essere fatto.
- precondizioni per poter avviare il lavoro, durata, risorse necessarie.
- risultati sul lavoro e criteri di accettabilità.
- rischi.
Un Deliverable è un qualsiasi risultato del progetto che tipicamente viene condiviso con uno Stakeholder
(può essere un prodotto, un risultato o della documentazione).
In un progetto, le attività devono essere organizzate in modo da produrre dei risultati valutabili dal
management (Milestones).
Le Milestones sono momenti decisivi dell’evoluzione del progetto, quali punti intermedi o una realizzazione
significativa del progetto.
Le Milestones sono spesso imposte dal progetto o auto imposte dal project manager.
L’obiettivo principale è di valutare se l’andamento del progetto sta rispettando il plan originale.
Scheduling di progetto:
- divide il progetto in attività e mansioni e stima il tempo necessario per portarle a termine.
- organizza le mansioni in modo concorrente per ottimizzare la forza lavoro.
- minimizza la dipendenza tra le singole mansioni per evitare ritardi dovuto all’attesa del completamento di
un’altra mansione.
- sono necessari intuito ed esperienza.
Lo scheduling prevede diverse problematiche come la difficoltà di prevedere costi e tempi e imprevisti che
possono accadere.

Le responsabilità delle attività vengono attribuite mediante uno strumento chiamato OBS (Organizational
Breakdown Structure).
Permette di identificare le competenze richieste nelle attività di progetto.

TIPOLOGIE DI TEAM:
Democratico decentralizzato:
- assenza di un leader permanente.
- consenso di gruppo sulle soluzioni e sulla organizzazione del lavoro.
- comunicazione orizzontale.
Vantaggi: attitudine positiva a ricercare presto gli errori.
funziona bene per problemi “difficili”.
Svantaggi: è difficile da imporre.
non è scalabile.
Controllato decentralizzato:
- un leader riconosciuto che coordina il lavoro
- la risoluzione dei problemi è di gruppo, ma l’implementazione delle soluzioni è assegnata a sottogruppi da
parte del leader.
- comunicazione orizzontale con i sottogruppi e verticale con i leader.
Controllato centralizzato:
- il team leader decide sulle soluzioni e sull’organizzazione.
- comunicazione verticale tra team leader e altri membri.
FORMALISMI PER IL PROJECT MANAGEMENT.
Grafo delle attività (PERT) e diagramma di Gantt (diverse rappresentazioni grafiche dello scheduling del
progetto).
PERT evidenzia le dipendenze e il cammino critico (percorso costituite da fasi di lavoro che possono essere
compiute soltanto con l’ultima azione della fase precedente. Consente di determinare la reale durata
dell’intera attività lavorativa.).
Gantt mostra il succedersi temporale delle fasi di un progetto.
Un grafico PERT è un grafo i cui nodi sono istanti di un processo (inizi di fasi) e gli archi rappresentano
fasi, stabilendo durata e relazioni di dipendenza temporale tra fasi.
C’è sempre un nodo iniziale “inizio del processo” e un nodo finale “fine del processo”. Gli altri nodi
formano una rete di attività che procedono in parallelo o in sequenza.
Cronogramma di Gantt è una rappresentazione su scala temporale dell’evoluzione del progetto.
Ogni barra rappresenta un’attività, la lunghezza di ognuna di esse è proporzionale alla durata dell’attività che
rappresenta e viene collocata sulla scala temporale in prossimità dell’attività stessa.
Limite: non sono evidenziati i legami tra le attività, né le risorse deputate al loro svolgimento.
Un diagramma di Gantt permette di rappresentare graficamente un calendario di attività.
Il diagramma è costruito da:
- un asse orizzontale (tempo).
- un asse verticale (mansioni).

SLIDES 12 - DESIGN PATTERNS


Fase di Object Design. In questa fase, l’analista deve scegliere tra i differenti modi di implementare i modelli
di analisi, rispettando requisiti non funzionali e design. È il passo finale prima dell’implementazione.
Obiettivi:
- identificazione delle componenti esistenti
- definizione completa delle relazioni
- definizione completa delle classi
- scelta di algoritmi e strutture dati
- individuare le possibilità di riuso
- ottimizzazione
-…
Le architetture sono un modo per definire la struttura più ad alto livello. Grazie alle architetture, c’è una
vasta scelta di soluzioni già testate per una grande classe di problemi.
Esiste qualcosa di simile a basso livello? Ci sono modi “standard” per poter combinare classi tra loro per
svolgere funzionalità tipiche? Si, Design Patterns.
Il riuso è importante: è inutile reinventare cose già esistenti e per di più testate.
Il design pattern è una soluzione comune ad un problema comune.
Astrae una struttura di design ricorrente.
Comprende classi e oggetti (dipendenze, strutture, interazioni e convenzioni).
Definisce in maniera esplicita le strutture di design.
Un design pattern ha 4 parti fondamentali: nome, problema, soluzione, conseguenza. È una soluzione
indipendente da qualsiasi linguaggio. È una “micro architettura”.
DAO (Data Access Object).
Problema: l’accesso ai dati può variare in base al tipo di storage dei dati.
Soluzione: utilizzare un pattern DAO per astrarre e incapsulare tutti gli accessi alla sorgente dati. Il DAO
gestisce la connessione con le sorgenti dati e inoltre implementa i meccanismi di accesso richiesti per
lavorare con le sorgenti dati.
Un DAO astrae le operazioni di CRUD.
Vantaggi:
- permette di collegare ogni differente metodo di storage dei dati con il minimo impatto sul resto del sistema
- incoraggia al riutilizzo del codice
Possiamo suddividerli in tre macro categorie:
Structural patterns: come gli oggetti sono composti per formare delle grandi strutture.
Vantaggi: realizza nuove funzionalità dalle vecchie funzionalità, fornisce flessibilità ed estensibilità.

Behavioral patterns: assegnamento di algoritmi e responsabilità agli oggetti


Vantaggi: basso accoppiamento per un particolare algoritmo.
Creational patterns: creazione di oggetti complessi (articolati).
Vantaggi: nasconde la complessità sulla creazione degli oggetti e su come sono messi insieme.
Esempi di Design Patterns: Singleton (creational), Composite (structural), Observer (behavioral).
Singleton:
Obiettivo: assicurarsi che una classe abbia una sola istanza

Composite:
Obiettivo: trattare in maniera omogenea oggetti singoli e multipli, definiti in maniera ricorsiva.
Observer:
Obiettivo: definire una relazione di dipendenza uno-a-molti tra oggetti, in modo che quando un oggetto
cambia stato, tutti quelli che ne sono dipendenti vengono notificati e si aggiornano di conseguenza.

SLIDES 13 – IL VERSIONING (SVN)


Implementazione. Si ha a che fare con tanto codice e con tante persone che fanno codice, quindi bisogna
gestire bene tutto.
Un problema che dev’essere affrontato è l’accesso concorrente al Repository (il Repository contiene tutto il
lavoro svolto). Se due persone hanno accesso contemporaneamente al Repository, c’è il rischio che uno dei
due può sovrascrivere il lavoro fatto dall’altro (sovrascrivendo magari un lavoro in stato più avanzato con un
altro in uno stato meno recente).
Soluzione: copy-modify-merge.
Ogni utente crea una copia in locale del progetto, detta working copy. Modifica la copia locale e la
sovrascrive nel repository. Così facendo però, prima della scrittura vengono identificati e risolti eventuali
conflitti prima della scrittura.
Ci sono diversi tool che permettono di applicare una soluzione del genere.
Un’altra soluzione può essere quella del “Distributed Version Control”, in cui ogni client ha una copia
completa del codice e la condivide con altri client (simile a una peer-to-peer).
Il SubVersioN
E’ un sistema di versioning centralizzato di Apache, memorizza tutte le informazioni in un albero di
filesystem.
Utilizza come elemento di controllo il concetto di “Repository”, ovvero di un insieme di file che tengono
traccia delle diverse versioni del progetto.
Sostanzialmente… un file server che offre più servizi avanzati.
Una Working copy è una directory in locale nella quale vengono copiati i file dal Repository. Le modifiche
effettuate in locale ai file vengono riportate sul server solo quando l’utente lo indica esplicitamente.
SVN indica con il termine revision lo stato del filesystem sul server. Le revision sono numerate con un
intero che viene incrementato automaticamente ad ogni aggiornamento del Repository. In SVN il revision
number corrisponde all’intero albero e non al singolo file.
Commit: indica al client di scrivere sul server le modifiche effettuate nella propria working copy.
Il commit è eseguito in modo atomico, per ogni commit è possibile effettuare un commento (è importante
sempre specificare nel commento le modifiche fatte).
L’esecuzione di un commit corrisponde alla creazione di una nuova revision.
Update: aggiornamento del contenuto della working copy locale.
Update e commit sono monodirezionali: update aggiorna solo il contenuto della working copy, commit
aggiorna solo il contenuto del server.
Per annullare “tutte” le modifiche effettuate a un file è possibile utilizzare il comando revert. Revert riporta
il file alla versione ottenuta dall’ultima operazione di checkout o update.
Ciclo di lavoro tipico:
- allinamento della working copy (update)
- esecuzione modifiche (add, delete, copy, move, file editing etc)
- verifica modifiche (status e diff)
- in caso… annullamento modifiche (revert)
- risoluzione conflitti (update e resove)
- aggiornamento repository (commit)
E’ possibile effettuare un branching del Repository così che ogni client può fare l’update solo della sezione
di Repository interessata.
Perché creare un branch?
Lo sviluppo di una nuova versione di un servizio potrebbe essere sviluppata localmente in autonomia, senza
eseguire il commit.
Pessima idea:
- forza la gestione del backup locale
- non è possibile avere “feedback” sul lavoro svolto
- se lo sviluppo prendere molto tempo, è possibile che il nuovo servizio non si integri perfettamente con i
vecchi (che, nel frattempo sono evoluti).
Creare un branch corrisponde alla creazione della copia di un intero progetto.
La copia può essere eseguita all’interno della working copy locale attraverso un svn copy oppure
direttamente sul repository.
La copia di una directory non duplica i file sul server ma crea “hard link”.
SVN non gestisce il branch in se.
Dopo la creazione del branch due linee di sviluppo sono indipendenti. L’esecuzione del commit su una non
modifica le altre.
SVN non impone alcuna struttura al Repository. È importante però che la politica decisa localmente sia
implementata da tutti.

SLIDES 14.2 – VERIFICA E VALIDAZIONE


Verifica: controllo che il software sia conforme alle sue specifiche (abbiamo creato bene il prodotto?)
Validazione: controllo che il software soddisfi le esigenze del cliente (abbiamo creato il giusto prodotto?)
La verifica può essere statica (se effettuata senza eseguire il codice) o dinamica (se effettuata attraverso
l’esecuzione del codice).
Le tecniche per migliorare l’affidabilità di un software si dividono in tre macro categorie:
- Fault Avoidance: tecniche per ridurre i difetti. Si tenta di prevenire l’inserimento di difetti, include
metodologie di sviluppo e verifica.
- Fault Detection: tecniche per identificare stati d’errore e trovare il difetto prima di rilasciare il sistema.
(Testing e Debugging)
- Fault Tolerance: tecniche che assumono che un sistema possa contenere bug e che i fallimenti del sistema
possano essere gestiti a run-time (transazioni atomiche e ridondanza).
Verifica statica e dinamica:
- software inspections: analisi statica delle rappresentazioni del problema per scoprire problemi.
- software testing: esecuzione ed osservazione del comportamento del prodotto.
Terminologia:
- Affidabilità: la misura del successo con cui il comportamento è conforme ad una certa specifica del relativo
comportamento.
- Fallimento (failure): qualsiasi deviazione del comportamento osservato dal comportamento specificato.
- Stato di errore (errore): il sistema è in uno stato tale che ogni ulteriore elaborazione da parte del sistema
porta ad un fallimento.
- Difetto (bug/fault): la causa di un errore.
Nota:
- non tutti i fault generare un failure
- una failure può essere generata da più fault
- un fault può generare diverse failure
Utilizziamo il termine defect quando non è importante distinguere tra fault e failure.
TEST
Un test è formato da una serie di test cases (un test case è un insieme di dati in input).
L’esecuzione del test consiste nell’esecuzione del programma per tutti i casi di test.
Un test ha successo se rileva uno o più malfunzionamenti del programma.
La condizione necessaria per effettuare un test è conoscere il comportamento atteso per poterlo confrontare
con quello generato.
L’oracolo conosce il comportamento atteso di ogni prova.
Oracolo umano: si basa sulle specifiche o sul giudizio.
Oracolo automatico: generato dalle specifiche, stesso software ma sviluppato da altri, versione precedente.
(ovviamente gli oracoli automatici sono essenziali…)
Verification e Validation dovrebbero stabilire un certo livello di fiducia in ciò che esegue il software
(ovvero, che fa il suo dovere). Questo non significa che è privo di difetti, ma bensì che deve essere
abbastanza buono per l’uso per il quale è stato pensato e proprio il tipo di uso determinerà il livello di fiducia
richiesto.
Il livello di fiducia dipende dagli scopi e funzioni del sistema, dalle attese degli utenti e dal time-to-market.
VERIFICA STATICA
Tecniche: review, ispezione, verifica formale, esecuzione simbolica, etc…
Review:
Ispezione manuale del codice sorgente.
Due tipi di review:
Walkthrough: lo sviluppatore presenta informalmente le API, il codice e la documentazione associata alle
componenti del team di review.
Inspection: simile al walkthrough, ma la presentazione delle unità è formale. Il team di review controlla le
interfacce e il codice con i requisiti, l’efficienza degli algoritmi con le richieste non funzionali. Lo
sviluppatore interviene soltanto se si richiedono chiarimenti.
Analisi informali:
Analizzare la specifica dei requisiti attraverso una simulazione manuale.
Ispezioni del codice (scoprire gli errori più comuni come variabili non inizializzate etc…).
Ispezione:
Le ispezioni trovano fault in una componente rivedendo il codice sorgente in meeting formali.
È condotta da un team di sviluppatori, incluso l’autore della componente.
Tecniche di ispezione del codice possono rivelare ed eliminare anomalie e rendere più precisi i risultati.
In poche parole, è una tecnica completamente manuale per trovare e correggere errori.
Vantaggi rispetto al testing:
- durante un test, un errore può nascondere altri errori. Ciò viene superato leggendo il codice sorgente.
- non ho necessità di sviluppare un driver-stub. Posso applicarlo a codice definitivo in unità incomplete.
- permette di valutare anche altri attributi del software (leggibilità, aderenza a standard, etc…)
- costi minori in termini di dipendenti (employees)
Ruoli nell’ispezione:
- moderatore: tipicamente proviene da un altro progetto. Presiede le sedute, sceglie i partecipanti, controlla
il processo.
- lettori, addetti ai test: leggono il codice al gruppo, cercano difetti.
- autore: partecipante passivo. Risponde a domande quando richiesto.
Processo di ispezione:
- pianificazione: scelta di partecipanti e checklist, pianificazione di meeting.
- fasi preliminari: fornire le informazioni necessarie e vengono assegnati i ruoli.
- preparazione: lettura del documento, individuazione dei difetti.
- ispezione: meeting. Raccolta e discussione dei problemi trovati dai singoli revisori, ricerca di ulteriori
difetti.
- lavoro a valle: l’autore modifica il documento per rimuovere i difetti.
- seguito: controllo delle modifiche, modifica delle checklist.
I metodi di ispezione sono molto efficaci: circa l’85% dei fault può essere individuato.
VERIFICA DINAMICA
È fondata sull’esecuzione del codice (testing).
Testing: approccio strutturato alla selezione di casi di test e dati associati.
Debugging: individuazione ed eliminazione dei difetti. Il Debugging implica formulare ipotesi osservando il
comportamento del programma e quindi verificare queste ipotesi per localizzare gli errori.
Il testing consiste nel trovare differenze tra il comportamento atteso, specificato attraverso il modello del
sistema e il comportamento osservato dal sistema implementato.
Obiettivo: progettare test per provare il sistema e rilevare problemi.
Il testing dovrebbe essere realizzato da persone che non sono state coinvolte nelle attività di sviluppo del
sistema.
Il testing avviene a vari livelli:
- unit testing: trovare le differenze tra object desing model e corrispondente componente
- integration testing: trovare differenze tra system design model e un sottoinsieme integrato di sottosistemi
- functional testing: trovare differenze tra use case model e il sistema
- system testing: trovare differenze tra requisiti non funzionali e il sistema
Unit testing
Il testing è applicato solamente ad un’unità (ovvero, un elemento definito nel progetto di un sistema software
che è testabile separatamente) di un sistema software.
Così facendo si riduce la complessità concentrandosi su una sola unit (quindi è più facile correggere bug).
Eseguire test case su un’unità richiede che questa sia isolata dal resto del sistema.
I test driver e test stub sono usati per sostituire le parti mancanti del sistema (precisamente, un test stub è
un’implementazione parziale di componenti da cui la componente testata è dipendente; un test driver è un
blocco di codice che inizializza e chiama la componente da testare).
Test driver + test stub: scaffolding.
Creare scaffolding:

Driver (modulo guida): deve simulare l’ambiente chiamante.


Stub (modulo fittizio): ha la stessa interfaccia del modulo simulato (tipicamente è una sottoclasse), ma è privo
di implementazioni significative.
Creare l’ambiente per l’esecuzione di test:
- lo scaffolding è estremamente importante per il test di unità e integrazione.
- può richiedere un grosso sforzo programmativo.
- uno scaffolding buono è un passo importante per un test di regressione efficiente.
- la generazione di scaffolding può essere parzialmente automatizzato a partire dalle specifiche.
- esistono pacchetti software per supportare la generazione di scaffolding (JUnit, PUnit etc…).

SLIDES 14.1 – TESTING WITH J-UNIT


Tipi di testing: System testing, Integration testing, Unit testing
Tipi di tecniche: White box, Black box
JUnit è un framework di testing per Java (istanza di xUnit generico).
Un framework è un’applicazione quasi completa che è dotata di un codice riutilizzabile, precisamente di
una struttura comune che può essere condivisa tra applicazioni.
Uno Unit test esamina il comportamento di una distinta unità di lavoro (una “unità di lavoro distinta”
solitamente è una classe).
Un test Unit si esegue velocemente e senza dipendenze.
Test Coverage: percentuale di codice testata dagli unit test.
Test Fixture: uno stato già verificato di software utilizzato come base per il test in corso.
Integration tests: test del comportamento di una componente.
Performance tests: misura le performance di un software.
JUnit include: assertions, test fixtures, test runners.
Tag di JUnit:
@Test public void testMethodName()
indica che testMethodName() è un test.
@Before public void beforeMethodName()
beforeMethodName() verrà eseguito prima di ogni test (solitamente chiamati setUp()).
@After public void afterMethodName()
afterMethodName() verrà eseguito dopo ogni test (solitamente chiamati tearDown()).
@Ignore
ignora un test
@Test(expected=Exception.class)
testa se il metodo lancia un eccezione
@Test(timeout=100)
fallisce se il test impiega più di 100 millisecondi.
Features: set di risorse comuni di cui si ha bisogno per eseguire uno o più tests.

SLIDES 14.2 – GUI TESTING


Test automation indica l’insieme di tool a disposizione per:
- generare test
- settare precondizioni di test
- eseguire tests
- effettuare confronti tra test
-…
Non tutti i test possono essere automatizzati. Su alcuni test, l’uomo può effettuare test in maniera più
efficiente rispetto ad una macchina (ad esempio, sugli aspetti visivi), molti bug infatti vengono trovati per
intuizioni dell’uomo.
I delle UI sono molto complessi.
Un test automatizzato dev’essere:
- indipendente da qualsiasi applicazione
- altamente riutilizzabile
- deve avere una facile manutenzione
- flessibile ed estendibile
Si parla quindi di software developement a tutti gli effetti.
Tipi di test:
Record and playback: viene generato uno script in base alle azioni dell’utente su una versione iniziale
dell’applicazion.
Keyword-driven test: un interprete legge il file in cui è descritto il test, trova delle keyword che fanno
riferimento a funzioni e verifica i vari casi.
GUI testing.
Una GUI map descrive gli oggetti del test presenti nell’applicazione sotto test. Viene utilizzato dal tool di
test per riconoscere gli oggetti della GUI. Ad ogni oggetto presente nella GUI map vi è associato uno script.
Una GUI Map deve però essere aggiornata nel caso di cambiamenti della GUI ed è piuttosto statico come
test.
TAF (Test Automation Framework) durante l’esecuzione di un test:
- esegue snapshots della UI corrente
- cerca tramite gli snapshots gli oggetti della GUI da testare
- esegue i test sugli oggetti
Viewer: prende informazioni riguardo gli oggetti della GUI dalla Java Virtual Machine dove l’applicazione è
in esecuzione.
Crea uno snapshot usando le informazioni recuperate.
Finder: ritrova gli oggetti più rilevanti, senza compromettere velocità e performance, utilizzando anche un
parziale o incompleto matching di informazioni.
Metrics: riporta sul monitor i risultati dei test. Sistema intelligente di analisi e di supporto per decisioni da
prendere.
ANT e Jenkins.
Apache ANT è una libreria Java e un tool a command-line il cui obiettivo è di eseguire processi descritti in
file XML.
ANT può essere usato per pilotare ogni tipo di processo che può essere descritto mediante tasks e obiettivi.
ANT può integrare JUnit.

SLIDES 15 – DEFINIZIONE DI CASI DI TEST


Strategie di testing:
- Black box: non considera la struttura del codice sorgente.
- White box: si basa sulla struttura del codice sorgente.
IL TESTING BLACK BOX
La definizione dei casi di test e dell’oracolo è fondata esclusivamente sulla base dei requisiti per l’unità da
testare.
L’unità per noi è una scatola nera.
Attività principale di generazione di casi di test:
- è scalabile (usabile per diversi livelli di testing)
- non può rilevare difetti dovuti a funzionalità extra rispetto alle specifiche.
Occorre gestire il problema di trovare un’opportuna copertura del dominio di input delle funzionalità.
Utilizziamo quindi delle classi di equivalenza.
Se un caso di test non rileva fallimenti per un elemento di una classe d’equivalenza, probabilmente non
rileva fallimenti per ogni altro elemento della classe.
Come identificare le classi di equivalenza?
E’ necessaria l’individuazione delle caratteristiche da partizionare.
Le caratteristiche sono dimensioni nel dominio della funzionalità oggetto di test:
- caratteristiche sintattiche derivanti dalla signature: una caratteristica per ogni parametro in ingresso della
funzionalità.
- caratteristiche semantiche derivanti dalla specifica: dipendono dalla funzionalità specifica, spesso sono
correlate ai parametri d’uscita delle funzionalità, possono essere trasversali rispetto alle caratteristiche
sintattiche.

Tecniche per identificare le classi di equivalenza:


- Enumerazione
Se la variabile in input è un elemento di un insieme discreto avremo una classe valida per ogni elemento
dell’insieme, una non valida per ogni elemento che non appartiene all’insieme; include il caso di valori
specifici; include il caso di valori booleani.
- Intervallo di valori
Se il parametro in input assume valori validi in un intervallo di valori di un dominio ordinato avremo una
classe valida per valori interni all’intervallo, una non valida per valori inferiori al minimo e una non valida
per valori superiori al massimo.
Le classi d’equivalenza individuate devono usate per identificare casi di test che minimizzino il numero
complessivo di test e che risultino significativi.
Euristiche di base:
- si devono individuare tanti casi di test per quante sono le classi d’equivalenza valide con il vincolo che
ciascun caso comprenda il maggior numero di classi d’equivalenza valide ancora scoperte.
- si devono individuare tanti casi di test per quante sono le classi d’equivalenza non valide, con il vincolo che
ciascun caso di test copra una ed una sola delle classi non valide.
Cosa accade quando una funzione accetta più parametri?
Si procede con una combinazione delle classi d’equivalenza e delle varie caratteristiche seguendo opportuni
criteri di copertura delle classi legate alle caratteristiche. La letteratura suggerisce diversi criteri di copertura
con implicazioni sul numero di test generati.
WECT Test Cases (Weak Equivalence Class Testing):
Per ogni classe d’equivalenza ci deve essere un test case che usa un valore nominale da quella classe di
equivalenza (se possibile valore nominale o valore medio della classe; possibile per classi superiormente e
inferiormente limitate e ordinate).
Es.

SECT Test Cases (Strong Equivalence Class Testing):


Dato il prodotto cartesiano delle classi di equivalenza delle caratteristiche, per ogni elemento del prodotto
cartesiano ci deve essere un caso di test che usa (simultaneamente) i valori nominali di ciascuna delle classi
nell’elemento del prodotto cartesiano considerato.
Es.

- la copertura WECT è la minima copertura considerabile.


Non garantisce un livello adeguato di test per la maggior parte delle soluzioni. Non considera l’eventuale
correlazione tra i dati in input. Richiede un numero di casi minimo pari al numero massimo di classi
associato a una caratteristica.
- la copertura SECT garantisce un livello di test più approfondito.
È più adeguata nel caso in cui vi sia correlazione tra i parametri di input (ne analizza tutte le combinazioni).
Richiede un numero minimo di casi di test pari al prodotto del numero di classi associato a ciascuna
caratteristica (cardinalità del prodotto cartesiano).
Tipici errori di programmazione capitano però spesso al limite tra classi diverse, quindi si effettua un testing
sui valori limite.
Idea di base:
Verificare i valori della variabile in input al minimo, immediatamente sopra il minimo, un valore intermedio,
immediatamente sotto il massimo e al massimo per ogni classe di equivalenza (convenzione: min, min+,
nom, max-, max).
Una funzione con n variabili richiede 4n + 1 casi di test.
Funziona bene con variabili che rappresentano quantità fisiche ben limitate.
Non considera la natura della funzione e il significato delle variabili.
Tecnica rudimentale che tende al test di robustezza.
Test di robustezza: Esegue 6n + 1 casi di test, date n variabili in input (considera anche min- e max+).
WCT Case Testing (Worst Case Testing)
La tecnica dei valori limite (Boundary) estende notevolmente la copertura rispetto al SECT, ma ha un
problema: non testa mai più variabili contemporaneamente ai valori limite.
WCT estende Boundary, testando il prodotto cartesiano di {min, min+, nom, max-, max}, per tutte le
variabili in input, per tutte le classi di equivalenza.
Più profonda del Boundary testing, ma molto più costosa: 5n test cases.
Buona strategia quando le variabili fisiche hanno numerose interazioni e quando le failure sono costose.
E in più: Robust Worst Case Testing (considera anche min- e max+).
Gerarchia (su n input):
- Boundary Value: 4n + 1
- Test di robustezza: 6n + 1
- WCT: 5n
- RWCT: 7n
IL TESTING WHITE BOX
La definizione dei casi e dell’oracolo è basata sulla struttura del codice.
Si selezionano quei test che esercitano tutte le strutture del programma.
Non è scalabile.
Attività complementare al testing funzionale.
Non può rilevare difetti che dipendono dalla mancata implementazione di alcune parti della specifica.
Un modello di rappresentazione dei programmi:
- il Grafo del flusso di controllo di un programma P è una quadrupla GFC(P) = (N, E, ni, nf)
- (N,E) è un grafo diretto con archi etichettati.

- Ns e Np sono insiemi disgiunti di nodi che rappresentano rispettivamente istruzioni e predicati.

- ni ed nf rappresentano rispettivamente nodo iniziale e nodo finale.


- un nodo in Ns U ni ha un solo successore ed il suo arco è etichettato come uncond.
- un nodo in Np ha due successori immediati e i suoi archi uscenti sono etichettati rispettivamente con true e
false.
- un GFC(P) è ben formato se esiste un cammino dal noto iniziale ni ad ogni nodo in N/{ni} e da ogni nodo
N/{nf} al nodo finale nf.
- diremo cammino o cammino totale un cammino da ni a nf.
Sequenze di nodi possono essere collassate in un solo nodo, purchè nel grafo semplificato vengano
mantenuti tutti i branch. Tale nodo collassato è etichettato con i numeri dei nodi in esso ridotti (in questo
esempio, 1,2,3,4,5,6,7,8 collassano in 3 nodi diversi: {1,2}, {3,4,5}, {6,7,8}).
Tecniche di testing strutturale
In generale, fondate sull’adozione di criteri di copertura degli oggetti che compongono la struttura dei
programmi.
Copertura: definizione di un insieme di casi di test in modo tale che gli oggetti di una definita classe siano
attivati almeno una volta nell’esecuzione dei casi di test.
Definizione di una metrica di copertura:
Test effectiveness Ratio (TER): #oggetti_coperti / #oggetti_totale
Come selezionare i casi di test per il criterio di copertura adottato?
Ogni caso di test corrisponde all’esecuzione di un particolare cammino sul GFC di P. Individuare i cammini
che ci garantiscono il livello di copertura desiderato.
Copertura dei nodi
Dato un programma P, viene definito un insieme di test case la cui esecuzione implica l’attraversamento di
tutti i nodi di GFC(P), ovvero l’esecuzione di tutte le istruzioni di P.
TER: #statements_eseguiti / #statements_totale
Copertura dei branch
Dato un programma P, viene definito un insieme di test case la cui esecuzione implica l’attraversamento di
tutti i rami di GFC(P), ovvero l’esecuzione di tutte le decisioni di P.
TER: #branch_eseguiti / #branch_totale NB: la copertura delle decisioni implica la copertura dei nodi.

Copertura di decisioni e condizioni


Dato un programma P, viene definito un insieme di test case la cui esecuzione implica l’esecuzione di tutte le
condizioni caratterizzanti le decisioni in P (per ogni condizione vengono considerate tutte le combinazioni di
condizioni).
Modified Condition Coverage: per applicazioni safety-critical. Per ogni condizione vengono considerate solo
le combinazioni di valori per le quali una delle condizioni determina il valore di verità della decisione.
Problema della copertura dei cammini: numero di cammini infinito (o comunque elevato…).
Un numero di cammini infinito implica la presenza di circuiti.
Soluzione: euristiche per limitare l’insieme dei cammini.
- criterio di n-copertura dei cicli
- metodi dei cammini linearmente indipendenti
-…
CONFRONTO TRA WHITE BOX E BLACK BOX
White box testing:
- un numero potenzialmente infinito di path da testare
- testa cosa è stato fatto, non cosa doveva essere fatto
- non può scoprire use case mancanti
Black box testing:
- potenzialmente c’è un’esplosione combinatoria di test cases
- non permette di scoprire use cases estranei
Entrambi sono necessari.
Sono agli estremi di un ipotetico testing continuum.
Qualunque test case viene a cadere tra loro, in base a:
- numero di path logici possibili
- natura dei dati in input
- complessità di algoritmi e strutture dati
INTEGRATION TESTING
Quando ogni componente è stata integrata in isolamento, possiamo integrarle in sottosistemi più grandi.
Due o più componenti sono integrate e analizzate, e se vengono trovati nuovi bug possono essere aggiunte
nuove componenti per riparare i bug.
L’ordine con cui le componenti vengono integrate può influenzare lo sforzo richiesto per l’integrazione.
Strategie di integration testing:
- Big Bang integration
- Bottom up integration
- Top down integration
FUNCTIONAL TESTING
Verificare che tutti i requisiti funzionali siano stati implementati correttamente.
Più espliciti sono i requisiti, più facili sono da testare.
Le qualità degli Use Case influenza il Functional Testing.
Obiettivo:
trovare le differenze tra i requisiti funzionali e le funzionalità realmente offerte dal sistema.
I test case sono progettati dal documento dei requisiti e si focalizza sulle richieste e le funzioni chiave.
Il sistema è trattato come un black box.
I test case per le unit possono essere riutilizzati, ma devono essere scelti quelli rilevanti per l’utente finale e
che hanno una buona probabilità di riscontrare un fallimento.
SYSTEM TESTING
Unit e Integration testing si focalizzano sulla ricerca di failures nelle componenti invidivuali. System testing
assicura che il sistema completo sia completo e conforme ai requisiti funzionali e non funzionali.
Attività:
- performance testing
- pilot testing
- acceptance testing
- installation testing
Performance testing:
Obiettivo: spingere il sistema oltre i suoi limiti!
- testare come il sistema si comporta quando è sovraccarico.
- tenta ordini di esecuzione non usuali
- controlla le risposte del sistema a grandi volumi di dati
Pilot testing:
Primo test pilota, sul campo, del sistema
Alpha test: è un test pilota con utenti che esercitano il sistema nell’ambiente di sviluppo
Beta test: il test di accettazione è realizzato da un numero limitato di utenti nell’ambiente di utilizzo
Acceptance testing:
Tre modi con cui il cliente può valutare un sistema durante l’acceptance testing:
- benchmark test: il cliente prepara una serie di test case che rappresentano le condizioni tipiche sotto cui il
sistema dovrà operare
- competitor test: il nuovo sistema è testato contro un sistema esistente o contro un prodotto competitore
- shadow testing: una forma di testing a confronto, il nuovo sistema e il sistema legacy sono eseguiti in
parallelo e i loro output sono confrontati
Se il cliente è soddisfatto, il sistema è accettato (eventualmente con una lista di cambiamenti da effettuare).
Installation testing:
Dopo che il sistema è accettato, esso è installato nell’ambiente di utilizzo.
In molti casi il test di installazione ripete i test case eseguiti durante il function testing e il performance
testing. Quando il cliente è soddisfatto, il sistema viene formalmente rilasciato ed è pronto all’uso.
Usability testing:
Testa la compatibilità del sistema da parte dell’utente.
I rappresentanti dei potenziali utenti trovano problemi “usando” le interfacce utente o una loro simulazione.
Le tecniche sono basate sull’approccio degli esperimenti controllati:
gli sviluppatori prima selezionano un insieme di obiettivi. In una serie di esperimenti, viene chiesto ai
partecipanti di eseguire una serie di task sul sistema. Gli sviluppatori osservano i partecipanti e raccolgono
una serie di informazioni oggettive e soggettive.
GESTIONE DEL TESTING
I 4 passi del testing:
1: scegliere cosa deve essere testato (affidabilità del codice, completezza dei requisiti etc…)
2: decidere come deve essere testato (code inspection, white box, black box, etc…)
3: definire i test cases
4: creare i test oracle
Un piano di testing è un documento che prevede i seguenti elementi:
- introduzione (descrizione degli obiettivi)
- relazioni con altri documenti (come e dove sono specificati requisiti funzionali e non funzionali)
- System overview (dettagli sul sistema e sul livello di granularità che si vuole testare)
- caratteristiche da testare/escludere
- criteri di pass/fail
- approcci (strategie di testing utilizzate)
- risorse (elenco di requisiti hardware/software/ambientali per poter effettuare il test)
- test cases (elenco dei test)
- test schedule (scheduling temporale e di risorse per il testing)
Definizione di base di test case:
Un test case è un insieme di input e di risultati attesi che stressano una componente con lo scopo di causare
fallimenti e rilevare fault.
Ha 5 attributi:
- name
- location
- input
- oracle (comportamento atteso del test case, ovvero la sequenza corretta che l’esecuzione del test dovrebbe
dare)
- log (insieme di correlazioni del comportamento osservato con il comportamento atteso per varie esecuzioni
del test)

OCL (OBJECT CONSTRAINT LANGUAGE)


Spesso servono vincoli aggiuntivi per gli oggetti come ad esempio invarianti di classi e pre e post condizioni
sui metodi. Per specificare i vincoli c’è bisogno di un linguaggio formale. OCL viene presentato quindi come
linguaggio formale di facile uso e comprensione per specificare i vincoli.
OCL è un linguaggio per scrivere espressioni sullo stato di un oggetto.
La valutazione di un’espressione restituisce un valore e non altera lo stato di un oggetto.
OCL non è un linguaggio di programmazione, parla solo delle proprietà di una collezione di oggetti.
OCL è un linguaggio tipato: ogni espressione ha un tipo e le operazioni applicate ad esso devono essere
conformi al suo tipo.
I vincoli espressi in OCL possono essere usati nelle seguenti occasioni:
- per esprimere invarianti su classi e tipi nel class diagram
- per esprimere pre-condizioni di un’operazione o di un metodo nel class diagram
- per esprimere guardie
- per esprimere vincoli sulle operazioni
- per esprimere l’insieme di destinatari di messaggi ed azioni
Un invariante è un vincolo di stato che deve essere soddisfatto da ogni istanza della classe quando non sia in
atto una variazione ad opera di un metodo o operazione
es.
context Company inv:
self.numberOfEmployees > 50
Variante equivalente
context c: Company inv
c.numberOfEmployees > 50
Invariante con nome
context c: Company inv enoughEmployees:
c.numberOfEmployees > 50
Una pre-condizione è un vincolo sullo stato e sui parametri attuali che dev’essere soddisfatto perché il
metodo possa essere invocato.
Una post-condizione è un vincolo sullo stato e sui parametri precedente l’invocazione del metodo, sullo
stato successivo ed eventualmente sul risultato restituito che deve essere soddisfatto dopo che il metodo è
stato invocato.
Es.
context Typename::operationName(param1: Type1, …);
Return Type
pre: param1: …
post: result = …
context Person::income(d: Date) : Integer
post: result = 5000
con nome..
context Typename::operationName(param1: Type1, …);
Return Type
pre parameterOk: param1: …
post resultOk: result = …
OCL può essere usato per esprimere interrogazioni su un class diagram

Body expression: formato

context Typename::operationName(param1: Type1, …);


Return Type
body: --some expression
es.
context Person::getCurrentSpouse(): Person;
pre: self.isMarried=true
body: self.marriages->select (m | m.ended.false).spouse
OCL può essere usato per inizializzazione di attributi e attributi derivati
context Typename::attributeName: Type
init: -- some expression representing the initial value
context Typename::assocRoleName: Type
derive: -- some expression representing the derivation rule
es.
context Person::income : Integer
init: parents.income->sum() * 1% -- pocket allowance
derive: if underAge
then parents.income->sum() * 1% -- pocket allowance
else job.salary -- income from regular job
endif
Tipi di base (con operazioni):
- boolean (and, or, xor, not, implies, if-then-else)
- integer (*,+,-,/,abs())
- real (*.+.-./,floor())
- string (concat(),size(),substring())
- enumeration
Tipi Collezione:
- Set(T) collezione di elementi di tipo T, senza molteplicità, senza ordinamento
- Bag(T) collezione di elementi di tipo T, con molteplicità, senza ordinamento
- Sequence(T) collezione ordinata di elementi di tipo T.
Come ottenere istanze di collezioni:
- mediante navigazione delle proprietà a partire da un’istanza di un oggetto o di una classe.
- context Company inv:
- self.employee
- usando operazioni sulle collezioni a partire da collezioni
- collection1->union(collection2)
- esistono tante altre operazioni sulle collection…
Operazioni sui tipi collection:
- size() : Integer
Il numero di elementi nella collezione self.
- isEmpty() : Boolean
true se self è la collezione vuota, false altrimenti
- notEmpty() : Boolean
true se self non è la collezione vuota, false altrimenti
- includes(object : T) : Boolean
True se object è un elemento di self, false altrimenti
- count(object : T) : Integer
Il numero di occorrenze di object in self.
- sum() : Integer
La somma di tutti gli elementi in self (T tipo compatibile)
- product(c2: Collection(T2)) : Set(Tuple(first: T, second: T2))
Il prodotto cartesiano di self e di T2
Operazioni sui tipi Set:
- tutte le operazioni definite su Collection
union(s : Set(T)) : Set(T)
L'unione di self and s.
intersection(s : Set(T)) : Set(T)
Intersezione di self and s
– (s : Set(T)) : Set(T)
Gli elementi di self, che non sono in s.
= (s : Set(T)) : Boolean
true se self ed s contengono gli stessi elementi.
…e molte altre.
Operazioni sui tipi OrderedSet
- Tutte le operazioni definite su Collection
- append (object: T) : OrderedSet(T)
L'insieme di elementi di self seguito da object
- prepend(object : T) : OrderedSet(T)
L'insieme di elementi di self preceduto da object
- insertAt(index : Integer, object : T) : OrderedSet(T)
L'insieme di elementi di self con l'inserimento di object nella posizione indicata
- at(i : Integer) : T
i-esimo elemento dell'insieme
- indexOf(obj : T) : Integer
L'indice dell'elemento obj nell'insieme
- first() : T
Il primo elemento dell'insieme
- last() : T
L'ultimo elemento dell'insieme
- …altre operazioni

E’ possibile associare un nome ed un tipo ad una sottoespressione quando è utile usarla più volte.
es.
context Person inv:
let income : Integer = self.job.salary->sum() in
if isUnemployed then
income < 100
else
income >= 100
endif
I tipi in OCL sono organizzati in una gerarchia. Un’espressione OCL è valida se i tipi coinvolti sono
conformi.
- Un tipo type1 è conforme ad un tipo type2 quando un’istanza di type1 può essere sostituita al posto di
un’occorrenza di type2.
- Ogni tipo è conforme al suo supertipo.
- La conformità è transitiva.
- l’operazione di casting ad un sottotipo può servire ad accedere alla proprietà di un’istanza quando sia certo
che essa è associata a un sottotipo type2 del tipo corrente type1.
- l’oggetto corrente può essere ri-tipato con l’operazione oclAsType(OclType)
- avendo un oggetto di tipo type1 ed un suo sottotipo type2 si può scrivere object.oclAsType(type2)
La valutazione di un’espressione può portare a valori indefiniti.
In generale:
- True OR Undef valuta True
- False AND Undef valuta False
- False IMPLIES Undef valuta True
- Undef IMPLIES True valuta True
Esiste un’operazione per testare l’indefinitezza di un’espressione: ocllsUndefined().
Oggetti e proprietà:
- se l'associazione ha cardinalità m .. * (molti)
- object.role1 è una espressione di tipo Set (Class1)
- ha come valore una collezione di oggetti di tipo Class1
- se l'associazione ha cardinalità m .. * (molti) ed è qualificata come {ordered}
- object.role1 è una espressione di tipo Sequence(Class1)
- ha come valore una collezione ordinata di oggetti di tipo Class1

Es.
Invariante (il manager di una compagnia non è disoccupato; la compagnia non ha insieme vuoto di
dipendenti)
Alla proprietà di una collezione si accede mediante ->

context Company
inv: self.manager.isUnemployed = false
inv: self.employee->notEmpty()
In caso di ruolo con cardinalità 0..1 o 1 è possibile considerare il valore
dell'espressione object.role come un insieme singoletto (contenente un unico
oggetto) anziché un oggetto.

Es. : Una compagnia ha uno ed un solo manager


context Company inv:
self.manager->size() = 1

Tecnica usata per controllare l'esistenza di un oggetto associato in caso di


partecipazione parziale all'associazione
context Person inv:
self.wife->notEmpty() implies self.wife.gender = Gender::female
Per la navigazione ad una classe di associazione si usa il nome della classe di associazione con lettera
minuscola. In caso di ambiguità viene messo il ruolo tra parentesi quadre (es. self.job[employer]).
Per la navigazione da una classe di associazione si usa l’indicazione del ruolo. Ad una istanza di
associazione è associato esattamente un oggetto per ogni ruolo.
Proprietà predefinite di tutti gli oggetti:
- oclIsTypeOf (t : OclType) : Boolean
Vera se type di self e t sono gli stessi.
- oclIsKindOf (t : OclType) : Boolean
Vera se type di self e t sono gli stessi o se t è un suo supertipo
- oclIsNew () : Boolean
Vera se self e un nuovo oggetto creato da un metodo (usato nelle post-condizioni)
- oclAsType (t : OclType) : instance of OclType (casting)
Ci sono proprietà non definite sulle istanze degli oggetti ma sulle loro classi. Il formato della espressione è
Class.proprietà. Tra le proprietà predefinite più interessanti c’è quella che associa ad una classe Class la
collezione delle sue istanze: Class.allInstances()
Esempio: Tutte le persone hanno nome diverso.
context Person inv:
Person.allInstances()->forAll(p1, p2 |
p1 <> p2 implies p1.name <> p2.name)
Person.allInstances() associa come valore il set delle istanze delle persone presenti al momento della
valutazione.
In una post condizione di può far riferimento ai valori di proprietà a due momenti temporali distinti. Di
default, si riferisce al valore ottenuto dal completamento dell’esecuzione di un metodo. Per fare riferimento
al valore precedente a tale esecuzione, si utilizza il suffisso @pre.
es.
post: age = age@pre + 1
Attenzione all'uso di @pre nella navigazione degli oggetti:
- a.b@pre.c – vecchio valore della proprietà b di a, sia x e poi il nuovo valore di c di x.
- a.b@pre.c@pre -- vecchio valore della proprietà b di a, sia x e poi il vecchio valore di c di x.
- il vecchio valore di un oggetto distrutto nel metodo è OclUndefined
- il vecchio valore di un oggetto creato nel metodo è OclUndefined
E’ possibile comporre anche diversi tipi in una ennupla.
Alle componenti di una ennupla si fa accesso mediante la notazione puntata.
OCL mette a disposizione anche operazioni di filtraggio di collezioni.
Filtraggio positivo: collezione->select (espressione booleana), ha come valore il sottoinsieme degli
elementi della collezione rispetto ai quali l’espressione booleana valuta a true.
Filtraggio positivo: collezione->reject (espressione booleana), ha come valore il sottoinsieme degli
elementi della collezione rispetto ai quali l’espressione booleana valuta a false.
SELECT:
collezione->select (espressione booleana)
es.
la compagnia deve avere almeno un impiegato di età superiore ai 50
context Company inv:
self.employee->select(age > 50)->notEmpty()
Sintassi generale:
- collezione->select(v: Type | espressione booleana con v)
- v è l’iteratore nella collezione
- gli elementi selezionati devono essere di tipo Type
- può essere utilizzato per selezionare oggetti con sottotipo specifico rispetto al tipo della collezione
REJECT:
idem…
Select e Reject servono per produrre una sottocollezione di una collezione data.
Per creare una collezione derivata da un'altra collezione si usa l’operazione collect.
collezione->collect(espressione)
collezione->collect(v: Type | espressione booleana con v)
Per la rimozione di eventuali duplicati: …->asSet()
Per esprimere vincoli su una collezione:
collezione->forAll(espressione-booleana)
collezione->exists(espressione-booleana)
Varianti sintattiche:
collezione->forAll(v: Type | espressione booleana con v)
collezione->exists(v: Type | espressione booleana con v)

Potrebbero piacerti anche