Progetto Ingegneria del Software Maze

Transcript

Progetto Ingegneria del Software Maze
Progetto Ingegneria del Software
Maze
di
Cerretani Giacomo – Cesca Giacomo – Pazzaglia Mirco
Indice
1. Introduzione.................................................................................................................................. 1
2. Descrizione del problema.............................................................................................................1
3. Requisiti....................................................................................................................................... 1
3.1 Requisiti Funzionali................................................................................................................2
3.2 Requisiti Non Funzionali........................................................................................................ 2
4. Analisi........................................................................................................................................... 3
4.1 Gellish.................................................................................................................................... 3
4.2 Alloy....................................................................................................................................... 5
4.3 UML (1).................................................................................................................................. 8
4.3.1 Use Case Diagram.........................................................................................................8
4.3.2 Activity Diagram............................................................................................................ 11
5. Progettazione............................................................................................................................. 13
5.1 UML (2)................................................................................................................................ 13
5.1.1 Class Diagram.............................................................................................................. 13
5.1.2 Design Pattern.............................................................................................................. 15
5.1.3 OCL.............................................................................................................................. 18
5.1.4 Sequence Diagram.......................................................................................................20
5.1.5 Package Diagram.........................................................................................................23
5.2 Petri Net............................................................................................................................... 23
6. Implementazione........................................................................................................................ 26
6.1 JML...................................................................................................................................... 26
6.2 UML (3)................................................................................................................................ 28
6.2.1 Deployment Diagram....................................................................................................28
7. Test............................................................................................................................................. 29
7.1 Junit..................................................................................................................................... 29
8. Conclusioni................................................................................................................................. 31
9. Appendice.................................................................................................................................. 32
9.1 Screenshot del programma..................................................................................................32
9.2 Programmi utilizzati.............................................................................................................. 33
9.3 Fonti..................................................................................................................................... 33
9.4 Ringraziamenti..................................................................................................................... 33
1 Introduzione
1 Introduzione
Questa è una relazione orientata agli esempi. Lo scopo di questo documento è quello di
fornire una linea guida, seppur di base, sull'esistenza e l'utilizzo di particolari strumenti adatti
per l'analisi e lo sviluppo del software. Cercando un approccio più sul piano pratico che su
quello teorico abbiamo ripercorso tutte le fasi che hanno portato un software dalla sua nascita
alla sua completa funzionalità. Ovviamente per non rendere ripetitiva la descrizione degli
strumenti abbiamo selezionato per le fasi di analisi, progettazione, implementazione e test,
esclusivamente gli esempi più utili ed educativi, rinunciando quindi a riportare la totalità della
documentazione prodotta.
Chiaramente questa relazione non ha la presunzione di voler rimpiazzare nessun manuale
su alcuno degli strumenti che andremo ad osservare ma solo di fornire una visione
panoramica di questi tool ingegneristici attraverso degli esempi che ne possano rendere più
immediata la comprensione.
2 Descrizione del problema
Il progetto che useremo come esempio ci è stato commissionato dal Museo di Camerino.
La richiesta è stata quella di realizzare un programma applicativo che su richiesta dell'utente
generasse casualmente un labirinto di dimensioni variabili e con un numero altrettanto
variabile di ostacoli. Nel labirinto l'utente deve poter liberamente posizionare un “pedone”
(tranne ovviamente che nelle caselle con gli ostacoli). Questo programma ha come scopo
didattico quello di mostrare la risoluzione di tale rompicapo in maniera grafica muovendo il
pedone in cerca di uscita passo dopo passo, sia che il labirinto presenti o meno l'uscita. Alla
fine, il numero di passi impiegati per uscire per ognuno degli algoritmi andava mostrato a
video. L'utente infatti avrebbe dovuto scegliere tra tre diversi tipi di algoritmo per cercare
l'uscita: un algoritmo sequenziale, uno parallelo ed un goloso (greedy). Lo scopo principale di
questo progetto è infatti quello di far comprendere attraverso un modo semplice ed intuitivo la
differenza tra i tre algoritmi a bambini e ragazzi di varie età in visita presso il museo.
Ci è stato richiesto che il programma fosse scritto in Java per migliorarne la portabilità, e
che il codice fosse “leggero”, veloce e performante anche su computer di medie prestazioni,
ma soprattutto con un grafica che non fosse né troppo complicata da comprendere né
troppo “vuota” da causare la quasi immediata perdita di concentrazione da parte del pubblico.
In ultimo, poiché il computer su cui sarebbe stato mostrato è a diretto contatto anche con i
visitatori, vi era la richiesta di inserire una shortcut segreta per chiudere l'applicazione anziché
tramite la classica icona “X” sull'angolo in alto a destra della finestra, per evitare l'utilizzo
improprio dell'attrezzatura del museo.
3 Requisiti
Il primo passo da fare è individuare i requisiti del problema posto. In pratica, una volta che
si ha tutto il materiale in cui è descritto ciò che viene richiesto dal cliente (genericamente
questo materiale è effettivamente presente dopo aver sottoposto il cliente ad un operazione
chiamata “Intervista”), bisogna individuare precisamente quali sono gli obiettivi che il
programma deve raggiungere, quali sono quelli più prioritari e quelli meno (utilizzando la
notazione MoSCoW) dividendoli in due macro categorie: i requisiti funzionali e i requisiti non
funzionali.
1
3 Requisiti
3.1 Requisiti Funzionali
Essenzialmente si possono catalogare in questa categoria tutti i requisiti che rispondono a
queste domande: “Cosa il sistema deve e/o non deve fare?”, “Come deve reagire agli stimoli
esterni?”. Ecco alcuni requisiti di esempio, sulla base della descrizione del problema fornita
sopra.
ID
Descrizione
MoSCoW
0
Il programma deve permettere la scelta della dimensioni del labirinto
Must Have
1
Il programma deve permettere la scelta del numero di ostacoli del labirinto
Must Have
2
Il programma deve permettere la creazione di nuovi labirinti casuali
Must Have
3
Il programma dovrebbe permettere il libero posizionamento del pedone
all'interno del labirinto.
Should Have
4
Il programma deve permettere la risoluzione del labirinto attraverso un
algoritmo sequenziale, un algoritmo parallelo o un algoritmo goloso
Must Have
5
Il programma deve rappresentare la risoluzione di un labirinto in maniera
grafica
Must Have
6
Il programma deve mostrare i passi utilizzati dal pedone per risolvere il
labirinto
Must Have
7
Il programma deve avere una scorciatoia di chiusura nascosta al pubblico
Must Have
…
…
...
3.2 Requisiti Non Funzionali
I requisiti che invece definiscono le proprietà del sistema che devono essere soddisfatte
sono inclusi in questo gruppo. Ecco quindi alcuni requisiti di esempio, basati sulla descrizione
del problema fornita sopra, per capire meglio questa tipologia di requisiti.
ID
Descrizione
MoSCoW
15
Il programma deve essere sviluppato in Java
Must Have
16
Le risorse del sistema potrebbero essere limitate
Could Have
17
Il programma dovrebbe essere veloce
Should Have
18
Il programma deve avere una grafica “accattivante”
Must Have
…
…
...
Nota: la colonna MoSCoW (must-should-could-would), sia nella tabella dei requisiti
funzionali che in quella dei requisiti non funzionali, definisce l'importanza che ogni requisito
possiede all'interno del sistema.
2
4 Analisi
4 Analisi
In questa fase andremo ad utilizzare tre strumenti che ci permetteranno di eliminare le
ambiguità di comprensione dei termini, di iniziare a definire il dominio del programma che si
andrà a sviluppare, di stabilire quali sono le attività che l'utente può fare attraverso il sistema
e quindi di creare i primi prototipi di algoritmo che verranno successivamente implementati.
4.1 Gellish
Gellish è un “dizionario intelligente”. Attraverso questo strumento, definito in due ISO, è
possibile eliminare le ambiguità del linguaggio specialistico utilizzato nel dominio
dell'applicazione. Gellish infatti non è altro che un grande dizionario basato sul linguaggio
naturale (per ognuno di esso esiste una variante Gellish). Con i termini definiti al suo interno
se ne possono definire altri attraverso la generica tupla <termine1, relazione, termine2> che
si può leggere: “il termine1 è collegato al termine2 attraverso la relazione”.
Ogni termine ed ogni relazione presenti nei vari dizionari Gellish sono standard e codificati
tramite un codice univoco; questo permette di tradurre ogni nuova definizione espressa come
indicato sopra in qualsiasi lingua possieda un Gellish Dictionary. In ultimo, ma non per minor
importanza, l'indicizzazione appena descritta e la forma di relazione tra due termini rende le
informazioni comprensibili ai computer senza necessità di alcuna codifica.
A seguire, un esempio di utilizzo di Gellish che mostra come abbiamo definito alcuni dei
termini del nostro dominio di applicazione.
(l'immagine qui sopra è da considerarsi con l'immagine seguente allineata alla sua destra)
3
4 Analisi
(l'immagine qui sopra è da considerarsi con l'immagine seguente allineata alla sua destra)
(l'immagine qui sopra è da considerarsi con l'immagine seguente allineata alla sua destra)
Notare ad esempio la definizione di “Maze” realizzata in Gellish attraverso quattro diverse
relazioni espresse nelle ultime quattro righe della tabella sopra proposta. La definizione del
termine “Maze” è data da tutte le righe che lo riguardano. Una di queste quattro, ad esempio,
è composta da: <maze> <is made of> <obstacle> : come spiegato sopra la relazione “is
made of” collega i due termini “maze” e “obstacle” spiegando in maniera non ambigua che “un
labirinto è fatto da ostacoli”; la colonna seguente offre la possibilità di inserire anche una
definizione in linguaggio naturale per rendere l'espressione precedente più comprensibile per
l'essere umano.
4
4 Analisi
4.2 Alloy
Alloy è un linguaggio utilizzato per il controllo di modelli espressi nella forma della Logica
del Primo Ordine (FOL). Lo strumento Alloy viene utilizzato in fase di analisi per generare
degli scenari d'esempio (modelli) da somministrare al cliente per verificare se i vincoli ed i
domini tratti dalle precedenti interviste sono esatti e completi. Con questo linguaggio è
possibile definire delle signature (sig) ossia dei domini di elementi. All'interno di ogni sig si
possono specificare le eventuali relazioni con altri elementi di altre signature o,
eventualmente, con se stessa. Una volta definiti gli insiemi di elementi che entrano in gioco
nel sistema, si possono definire i fatti (fact) ossia dei postulati sempre veri o dei predicati
(pred) per vincolare il sistema. Inoltre il sistema offre la possibilità di “verificare” delle
asserzioni (assert) specifiche per testare la bontà del modello creato attraverso la direttiva
check, la quale cerca dei controesempi (se esistono) per smentire l'asserzione postagli. In
questo linguaggio esistono anche numerose parole chiave per modificare la cardinalità delle
relazioni e poter creare relazioni multiple (set), o per limitarle ad una singola (one). Offre
anche un buon numero di comandi per le operazioni su insiemi come ad esempio la
cardinalità (come #( elements )), operazioni relazionali tra numeri (come <, >, =, <=, >=, !=)
ed operatori logici (come implies, iff).
Abbiamo quindi modellato la realtà del sistema utilizzando Alloy. Nell'esempio abbiamo
lasciato gran parte dei fatti sviluppati per fornire esempi sulla sintassi e sulla modalità di
utilizzo di questo strumento. Inoltre sono presenti, sotto forma di commento, anche una
assert con relativo comando check.
open util/integer
sig Maze {
Map : set Cell,
Width : one Int,
Height : one Int,
nObstacles : one Int,
Player : set Pawn
}
sig Cell { }
sig Pawn {
Position : one Point,
State : set Status,
Clone : set Pawn
}
sig Status { }
sig Point {
X : one Int,
Y : one Int
}
//Fatto A : in ogni Maze la cardinalità di Map deve essere
// esattamente uguale al prodotto dei suoi due attributi
// Width e Height
fact A { all m : Maze | #(m.Map) = mul [ m.Width , m.Height ] }
//Fatto B : il labirinto deve essere quadrato
fact B { all m : Maze | m.Width = m.Height }
//Fatto C : in ogni Maze il numero di ostacoli (nObstacles) deve sempre
// essere al massimo la cardinalità del rispettivo Map - 2 e al minimo 0
fact C { all m : Maze | m.nObstacles > -1 and m.nObstacles < ( #(m.Map) - 2 ) }
5
4 Analisi
//Fatto D : in ogni Maze le rispettive Height e Width non devono
// essere minori di 5 e maggiori di 50
fact D { all m : Maze | m.Width >= 5 and m.Height >= 5 and
m.Width <= 50 and m.Height <= 50 }
//Fatto E : ogni Cell deve appartenere ad un Map (non devono esistere
// elementi di Cell sfusi)
fact E { all c : Cell | some m : Maze | c in m.Map }
//Fatto F : ogni Pawn deve appartenere ad un Maze (non devono
// esistere elementi di Pawn sfusi)
fact F { all p : Pawn | some m : Maze | p in m.Player }
//Fatto G : ogni Point deve appartenere ad un Pawn (non devono esistere
// elementi di Point sfusi)
fact G { all p : Point | some pa : Pawn | p in pa.Position }
//Fatto H : i valori di X e Y della Position di un Player devono sempre essere
// maggiori o uguali a 0 e minori di Width e Height del Maze di riferimento
fact H { all m : Maze | all p : m.Player.Position | p.X >= 0 and p.Y >= 0 and
p.X < m.Width and p.Y < m.Height }
//Fatto I : non possono esistere elementi di Status non collegati ad un elemento Pawn
// (non devono esistere elementi Status sfusi)
fact I { all s : Status | some p : Pawn | s in p.State }
//Fatto L : ogni oggetto Pawn deve avere sempre almeno uno stato
fact L { all p : Pawn | some s : Status | s in p.State }
//Fatto M : ogni Maze deve avere almeno un pedone
fact M { all m : Maze | #(m.Player) > 0 }
//Fatto N : non devono esistere due o più pedoni contemporaneamente sulla stessa cella
fact N { all disj p1 , p2 : Pawn |
(p1.Position.X = p2.Position.X implies p1.Position.Y != p2.Position.Y) or
(p1.Position.Y = p2.Position.Y => p1.Position.X != p2.Position.X) }
//Fatto O : ogni Pawn non può avere più di 4 cloni
fact O { all p : Pawn | #(p.Clone) < 5 }
//Fatto P : nessun Pawn può essere clone di se stesso (ne dei suoi cloni)
fact PSelf {all p : Pawn | p not in p.Clone }
fact Pouter {all p : Pawn | p not in p.^Clone }
//Fatto Q : ogni Pawn non può essere clone di più di un Pawn
fact Q { all p : Pawn | #(p.~Clone) <= 1 }
assert Bb{ all m : Maze | m.Width = 0 or m.Height = 0}
// check Bb for 7 Int
//Forzo il sistema a costruirmi un labirinto di dimensioni 5x5
pred dim{all m : Maze | m.Width = 5 and m.Height = 5 }
run dim for 7 Int , exactly 1 Maze , exactly 5 Pawn , 5 Point , 90 Cell , 5 Status
Notare infine nell'ultima riga l'utilizzo del comando run, con lo scopo di eseguire il predicato
“dim” in cui obblighiamo ogni elemento di Maze ad avere dimensione 5x5.
Segue la rappresentazione di un modello risalente agli stadi iniziali dell'analisi in cui
mancava il vincolo di dimensione minima pari a 5x5.
6
4 Analisi
7
4 Analisi
4.3 UML (1)
Verranno ora presentati alcuni diagrammi dello standard UML (Unified Modeling
Language), basato sul paradigma object-oriented, con lo scopo di agevolare e dare sostegno
in fase di analisi sia agli analisti che ai clienti. Si tratta infatti di diagrammi di facile
comprensione anche a chi non si muove nel mondo dello sviluppo di un software ma pur
sempre formali.
4.3.1 Use Case Diagram
In UML, gli Use Case Diagram (UCD o diagrammi dei casi d'uso) sono diagrammi dedicati
alla descrizione delle funzioni o servizi offerti da un sistema, così come sono percepiti e
utilizzati dagli attori che interagiscono col sistema stesso.
Sono impiegati soprattutto nel contesto della Use Case View (vista dei casi d'uso) di un
modello, e in tal caso si possono considerare come uno strumento di rappresentazione dei
requisiti funzionali di un sistema.
Negli UCD entrano in gioco diverse entità:
•
Il sistema che è rappresentato con un rettangolo, il quale definisce i limiti del sistema
stesso.
•
Gli attori che vengono rappresentati con degli "omini" stilizzati, i quali definiscono chi o
cosa interagisce con il sistema (persone fisiche e/o altri software).
•
Gli Use case, che vengono rappresentati con degli ellissi etichettati con il nome caso
d'uso, i quali definiscono una funzione o un servizio offerto dal sistema.
Nello UCD da noi realizzato possiamo vedere come un utente si interfaccia con il sistema
Labirinto. Nello specifico l'utente generico potrà effettuare tutte le operazioni ad esso
collegate, ad eccezione della chiusura del programma che, come si può evincere dal
diagramma sottostante, è una funzionalità eseguibile esclusivamente dall'amministratore.
8
4 Analisi
9
4 Analisi
10
4 Analisi
4.3.2 Activity Diagram
L'Activity Diagram può essere utilizzato durante l'analisi del progetto per dettagliare un
determinato algoritmo definendo una serie di attività o flussi. L'Activity Diagram è spesso
usato come modello complementare allo Use Case Diagram, per descrivere le dinamiche con
cui si sviluppano i diversi use case e permette di modellare un processo come un insieme di
nodi ed archi. La sua lettura va effettuata ponendo un token nel nodo iniziale e seguendo il
suo flusso fra gli altri nodi fino a giungere in un nodo finale. I principali tipi di nodo possono
essere:
•
Nodo di inizio attività, rappresentato da un pallino pieno. E' il punto di partenza per
l'attività che si vuole rappresentare con il diagramma.
•
Nodo di azione, rappresentato da un rettangolo contenente un'etichetta. Sta a
significare l'azione da svolgere quando raggiunto dal token.
•
Nodo di decisione, rappresentato da un rombo ed etichettato da una condizione.
Stabilisce un set di percorsi possibili per il token che lo raggiunge. Il token fluirà
attraverso solo uno degli archi sulla base del condizione imposta.
•
Nodo di riunione (merge), rappresentato da un rombo senza etichette. Ricongiunge
un percorso precedentemente diviso da un nodo decisionale.
•
Nodi di fork e di join rappresentati da una sbarra. Quando vi è un arco entrante e più
uscenti, ci si trova in una situazione di fork, nel caso inverso ci si trova in una
sitauzione di join. In una fork il token da vita ad un numero di token pari al numero di
archi uscenti dal nodo che procederanno l'uno indipendentemente dall'altro sulle
proprie linee di flusso in via del tutto parallela. In un join, al contrario, le attività di più
token convergeranno sincronizzando l'attività dei vari flussi.
•
Nodo finale, indicato da un pallino pieno cerchiato. Quando un token giunge su
questo nodo vuol dire che l'intera attività è giunta a conclusione.
Il seguente Activity Diagram fa riferimento alla fase di analisi dell'algoritmo sequenziale.
Avendo presenti le definizioni di nodo ed arco scritti in precedenza, questo diagramma risulta
di facile lettura per il cliente e come buon punto di riferimento per gli sviluppatori che
andranno ad implementare l'algoritmo.
11
4 Analisi
12
5 Progettazione
5 Progettazione
Stabilito nelle fasi analisi e studio dei requisiti quanto più dettagliatamente e chiaramente
possibile quello che il cliente vuole dal software, giunge il momento di porre le basi concrete
per lo sviluppo. Si sta infatti passando dalla comprensione della richiesta alla fase di
produzione e per far ciò si ricorre all'utilizzo di particolari strumenti, alcuni dei quali presenti
nello standard UML. In questa fase si andrà ad espandere il materiale prodotto nelle fasi
precedenti con quello specifico della progettazione. Una buona progettazione implica una
maggior sicurezza in fase di implementazione, portando alla produzione di codice più robusto,
adattabile, scalabile e tollerante a possibili modifiche e/o aggiornamenti, minimizzando al
contempo le possibilità di errore e facilitando la collaborazione fra membri del team di
sviluppo.
5.1 UML (2)
In questa sezione faremo ricorso a nuovi strumenti UML in aggiunta a quelli del paragrafo
4.3 . Questa volta il livello di dettaglio sarà maggiore poiché ci si sta avvicinando sempre di
più all'implementazione, in modo tale da poter porre basi migliori per lo sviluppo ed
eventualmente poter sfruttare i potenti strumenti C.A.S.E. (per la generazione automatica di
codice).
5.1.1 Class Diagram
I diagrammi delle classi (class diagram) sono uno dei tipi di diagrammi che possono
comparire in un modello UML. In termini generali, consentono di descrivere tipi di entità, con
le loro caratteristiche, funzioni e le eventuali relazioni fra questi tipi. Gli strumenti concettuali
utilizzati sono l'idea di classe del paradigma object-oriented e altri correlati (per esempio la
generalizzazione, che è una relazione simile al meccanismo object-oriented dell'ereditarietà).
Nel nostro class diagram si possono identificare cinque entità principali che sono:
•
Maze, che rappresenta il labirinto con le sue caratteristiche e funzionalità
•
Pawn, che rappresenta il pedone che percorrerà il labirinto
•
Solver, che è il vero e proprio "cervello" di tutta l'applicazione. In relazione di
aggregazione con Solver, si trovano sia Pawn che Maze; entrambi verranno infatti
sfruttati nel processo di computazione, muovendo la pedina (o le pedine nel caso di
algoritmo parallelo) nello specifico labirinto. Dovendo specializzare il programma
nell'esecuzione dei tre algoritmi citati nei requisiti funzionali(sequenziale, parallelo e
greedy), abbiamo deciso di creare tre specializzazioni del solver che implementassero
appunto i tre algoritmi, aggiungendo quindi le opportune funzionalità rispetto a quelle
già presenti nel solver.
•
GUI, che rappresenta l'interfaccia grafica che utilizzerà l'utente, è stata pensata e
realizzata in modo tale da essere completamente indipendente dal resto del
programma anche in visione di future modifiche sul metodo di interazione con l'utente.
Questo ci è stato reso possibile dall'uso di uno specifico design pattern (observer) che
spiegheremo nel paragrafo successivo.
•
Parallel Controller, a cui è assegnato il compito di gestire e controllare la molteplicità di
pedoni generati durante l'esecuzione dell'algoritmo parallelo.
13
5 Progettazione
14
5 Progettazione
5.1.2 Design Pattern
In questa fase progettuale abbiamo deciso di sviluppare il nostro programma
implementando quattro design pattern differenti. I design pattern sono infatti delle classi
strutturate tra loro in modo tale da ottenere determinate caratteristiche implementative e/o per
aggiungere particolari funzionalità al programma, avendo comunque la certezza che non
venga compromessa la funzionalità dell'intero sistema. I design pattern si dividono in tre
tipologie: creazione, struttura e comportamento. In particolare i design pattern da noi utilizzati
sono i seguenti:
Singleton
Questo particolare design pattern di creazione ci ha permesso di rendere la classe Maze a
singola istanza ciò significa che durante l'esecuzione del programma non sarà possibile
istanziare più di un oggetto di tipo Maze. Questo è stato fatto per impedire l'esecuzione
concorrente di più algoritmi su più labirinti cosa che avrebbe reso il sistema instabile e
logicamente incomprensibile.
Prototype
15
5 Progettazione
Anche questo è un design pattern di creazione (come quello visto sopra). Abbiamo
implementato la classe “Pawn” con questa struttura per poter sfruttare la sua funzionalità di
clonazione che quindi rende l'istanziazione di un nuovo oggetto di questo tipo molto più
leggera e veloce. La clonazione, infatti, annulla i costi di accesso al disco che sarebbero stati
necessari per caricare ogni volta la grafica di un pedone clonandola direttamente da
un'istanza già presente in memoria.
Template Method
A differenza dei primi due, questo design pattern è di tipo comportamentale. Abbiamo
sintetizzato tutti i comportamenti comuni nel processo risolutivo degli algoritmi in un'unica
classe astratta implementandoli all'interno di metodi e lasciando invece i “passi risolutivi”
differenti di ogni algoritmo in metodi astratti che poi verranno implementati dalla classe che li
erediterà.
16
5 Progettazione
Observer
Anche questo come il precedente è un design pattern di comportamento. Lo abbiamo
implementato per rendere il risolutore indipendente dall'interfaccia grafica. Infatti il risolutore
durante la sua esecuzione non deve chiamare al suo interno metodi della GUI ma solo il suo
metodo notify() che si preoccuperà di “informare” la GUI (Graphic User Interface) che ci sono
degli eventi da visualizzare all'utente. Si noti come il pattern observer è stato utilizzato
inglobando al suo interno il pattern Template Method. Questo è un esempio molto importante
di utilizzo dei design pattern poiché mostra come possano venire utilizzati gli uni insieme agli
altri. L'utilizzo dei design pattern non è infatti mutualmente esclusivo.
17
5 Progettazione
5.1.3 OCL
L'Object Constraint Language, o OCL, è un linguaggio di specifica formale che fa uso della
logica del primo ordine. Questo permette di imporre vincoli su specifiche operazioni definite in
altri diagrammi dello standard UML. E' infatti possibile aggiungere l'OCL direttamente nei
diagrammi sotto forma di annotazione o, come nel caso che segue, riportarlo in un
documento separato. Normalmente, prima di porre vincoli OCL è bene avere un'idea chiara
delle classi che comporranno il progetto, quindi, dei loro metodi ed attributi principali. L'OCL è
uno strumento complementare a qualunque diagramma ed è utilizzato per dare
un'indicazione di come si comporteranno i metodi, cosa e come andranno a modificare, quali
valori potranno assumere gli attributi e così via.
Nel progetto del labirinto diverse informazioni acquisite nel corso dell'analisi hanno avuto
necessità di essere espresse come vincolo aggiuntivo ai diagrammi. Riprendendo il codice
Alloy del capitolo 4.2 si possono notare delle analogie tra i fatti allora discussi ed i vincoli
imposti tramite OCL nell'attuale fase di progettazione.
// Maze
context Maze::Maze( Width : Integer, Height : Integer, Obstacles : Integer )
pre:
Width = Height
Obstacles >= 0 and Obstacles <= Width * Height - 2
In questo contesto andiamo ad imporre che tratteremo solo labirinti di forma quadrata
esattamente come nel Fatto B di Alloy. Imporremo anche che il numero di ostacoli nel labirinto
dovrà sempre essere in numero maggiore di zero (sarebbe una situazione inconsistente in
caso contrario) e in numero massimo tale per cui ci siano due posti liberi (punto di partenza
ed uscita) come definito dal fatto C. Si potrebbe pensare che manchi un vincolo sulla
dimensione del labirinto, come definito dal fatto D di Alloy. In realtà non è stato introdotto
poiché in fase progettuale, con un occhio rivolto verso possibili future richieste del cliente, si è
pensato che fosse opportuno puntare all'adattabilità del software per ogni evenienza.
context Maze::GetStatus( Position : Point ) : Boolean
pre:
Position.X >= 0 and Position.X < self.Width
Position.Y >= 0 and Position.Y < self.Height
post :
if (one(self.field(Position.X,Position.Y)=1) then result = false else result =
true
Come espresso dal fatto H, si vincola la ricerca dello stato di una cella del labirinto al range
di celle possibili.
Attraverso la keyword post si specifica cosa ci si aspetta restituisca il futuro metodo. In
questo caso ci si aspetta che in presenza di un muro (cioè un intero pari ad uno) ritorni un
valore booleano di false; al contrario, in presenza di una strada percorribile, ritorni true.
// Pawn
context Pawn::status : Integer
init: 0
In questo frammento OCL si va a specificare con quale valore iniziale dovrà presentarsi
uno specifico attributo. Nello specifico, l'attributo "status" di Pawn avrà valore zero alla sua
istanziazione.
18
5 Progettazione
context Pawn
inv: status >= 0 and status < icons->size()
In questo frammento di codice OCL si specifica un'invarianza (inv). Una invarianza è
un'espressione che deve essere sempre rispettata durante tutta l'esecuzione
dell'applicazione. Nel caso del labirinto si è reso necessario specificare che l'attributo status
di Pawn non dovrà mai, per alcun motivo, superare in valore la lunghezza di icons. "icons"
sarà un vettore di percorsi ad immagini su disco (sotto forma di stringhe), una per ogni stato
possibile della pedina (pedina in direzione ovest, sud, est, nord, pedina addormentata e così
via). "status" rappresenta invece lo stato corrente in cui si trova la pedina, ed è di fatto,
l'indice con cui prelevare il path all'immagine nel vettore di "icons". A seguito di questa breve
spiegazione dovrebbe risultare più chiaro il motivo per cui status non può, in quanto indice di
un array, essere minore di zero o eccedere la sua dimensione.
context Pawn::Clone() : Pawn
post:
Pawn::allInstances()->size() = Pawn::allInstances()@pre->size()+1
self.position = result.position
self.icons = result.icons
self.status = result.status
self.steps = result.steps
Con questa specifica OCL si vuole definire il comportamento della clonazione di una
pedina. Ci si trova nel caso di esecuzione dell'algoritmo parallelo, in cui ogni pedina avanza
per mezzo della clonazione. Il codice specifica che il numero di pedine a seguito
dell'invocazione del metodo in questione dovrà essere incrementato di uno a partire dal
valore che possedeva prima dell'invocazione. Si noti la presenza del suffisso-keyword "@pre"
per indicare appunto il valore posseduto da una certa variabile prima dell'invocazione del
metodo. Si noti inoltre l'uso di allInstances() per effettuare operazioni sull'intera collezione di
istanze di quella classe.
Seguono altri esempi di specifiche OCL.
// Solver
context Solver::nSteps : Integer
init: 0
context Solver::pause : Boolean
init: false
context Solver::solved : Boolean
init: false
context Solver::Solve() : void
post:
if self.player.position.X@pre <> self.player.position.X or
self.player.position.Y@pre <> self.player.position.y then
self.nSteps = self.nSteps@pre + 1 else self.nSteps = self.nSteps@pre
19
5 Progettazione
5.1.4 Sequence Diagram
Un altro diagramma fondamentale dell'UML è il Sequence Diagram. In questa nuova
rappresentazione vengono generalmente riproposti tutti quei flussi già esaminati negli Activity
Diagram ma in maniera molto più approfondita e più vicina a quella che sarà poi la reale
stesura dell'algoritmo. Inoltre questo diagramma offre una visione di come differenti oggetti
collaborino all'interno di uno stesso flusso per il raggiungimento di uno scopo comune. In
esso si riportano tutti gli attori e i sistemi che hanno un “ruolo” nel flusso che si vuole
rappresentare. Da questi elementi, che sono disposti l'uno di fianco all'altro, vengono poi fatte
partire delle linee tratteggiate perpendicolari che rappresentano le “lifelines” (le linee della
vita) di ognuno. Si passa poi a mostrare i “messaggi”, disegnati mediante delle frecce
orizzontali orientate che collegano le lifelines di due elementi (non necessariamente distinti),
che hanno come descrizione i nomi dei metodi che vengono invocati. Questi messaggi
scatenano l'esecuzione dei metodi da loro riferiti sugli elementi puntati: le vite di questi metodi
vengono rappresentate mediante dei rettangoli che si sovrappongono alle rispettive lifeline.
Chiaramente da ogni rettangolo possono partire altre frecce che a loro volta genereranno altri
rettangoli in altri elementi del diagramma o su se stessi in caso che la freccia si riferisca allo
stesso elemento che l'ha scaturita. Il fatto che un rettangolo ritorni un valore al rettangolo che
l'ha generato si esprime attraverso una freccia tratteggiata di verso opposto a quella che ha
dato vita al metodo stesso.
20
5 Progettazione
Nel Sequence Diagram seguente, si mostra il flusso rappresentato dall'Activity Diagram del
capitolo precedente mostrando l'attore che scatena l'esecuzione dell'algoritmo sequenziale
(:User) attraverso la chiamata della funzione Sequential() all'oggetto di tipo GUI. Quest'ultimo,
quindi, crea l'oggetto di tipo Sequential attraverso la freccia “«create»” e poi sull'elemento
appena creato invoca il metodo Run(). All'interno di questo metodo l'oggetto di tipo Sequential
invoca la sua stessa funzione Solve() (notare come il rettangolo rappresentante la vita del
metodo Solve si sovrappone al rettangolo rappresentante la vita del metodo Run che lo ha
invocato). Viene quindi invocato, dal metodo Solve, il metodo ChooseDirection() dal quale
parte un messaggio verso l'oggetto di tipo Maze sul quale scatena la funzione
GetStatus(dest: Point) che al termine ritorna, attraverso una freccia tratteggiata, all'oggetto di
tipo Sequential. Quest'ultimo invoca il metodo Move(dest: Point) sull'oggetto di tipo Pawn e
poi effettua un chiamata su se stesso che sappiamo essere una chiamata ricorsiva.
In UML 1 un Sequence Diagram poteva solo riportare flussi in cui ogni “decisione” era stata
presa a priori e quindi non poteva rappresentare “salti” (if-then-else) o “cicli” (do-while). Con
l'UML 2 è invece possibile rappresentare anche questo genere di istruzioni. Ad esempio,
attraverso l'operatore “alt” si può rappresentare un salto condizionato (il costrutto if-then-else),
oppure tramite l'operatore “par” si possono descrivere due porzioni di codice che verranno
eseguite in parallelo o ancora con l'operatore “loop” si possono definire i cicli delimitati da
guardia (come il costrutto while).
21
5 Progettazione
Per mostrare questo nuovo tipo di funzionalità implementate dall'UML 2.0 abbiamo deciso
di riportare il Sequence Diagram che descrive ciò che succede quando, durante l'esecuzione
di Sequential, l'utente preme il tasto di “Play/Pause” per pausare o riprendere la risoluzione
del labirinto. Quando l'attore “:User” lancia il metodo “PlayPause()” sull'oggetto di tipo GUI si
entra nel frammento combinato definito dall'operatore “alt” per specificare un costrutto, come
detto sopra, di tipo if-then-else. Questo frammento infatti controllerà “se paused = false allora
l'oggetto di tipo GUI invocherà il metodo SetPaused(true) sull'oggetto di tipo Sequential
altrimenti l'oggetto di tipo GUI invocherà il metodo SetPaused(false) sull'oggetto di tipo
Sequential”.
22
5 Progettazione
5.1.5 Package Diagram
Il Package Diagram in UML descrive le dipendenze che ci sono tra i vari pacchetti che
definiscono il modello. Come per gli altri diagrammi UML anche per il Package Diagram
esistono le relazioni che fanno da legame tra un package ed un altro. Nel nostro caso
abbiamo legato i tre pacchetti da relazioni di uso e da relazioni di chiamata. Come già detto in
precedenza la GUI risulta indipendente rispetto al core del programma, mentre il solver e il
labirinto sono strettamente legati tra di loro, perciò risiedono entrambi nello stesso package
Core, legati da una relazione d'uso.
5.2 Petri Net
La Rete di Petri è un formalismo utilizzato per descrivere l'evoluzione di un sistema
concorrente. Lo spostamento di unità chiamate “token” da un nodo, detto “piazza”, ad un altro
attraverso delle “transizioni” riesce infatti a descrivere l'evoluzione di qualsiasi “processo”, che
esso sia semplicemente sequenziale oppure parallelo. In questa relazione presentiamo due
esempi di Reti di Petri utilizzate per modellare due aspetti, entrambi con esecuzione
concorrente, del nostro progetto.
Nella prima rete (quella nell'immagine seguente) viene modellata la realtà riguardante
l'esecuzione di qualsiasi algoritmo di risoluzione contemporaneamente alla gestione del
comando di pausa presente nella GUI. In pratica dalla piazza “Start” parte un token che si
sdoppia mandando un token alla piazza “Play” (per la gestione della pausa) ed un token alla
piazza “Execute” (che rappresenta l'esecuzione di un algoritmo di risoluzione). Grazie alla
23
5 Progettazione
transizione “Check Play/Pause” che richiede un token da ognuna delle due piazze definite
sopra, obblighiamo il sistema a poter proseguire nella risoluzione e quindi nella
rappresentazione solo se, appunto, c'è un token nella piazza di “Play”: infatti se il token dalla
suddetta piazza fosse passato alla piazza “Pause” la transizione non potrebbe avere luogo e
questo obbligherebbe l'esecuzione a fermarsi fintanto che il token presente nella piazza
“Pause” non ritorni nella piazza “Play”. Ogni volta che ha luogo l'esecuzione, l'opzione di
mettere in pausa il programma ritorna nuovamente disponibile solo quando dalla piazza di
“Draw” il token scatta attraverso la transizione “Next Iterate” riportando quindi la rete nella sua
configurazione iniziale. In ultimo quando il token passa dalla piazza “Draw” alla piazza “End”
l'esecuzione dell'algoritmo termina.
Nella seconda rete (quella riportata sotto) mostriamo l'interazione tra la classe Parallel e la
classe ParallelController durante la risoluzione di un labirinto mediante l'algoritmo parallelo.
Dalla piazza “Start” parte un token che attraverso la transizione “Initializing” si sdoppia e va
nella piazza “ParallelController” e nella piazza “Thread Creator”. Dalla piazza
ParallelController il token, nella sua prima iterazione, è obbligato ad andare nella transizione
“Launching Threads” (in quanto per la transizione “Finalizing” sarebbero richiesti due token).
Launching Theads manda un token ad ognuno degli N thread nelle rispettive piazze “Thread x
Exec”. Da qui in poi, ogni thread dovrà spostarsi dalla piazza di Exec alla piazza di “Thread x
Terminate” attraverso la rispettiva transizione “Drawing x”; questa però potrà scattare solo
quando la piazza “Graphic Elaborator”, che rappresenta l'unità di elaborazione grafica del
computer e che quindi può lavorare ad una sola “stampa” alla volta, possiederà il suo token
(che rappresenta la possibilità dell'unità grafica di elaborare); in questo modo abbiamo
mostrato la mutua esclusione della risorsa grafica. Ogni volta che un token si sposta nella
piazza “Thread x Terminate” può far scattare la rispettiva transizione “Notification x” che
restituirà un token alla piazza dell'unità grafica, per permettere l'elaborazione di eventuali altri
thread che attendono il loro turno, e manderà un altro token alla piazza comune “Sync
Controller”. Soltanto quando tutti i thread avranno terminato, e quindi saranno presenti N
token nella piazza suddetta, potrà scattare la transizione “Notification” (questo perchè il peso
dell'arco entrante in questa transizione ha valore N) che manderà due token alla piazza
ParallelController (perchè il peso dell'arco uscente è 2); a questo punto l'unica transizione che
24
5 Progettazione
potrà scattare sarà “Finalizing” (perchè la piazza “ParallelController” possiede due token e
perchè per far scattare di nuovo la transizione “Launching Threads” sarebbe necessario che
in “Threads Creator” fosse presente il token già consumato ad inizio esecuzione) che porterà
dunque la rete nella sua piazza finale denominata appunto “End”.
Nota: in questo secondo esempio sono rappresentati soltanto i cicli di tre thread
rappresentando tutti gli altri attraverso “......” in ogni loro stato al fine di rendere la rete più
comprensibile nella lettura e più chiara nel disegno.
25
6 Implementazione
6 Implementazione
Dal momento che questa relazione è incentrata sulla dimostrazione delle tecnologie
utilizzabili nell'ingegneria del software l'implementazione, intesa come stesura del codice
Java compilabile ed eseguibile, verrà tralasciata. Saranno invece presentati in questo capitolo
strumenti “ausiliari” dell'implementazione come il JML ed i Deployment Diagram dell'UML.
6.1 JML
Il Java Modeling Language, abbreviato JML, è un linguaggio di specifica per Java con
comportamento simile a quello di OCL. Tramite le specifiche JML si impone che un certo
metodo abbia un ben preciso comportamento, accetti parametri di un certo tipo e che svolga
solo determinate operazioni. E' possibile sviluppare codice JML con il fine di lasciare
l'implementazione a terzi con la sicurezza che (se le specifiche di partenza erano corrette) il
codice sviluppato esegua realmente quanto stabilito in fase di progettazione. JML è quindi sia
strumento di controllo che di documentazione sul comportamento delle classi (e loro metodi
ed attributi) per i programmatori, siano essi interni o esterni al team di progettazione.
Quelli che dapprima erano fatti Alloy in fase di analisi e successivamente specifiche OCL in
fase di progettazione, sono ora specifiche per il linguaggio JML, di facile lettura e
comprensione per programmatori Java.
public class Maze
{
private /*@ spec_public
private /*@ spec_public
private /*@ spec_public
private /*@ spec_public
private /*@ spec_public
@*/
@*/
@*/
@*/
@*/
int width;
int height;
int obstacles;
int[][] field;
int nSteps;
...
/*@
@ requires Width == Height
@ requires Obstacles >= 0 && Obstacles <= Width * Height – 2
@
@ ensures nStpes == 0
@*/
private Maze(int Width, int Height, int Obstacles)
{
...
}
In questo primo stralcio di codice si forza il metodo Maze a fare controlli sulla validità dei
parametri passatigli, secondo quanto già discusso in Alloy ed OCL. Si noti che per accedere a
d attributi definiti in Java come private, va modificata la loro visibilità a livello JML tramite la
specifica /*@ spec_public @*/. Questo è per il compilatore Java alla stregua di un commento
ed in quanto tale, completamente ignorato. Si esplica inoltre che il costruttore dovrà occuparsi
della corretta inizializzazione degli attributi. Nello specifico, che nSteps venga inizializzato a
zero.
26
6 Implementazione
/*@
@ normal_behavior
@ requires
@
Position.X >= 0 && Position.X <= width;
@ requires
@
Position.Y >= 0 && Position.Y <= height;
@
@ assignable \nothing;
@
@ ensures
@
field[Position.X][Position.Y] == 1 ==> \result == false;
@ ensures
@
field[Position.X][Position.Y] == 0 ==> \result == true;
@
@ exception_behavior
@ signals (MazeOutOfBound e)
@
e.getMessage != null &&
@
!(Position.X >= 0 && Position.X <= width) &&
@
!(Position.Y >= 0 && Position.Y <= height);
@*/
public boolean GetStatus( Point Position ) throws MazeOutOfBound
{
...
}
}
Anche in questo caso come prima, si obbliga il programmatore a controllare l'effettiva
consistenza dei parametri in ingresso (requires). Si stabilisce inoltre tramite la specifica di
assignable che nel corso dell'esecuzione del metodo GetStatus, nel suo corpo, non sarà mai
possibile assegnare alcun valore a qualche variabile. Tramite la specifica di ensures si
stabilisce cosa si vuole il metodo abbia fatto dall'inizio alla fine della sua esecuzione. In
questo caso si vuole che il metodo ritorni true nel caso in cui la cella del labirinto nelle
coordinate Position.X e Position.Y (field[Position.X][Position.Y]) presenti un valore pari a 0 o
false in presenza di un valore pari ad 1.
public class Pawn
{
private /*@ spec_public @*/ int status;
private /*@ spec_public @*/ string[] icons;
//@ assert status < icons.length;
...
}
Nel frammento di codice qui sopra, si stabilisce una sorta di invarianza, che dovrà rimanere
sempre vera durante l'intera esistenza di ogni oggetto Pawn. Si sta imponendo che status,in
qualità di indice dell'array “icons”, non superi mai la sua lunghezza.
27
6 Implementazione
public class Sequential extends Solver
{
private /*@ spec_public @*/ int nSteps;
//@ ensures \modified(player) => nSteps == \old(nSteps) + 1;
//@ ensures \not_modified(player) => nSteps == \old(nSteps);
public Solve()
{
...
}
...
}
In questo frammento si va a specificare il comportamento del metodo Solve della classe
Sequential. Nello specifico, se durante l'esecuzione del metodo la pedina ha avuto modo di
spostarsi, il valore dei passi percorsi sarà pari al valore precedente incrementato di uno (un
po' come accadeva in OCL con la parola chiave @pre). Se invece la pedina non si è potuta
spostare da nessuna parte l'implicazione logica impone che il valore di “nSteps” prima e dopo
l'invocazione del metodo rimanga la stessa.
6.2 UML (3)
Concludiamo l'utilizzo dello standard UML con gli strumenti che esso ci fornisce per la fase
post-implementativa.
6.2.1 Deployment Diagram
Il Deployment Diagram ("diagramma di dispiegamento") è un diagramma di tipo statico
previsto dal linguaggio di modellazione object-oriented UML per descrivere un sistema in
termini di risorse hardware, dette nodi, e di relazioni fra di esse. Si utilizza questo diagramma
per mostrare come le componenti software siano distribuite rispetto alle risorse hardware
disponibili sul sistema.
28
7 Test
7 Test
Questa è l'ultima delle cinque macro-fasi che descrivono lo sviluppo del software ed è
anch'essa estremamente importante e delicata. Un programma perché possa essere
consegnato al committente o immesso nel mercato ha bisogno di essere garantito come
funzionante. Testare tutte le possibilità di stati in cui può transitare un programma sarebbe
una cosa dispendiosa e difficile per piccoli progetti, pressoché impossibile per programmi di
grandi dimensioni. Ecco dunque che diventano indispensabili altri tool specifici per il testing,
che possano generare casi di test e dare una mano automatizzando controlli altrimenti
eccessivamente lunghi e a loro volta soggetti ad errori.
7.1 Junit
JUnit è un unit test framework per il linguaggio di programmazione Java, introdotto per
agevolare il lavoro di manutenzione e configurazione del codice.
Con unit testing si intende la procedura usata per verificare le singole parti di un codice
sorgente.
Se come nel nostro caso si decide di procedere al test del codice utilizzando JUnit, si
devono creare delle apposite classi di test che controlleranno pezzo per pezzo il corretto
funzionamento dei metodi.
JUnit utilizza delle annotazioni per marcare i metodi incaricati di fare i test:
@Test
public void method() // Identifica il metodo
@Before
public void method() // Prima di ciascun test
@After
public void method() // Alla fine di ciascun test
@BeforeClass
public void method() // Prima di tutti i test
@AfterClass
public void method() // Alla fine di tutti i test
@Test(expected=Exception.class) // Test che deve generare eccezione
@Test(timeout=100) // Massimo tempo di esecuzione
All'interno del metodo preceduto da @Test si potranno utilizzare diverse asserzioni per
verificare il corretto comportamento del metodo:
•
assertEquals(e,a) il valore atteso e deve essere uguale al valore attuale
•
assertArrayEquals(e,a) i due vettori e ed a devono essere uguali in dimensione e
contenuto
•
assertTrue(c) richiede che il valore c sia true
•
assertFalse(c) richiede che il valore c sia false
•
assertNull(o) richiede che o sia un riferimento nullo
•
assertNotNull(o) richiede che o sia un riferimento non nullo
29
7 Test
In riferimento al nostro codice JUnit, abbiamo deciso di testare il funzionamento di tre
metodi in particolare:
•
il metodo Clone() della classe Pawn, del quale andremo a testare se effettivamente
esegue una copia esatta del pedone da cui viene invocato
import org.junit.*;
import static org.junit.Assert.*;
public class PawnTest {
private Pawn p;
@Before
public void setUp(){
p = new Pawn();
}
@Test
public void testClone(){
assertEquals(p, p.clone());
}
@After
public void TearDown() { p = null; }
}
•
il metodo GetStatus() della classe Maze, del quale andremo a testare se passati due
punti, che rappresentano uno il punto di partenza è uno l'uscita, ritorni true perchè in
base alle specifiche non ci devono essere ostacoli posizionati ne sul punto di partenza
ne sull'uscita, inoltre questa classe di test controlla anche se viene lanciata
un'eccezione di tipo MazeOutOfBound
public class MazeTest {
private Maze m;
@Before
public void setUp(){
m = new Maze();
}
@Test (expected=MazeOutOfBound.class)
public void testGetStatus() throws MazeOutOfBound{
Point start = new Point (0,0);
Point end = new Point (m.GetWidth-1,m.GetHeight-1):
assertTrue(m.GetStatus(start));
assertTrue(m.GetStatus(end));
}
@After
public void TearDown() { m = null; }
}
30
7 Test
•
il metodo CheckDirection() della classe Sequential, del quale andremo a testare se
effettivamente ritorna false se invocato su un Maze di dimensioni 5x5 con 23 ostacoli
public class SequentialTest {
private Sequential s;
@Before
public void setUp(){
s = new Sequential(new Maze(5,5,23),new Pawn(new Point(0,0),0), new
OutputPrinter());
}
@Test
public void testCheckDirection(){
Point destination = new Point (1,1);
assertFalse(s.CheckDirection(destination));
}
@After
public void TearDown() { s = null; }
}
8 Conclusioni
Sviluppare questo progetto è stato un ottimo modo per imparare ed approfondire,
mediante l'utilizzo diretto, le tecniche di analisi e progettazione di un software, nonostante il
nostro non era un progetto di grandi dimensioni. Inoltre possiamo aggiungere che il lavoro di
gruppo è stato formativo e ha reso la cosa molto simile ad un vero team di sviluppo. In
conclusione speriamo di aver reso il più comprensibile possibile tutte le fasi affrontate.
31
9 Appendice
9 Appendice
9.1 Screenshot del programma
32
9 Appendice
9.2 Programmi utilizzati
La realizzazione di questo progetto ci è stata resa possibile anche grazie all'aiuto di diversi
strumenti di sviluppo come:
•
Alloy v4
•
Violet
•
Star UML
•
Pipe v4
•
Microsoft Visio 2010
•
Adobe Photoshop
Inoltre l'interazione e la coordinazione tra i membri del gruppo è stata facilitata dall'uso di
software come:
•
Dropbox
•
Skype
9.3 Fonti
•
UML Distilled Third Edition
•
Slide del corso
•
Wikipedia
9.4 Ringraziamenti
Si ringraziano il cliente (nonché docente del corso) Rosario Culmone, il docente Roberto
Gagliardi ed il dipartimento di Informatica per i mezzi forniti.
33