Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Lorenzo Laneve
• pianificare le attività e le risorse necessarie (p.es. sapere quando servono certe parti e quante ne
servono);
• prevedere e controllare i costi del processo e la prevedere e controllare i costi del processo e la qualita
dei prodotti.
• analisi e specifica dei requisiti: individuazione delle caratteristiche del software e descrizione non
ambigua di esse;
• progetto: produce un ”Documento di Progetto” e definisce le componenti
– di massima (architettura): scomposizione dei moduli e delle relazioni tra essi (supporta lo
sviluppo parallelo tra essi e la suddivisione delle responsabilità);
– di dettaglio: progettazione dei singoli moduli nel dettaglio, in particolare le interfacce e le
modalità di verifica;
• implementazione e test:
– codifica: codifica dei moduli mediante opportuni linguaggi di programmazione e prime verifiche
statiche;
– test di unità: ogni modulo viene verificato e testato isolatamente;
– test di integrazione: si uniscono man mano i moduli testandone i legami, fino ad averli connessi
tutti;
– test di sistema: si convalida il sistema rispetto ai suoi requisiti funzionali;
• deployment: viene distribuito e installato il software sulle macchine dei clienti, il personale addetto
al suo utilizzo viene addestrato;
• modifica e manutenzione:
1
Le evoluzioni del software sono generalmente dovute a mutamenti nel contesto o dei requisiti, o a specifiche
errate.
Il modello a cascata presenta diversi problemi:
• è un insieme di fasi eseguite in rigida sequenza;
• la fase di manutenzione è tenuta in minore considerazione, che non viene strutturata in termini di
attività elementari;
• troppo poco pratico e realistico: non permette di parallelizzare processi che possono essere svolti
contemporaneamente (diverse parti del software);
• coinvolge poco l’utente, che potrebbe dare il suo contributo (es. trovando errori nei requisiti);
Durante questo processo si sviluppa anche un prototipo, che è un modello approssimato dell’applicazione.
L’incrementalità è fondamentale per una rapida uscita sul mercato e la fidelizzazione del cliente. Spesso,
i requisiti si raccolgono man mano, a ogni iterazione del modello.
2 Il linguaggio UML
Il linguaggio UML è diventato uno standard de facto per la modellizzazione di software orientati agli oggetti.
• classi
• relazioni tra classi:
– associazione (uso);
– generalizzazione (ereditarietà);
– aggregazione (contenimento).
• nome;
• attributi (stato dell’oggetto);
• metodi (il comportamento).
Inoltre, è possibile definire la visibilità di ciascuno di essi, apponendo uno tra i simboli + (public), -
(private), # (protected), ∼ (package-local).
Il tipo degli attributi, del ritorno dei metodi, e dei loro parametri è definibile con :, mentre eventuali
componenti statiche della classe si sottolineano. Le classi possono essere raggruppate in package.
2
2.1.2 Generalizzazione
Le classi possono ereditare da altre classi (freccia continua con testa bianca) e implementare da interfacce
(freccia tratteggiata con testa bianca). UML ammette l’ereditarietà multipla.
• oggetto: concetto, astrazione o entità con un significato ben preciso per l’applicazione;
3 Astrazioni Procedurali
• Abstraction by parameterization: generalizzare un modulo per usarlo su dati diversi (esempio con abs);
• Abstraction by specification: non importa come una funzione venga implementata, basta che si
comporta come ci si aspetta;
– località: la specifica può essere letta o scritta senza necessità di esaminare l’implementazione;
– modificabilità: reimplementazione senza intaccare il codice cliente;
Un contratto può essere soddisfatto in molti modi, ma cambiano le proprietà non funzionali (complessità).
3
3.1 JML
3.1.1 Clausole
Attensione: ricordarsi il semicolon alla fine di ogni clausola.
4
• producers: da oggetti di quel tipo ne creano altri (esempio Poly.add(Poly) → Poly);
• mutators: modificano lo stato dell’oggetto;
• observers: si limitano a restituire informazioni sul tipo (valori di un altro tipo).
Tramite i metodi si separa l’oggetto astratto (Polinomio) dall’oggetto concreto (array di interi).
Un tipo è adeguato se fornisce operazioni sufficienti. Un buon modo di verificare l’adeguatezza è:
• se mutabile almeno creators, observers, mutators;
Si definisce una funzione di astrazione AF : ConSt → AbsSt, che associa uno stato concreto a uno
stato astratto. Si noti che questa funzione non è necessariamente iniettiva. Esempio: un insieme di interi
non conta l’ordine, mentre l’array che lo implementa si, quindi ogni permutazione del’array è associata
tramite AF allo stesso insieme. Si può definire indirettamente AF tramite un invariante privato, definendo
la relazione tra il ritorno degli observer e i valori concreti dell’oggetto.
Definiamo un invariante di rappresentazione J : ConcSt → bool tale per cui J(a) = true ⇔ a è un
oggetto valido per la rappresentazione astratta. Infatti, AF non è totale: esistono oggetti concreti che non
hanno un corrispettivo astratto, e J è vero se e solo se AF è definito. Esempio: [1, 2, 2] come array che
rappresenta un insieme quando si è deciso di rimuovere i duplicati. L’invariante di rappresentazione si può
esprimere in JML come invariante privato.
Chiamiamo effetto collaterale benevolo un passaggio da uno stato S a uno stato T tale per cui
AF (S) = AF (T ). Servono a trasformare l’oggetto concreto, magari per ragioni di efficienza, mantenendolo
inalterato agli utilizzatori.
5
5 Design Patterns
5.1 Pattern creazionali
• Singleton: tenere privato il costruttore e dare l’accesso ad un’unica istanza della classe tramite metodo
statico (synchronized sulla creazione istanza per thread safety);
• Factory: viene definita una classe C con un metodo che crea un’oggetto di una classe T e per ogni
sottoclasse di T viene definita una sottoclasse di C che fa l’override del metodo menzionato. Al posto
di chiamare il costruttore a seconda della classe, si chiama il metodo di un oggetto generico di C;
• Abstract Factory: come Factory, ma restituisce un oggetto generico di una classe astratta, da cui
eredita una famiglia di classi da poter costruire.
6
6 Design Principles
• Principio Open-Closed: ogni classe deve essere aperta alle estensioni ma chiusa alle modifiche (se
non modifico la classe non rischio di fare errori);
• Principio di Sostituzione di Liskov: gli oggetti della sottoclasse devono rispettare il contratto della
superclasse, in modo che qualsiasi codice che utilizza un oggetto della superclasse possa utilizzare un
oggetto della sottoclasse senza differenze;
– Coesione di Metodo: la procedura implementa una singola e ben definita funzione, descrivibile
con una sola e semplice frase;
– Coesione di Classe: la classe rappresenta un solo concetto e tutti gli attributi contribuiscono
alla sua rappresentazione. Diversi concetti rappresentati dalla classe o gruppi di metodi diversi
che lavorano con insiemi di attributi disgiunti sono sintomi di bassa coesione;
– Coesione di Ereditarietà: in generale, ereditarietà introdotte per generalizzazione/specializza-
zione implicano coesione maggiore di quelle introdotte per riuso del codice;
7
7.2 Metriche del Software
Definizione con caratteristiche comuni nei software professionali:
• Weighted Methods per Class (WMC): numero dei metodi pesati per la loro complessità;
– la maggior parte delle classi ha pochi metodi;
– più WMC è alto, più il software è propenso ad avere errori.
• Depth of Inheritance Tree (DIT)
– massimo 10;
– la maggior parte delle classi non hanno sottoclassi e sono dirette alla radice;
• Number of Children (NOC)
– spesso numero limitato di sottoclassi, in molti casi 0 (ereditarietà non sfruttata a pieno);
• Coupling Between Classes (CBC)
– spesso le classi sono auto-contenute (CBC = 0);
– gli oggetti di interfaccia tendono ad avere un CBC più grande.
• Response For a Class (RFC): somma di tutti i metodi invocabili dagli oggetti della classe (metodi
chiamati nel codice + metodi della classe stessa);
– la maggior parte delle classi invoca pochi metodi esterni;
– le classi di oggetti di interfaccia hanno RFC maggiore;
• Lack of Cohesion in Methods (LCOM): quanto i metodi di una classe accedono ad attributi
comuni.
– non molto correlato alla error-proneness;
Se un software è progettato male, esso tende a presentare sintomi come:
• Rigidità: difficoltà a cambiarlo, provoca cambiamenti a cascata;
• Fragilità: anche per un minuscolo cambiamento, tende a rompersi in molti punti;
• Immobilità: non si riesce a riusare software da altri progetti o moduli;
• Viscosità: quando è più facile usare scorciatoie che i metodi che rispettano la struttura del progetto.
Per ovviare a questi problemi, bisogna migliorare le dipendenze tra classi:
• minimizzare l’interfaccia delle classi;
• pochi parametri per i metodi;
7.3 Anti-Patterns
• classe Blob che contiene la maggior parte della logica applicativa;
• codice duplicato;
• metodi lunghi;
• mix di livelli di astrazione all’interno dello stesso metodo;
• far fare a un metodo più cose (ogni cosa un metodo);
• metodo che usa principalmente i dati di un’altra classe (fare un metodo in quella classe);
• uso dello switch per discriminare i tipi (sfruttare il polimorfismo);
• blocchi di dati (dati correlati dovrebbero stare in una classe a parte);
• commenti (racchiudere parti di codice in metodi con nome significativo);
8
8 Estensione vs Astrazione
Se vogliamo estendere una classe, dobbiamo stare attenti a non violare il principio di sostitusione di Liskov.
Questo implica che tutti i contratti della superclasse devono essere compatibili con quelli delle sottoclassi:
• Regola della segnatura: il metodo delle classi ereditate non cambia la segnatura;
– è possibile restringere il tipo di ritorno a un sottotipo (covarianza del risultato);
– è possibile allargare il tipo dei parametri a un supertipo (controvarianza dei parametri, NON
consentito in Java);
• Regola dei metodi: il contratto dei singoli metodi ereditati è compatibile con il contratto dei metodi
originali;
– i metodi ereditati devono rispettare la specifica degli originali, eventualmente indebolendo la
precondizione o rafforzando la postcondizione (estensioni impure);
– P REsuper ⇒ P REsub ;
– P OSTsub ⇒ P OSTsuper ;
– attenzione alle eventuali signals della superclasse.
• Regola delle proprietà: verifica che la specifica nel complesso sia compatibile con quella della
superclasse.
– tutti i metodi ereditati, nuovi o ridefiniti, inclusi i costruttori, devono conservare le proprietà
invarianti ed evolutive osservabili dagli observer della superclasse.
// s u p e r c l a s s e
@ r e q u i r e s A;
@ ensures B;
// s o t t o c l a s s e
@also
@requires C;
@ e n s u r e s D;
// r i s u l t a t o
@requires A | | C;
@ e n s u r e s ( \ o l d (A) ==> B) && ( \ o l d (C) ==> D) ;
9
9 Programmazione Concorrente in Java
Ricordare sempre di porre wait e notify sotto synchronized, e di catturare/rilanciare InterruptedException
• Lock.tryLock(), Lock.lockInterruptibly()
• Executor.execute(Runnable)
• Remote Method Invocation: stub, skeleton, proxy.
11 Testing
• Verifica: il programma soddisfa le specifiche?
• Testing: verifica sperimentale facendo girare il software con degli input selezionati
• Prova di correttezza: argomentare in modo formale o informale che il programma è corretto
• Convalida: assicurarsi che le specifiche o il sistema completo rispondono alla richiesta del committente
• Debugging: localizzare errori o difetti nel codice (il testing ne rivela la presenza ma non li localizza)
• Programmazione difensiva: insieme di tecniche che cercano di evitare il più possibile gli errori
durante la scrittura del codice
10
Il testing si dice esaustivo se comprende tutti i possibili input del programma (intrattabile in generale).
Un test può essere usato come controprova di correttezza, ma mai come prova. Dobbiamo trovare i casi di
test che massimizzino la probabilità di errore.
Dobbiamo scegliere opportuni casi di test, che siano sufficienti a ”convincerci” che il programma è corretto.
Si possono generare casi di test:
• In maniera casuale: evita le polarizzazioni del progettista, ma non esplora valori particolarmente
significativi;
• In maniera sistematica: partizioniamo lo spazio di input per ottenere casi di test con un particolare
obiettivo
– funzionale: in base a ciò che il componente deve fare (black-box);
– strutturale: in base alla struttura dell’implementazione (white-box).
• Copertura delle diramazioni: selezionare un insieme T di test case per un programma P tali per
cui, per ogni salto B in P esiste un test case in T che induce P ad eseguire B e uno che induce P a non
eseguirlo. Il seguente indice dice quanta copertura dei branch abbiamo:
Ncoperte
Ibr = Ntotali
• Copertura delle condizioni: circa come la copertura delle istruzioni. Bisogna fare in modo che i
test case raggiungano tutte le possibili condizioni dei costituenti della condizione, in modo da evitare
che alcune parti della condizione non vengano mai eseguite per colpa della short-circuit evaluation.
• Copertura delle cammini
In conclusione, il Black-box Testing è più semplice ed intuitivo, ma il white-box Testing permette di
complementarne i risultati ed avere maggiore confidenza sulla correttezza.
11