UNIVERSITÀ DEGLI STUDI DEL SANNIO Facoltà di Ingegneria CdiL in Ingegneria Informatica
Ingegneria del Software.pdf
Transcript of Ingegneria del Software.pdf
Item 1 - Considerare metodi statici factory invece dei costruttori
Il modo normale per una classe per consentire a un client di ottenere un'istanza di se stessa è fornire un
costruttore pubblico . Vi è un'altra tecnica che dovrebbe essere parte di ogni toolkit di un programmatore .
Una classe può fornire un metodo pubblico statico factory , che è semplicemente un metodo statico che
restituisce un'istanza della classe . Esempio : Questo metodo converte un valore primitivo booleano in un
riferimento a un oggetto booleano:
public static Boolean valueOf ( boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE ;}
Si noti che un metodo statico factory non è lo stesso del modello Factory Method dei Design Patterns. Il
metodo statico factory descritto in questo item non ha rilevanza con quello dei Design Patterns. Una classe
può fornire i propri clienti attraverso metodi statici factory invece di, o in aggiunta a , costruttori . Fornire
un metodo statico factory invece di un pubblico costruttore presenta sia vantaggi che svantaggi .
1) Un vantaggio dei metodi statici factory è che, a differenza costruttori , essi hanno nomi . Se i parametri
di un costruttore non li hanno , di per sé ,descrivono l'oggetto che viene restituito , un metodo statico
factory con un nome ben scelto è più facile da utilizzare e il codice client risultante è più facile da leggere .
2) Un secondo vantaggio di metodi statici factory è che, a differenza costruttori , essi non sono tenuti a
creare un nuovo oggetto ogni volta che vengono invocati . Questo consente classi immutabili ( item 15 ) per
utilizzare le istanze precostruite, per evitare di creare oggetti duplicati inutili . Il metodo Boolean.valueOf (
boolean) illustra questa tecnica : essa non crea mai un oggetto. Si può migliorare notevolmente le
prestazioni , se oggetti equivalenti vengono richiesti spesso , soprattutto se sono costosi da creare .La
capacità dei metodi statici factory di restituire lo stesso oggetto da ripetere nelle invocazioni consente alle
classi di mantenere uno stretto controllo su quali siano le istanze in qualsiasi momento . Le classi che fanno
questo sono dette istanze controllate. Ci sono diversi motivi per scrivere classi di istanza controllata. Il
Controllo di istanza consente a una classe di garantire che si tratta di un singleton ( item 3) o non-
instantiable ( item 4) . Inoltre, permette una classe immutabile ( item 15 ) utile per rendere la garanzia che
non esistono due istanze uguali.
3) Un terzo vantaggio dei metodi statici factory è che, a differenza costruttori , essi possono restituire un
oggetto di qualunque sottotipo del loro tipo di ritorno . Questo vi dà grande flessibilità nella scelta della
classe dell'oggetto restituito . Una applicazione di questa flessibilità è l' API che può restituire oggetti senza
che essi faccino le classi pubbliche . Nascondere le classi di implementazione in questo modo porta a una
API molto compatta . Questa tecnica si presta a interfacce – based framework. (item 18) , dove le interfacce
forniscono tipi di ritorno naturali per i metodi statici factory .Le interfacce non possono disporre di metodi
statici, così per convenzione , i metodi statici factory perun'interfaccia denominata Type sono messi in una
classe non-instantiable ( item 4 ) chiamata Types.
4) Un quarto vantaggio di metodi statici factory è che riducono il livello di dettaglio per creare istanze di tipi
parametrizzati . Purtroppo , è necessario specificare i parametri di tipo quando si richiama il costruttore di
una classe parametrizzata anche se sono evidenti dal contesto . Questo di solito richiede di fornire i
parametri di tipo due volte in rapida successione :
Map<String, List<String>> m =
new HashMap<String, List<String>>();
Lo svantaggio principale di fornire metodi statici factory è solo che classi senza costruttori pubblici o
protetti non possono essere sottoclassate . Lo stesso vale per le classi non pubbliche restituite da factories
statiche pubbliche . Per esempio , è impossibile sottoclassare qualsiasi delle classi di implementazione
convenienti nel Collection Framework. Probabilmente questo può essere una benedizione sotto mentite
spoglie , in quanto incoraggia i programmatori ad usare la composizione invece di eredità ( item 16), che è
un male. Un secondo inconveniente dei metodi statici factory è che essi non sono facilmente distinguibili
da altri metodi statici . Essi non si distinguono, nella documentazione API, nel modo in cui i costruttori lo
fanno e quindi può essere difficile da capire come creare un'istanza di una classe che fornisce metodi statici
factory invece di costruttori . Lo strumento Javadoc potrebbe un giorno attirare l'attenzione su metodi
statici factory . Intanto , si può ridurre questo svantaggio , richiamando l'attenzione ai metodi factory statici
in classe o in commenti d’interfaccie , e aderendo alle convenzioni di denominazione comuni .
In sintesi , metodi statici factory e costruttori pubblici entrambe hanno i loro usi , e vale la pena di capire i
loro meriti . Spesso i metodi factory statici sono preferibili, in modo da evitare per riflesso di fornire
costruttori pubblici.
Item 2 - Applicare la proprietà Singleton con un costruttore privato
Un singleton è semplicemente una classe che viene istanziata esattamente una sola volta. Il Singleton in
genere rappresenta le componenti di un sistema che sono intrinsecamente uniche , come window manager
o il file system. L’esecuzione di una classe singleton può rendere difficile testare i propri clients , in quanto è
impossibile sostituire una implementazione finta per un singleton a meno che implementi un'interfaccia
che sia adatta al suo tipo. Ci sono due modi per implementare singleton. Entrambi sono basati sul
mantenimento del costruttore privato e sull'esportazione di un membro statico pubblico, utile per fornire
un accesso alla sola istanza.
Nel primo approccio, il membro è un campo finale :
/ / Singleton con campo finale pubblico
public class Elvis {
public static final Elvis INSTANCE = new Elvis ( ) ;
private Elvis ( ) { ... }
public void leaveTheBuilding ( ) { ... }
}
Il costruttore privato viene chiamato una sola volta , per inizializzare il campo finale public static
Elvis.INSTANCE . La mancanza di un costruttore pubblico o protetto garantisce un ambiente " monoelvistic”:
esattamente un'istanza Elvis esisterà, una sola volta la classe Elvis viene inizializzata - né più, né meno .
Niente di tutto ciò che un clients non può cambiare, con un avvertimento : un clients privilegiato può
richiamare il costruttore privato riflessivamente con l' aiuto del metodo AccessibleObject.setAccessible . Se
si necessità di difendersi da questo attacco, modificare il costruttore per fargli gettare un un'eccezione se è
chiesto di creare una seconda istanza .
Nel secondo approccio dell’attuazione del singleton , il membro pubblico è un metodo factory static :
/ / Singleton con la factory statica
public class Elvis {
private static final Elvis INSTANCE = new Elvis ( ) ;
private Elvis ( ) { ... }
public static Elvis getInstance ( ) {return INSTANCE ; }
public void leaveTheBuilding ( ) { ... }
Tutte le chiamate verso Elvis.getInstance restituiscono lo stesso riferimento all'oggetto , e nessun altra
istanza Elvis sarà mai creata ( con la stessa avvertenza di cui sopra ) .
Il principale vantaggio dell'approccio campo pubblico (numero 1) è che le dichiarazioni fanno si che la
classe è un singleton : il campo statico pubblico è definitivo , quindi sarà sempre limitato allo stesso
riferimento all'oggetto.
Un vantaggio dell'approccio metodo factory è che ti dà la flessibilità di cambiare idea sul fatto che la classe
dovrebbe essere un singleton senza cambiare la sua API . Il metodo factory restituisce l'unica istanza , ma
potrebbe essere facilmente modificato per tornare indietro. Un secondo vantaggio, riguarda i tipi generici.
Spesso nessuno di questi vantaggi è rilevante, e l'approccio campo pubblico è più semplice.
Item 3 - Applicare non-instantiability con un costruttore privato
Di tanto in tanto ti consigliamo di scrivere una classe che è solo un raggruppamento di metodi statici e
campi statici . Tali classi hanno acquisito una cattiva reputazione perché alcune persone abusano di loro per
evitare di pensare in termini di oggetti , ma essi hanno usi corretti. Essi possono essere usati per
raggruppare metodi sui valori primitivi o matrici , in mododi java.lang.Math o java.util.Arrays , possono
anche essere utilizzati per creare un gruppo statico dimetodi, o per gli oggetti che implementano una
particolare interfaccia , in maniera di java.util.Collections . Infine , possono essere utilizzati raggruppare i
metodi su una classe finale , invece di estendere la classe . Tali classi di utilità non sono stati progettate per
essere istanziate : un'istanza sarebbe priva di senso . In assenza di costruttori espliciti , tuttavia , il
compilatore fornisce un costuttore pubblico , senza parametri, chiamato costruttore predefinito . Per un
utente, questo costruttore è indistinguibile da qualsiasi altro . Il tentativo di imporre non-instantiability
facendo una classe astratta non funziona . La classe può essere sottoclassata e la sottoclasse instanziata .
Inoltre, ciò inganna l'utente facendogli credere la classe è stata progettata per l'ereditarietà (item 17). Vi è,
tuttavia , un semplice “tocco di stile” per assicurare non-instantiability . Un costruttore di default viene
generato solo se una classe non contiene esplicitamente costruttori , quindi una classe può essere fatta
non-instantiable includendo un costruttore privato :
/ / Noninstantiable classe di utilità
public class UtilityClass {
/ / Elimina costruttore predefinito per non-instantiability
Private UtilityClass ( ) {
throw new AssertionError ( ) ;
} ... / / Resto omesso }
Poiché il costruttore esplicito è privato , è inaccessibile al di fuori del classe . L’AssertionError non è
strettamente necessario , ma fornisce assicurazione in caso il costruttore viene accidentalmente richiamato
dall'interno della classe . Garantisce che la classe non verrà mai creata un'istanza in nessun caso . Questo
“tocco di classe” è leggermente contro intuitivo, così come il costruttore viene fornito espressamente così
che non può essere invocato . È pertanto consigliabile includere un commento , come mostrato sopra.
Come effetto collaterale, questo trucchetto impedisce anche alla classe di essere sottoclassata. Tutti i
costruttori devono invocare un costruttore della superclasse , esplicitamente o implicitamente , e una
sottoclasse non avrebbe alcun costruttore della superclasse accessibile da richiamare .
Item 4 - Evitare la creazione di oggetti non necessari
Spesso è opportuno riutilizzare un singolo oggetto invece di creare un nuovo oggetto equivalente ogni volta
che è richiesto. Il riutilizzo può essere sia più veloce e più elegante. Un oggetto può sempre essere
riutilizzato se è immutabile (item 15) . Come esempio estremo di cosa non fare, prendiamo in
considerazione questa dichiarazione :
String s = new String ( " stringette " ) ; / / NON FARE QUESTO !
L'istruzione crea una nuova istanza String ogni volta che viene eseguita, e nessuna di quelle creazioni di
oggetti è necessario. L'argomento al costruttore String ( " stringette " ) è di per sé una istanza String ,
funzionalmente identico a tutte le oggetti creati dal costruttore . Se questo utilizzo si verifica in un ciclo o in
un frequente metodo richiamato, milioni di istanze String possono essere create inutilmente. La versione
migliorata è semplicemente il seguente :
String s = " stringette " ;
Questa versione utilizza una singola istanza String , piuttosto che crearne uno nuova ogni volta che viene
eseguita. Inoltre è garantito che l'oggetto sarà riutilizzata da qualsiasi altro codice in esecuzione nella stessa
macchina virtuale che mantenga la stessa stringa letterale. Spesso è possibile evitare di creare oggetti non
necessari utilizzando metodi factory statici (item 1 ), preferendo costruttori di classi immutabili che
forniscono entrambi. Ad esempio, il metodo factory static Boolean.valueOf ( String ) è quasi sempre
preferibile alla booleano di costruzione ( String) . Il costruttore crea un nuovo oggetto ogni volta che viene
chiamato, mentre il metodo factory statico non viene mai richiesto a farlo. Oltre al riutilizzo di oggetti
immutabili, è anche possibile riutilizzare oggetti mutabili se si sa che non saranno modificati. Qui la
situazione è un po’ più sottile , e molto più comune , esempio di cosa non fare . Si tratta di oggetti Date
mutabili che non vengono mai modificati una volta sono stati calcolati i loro valori . Questi modelli di classe
di un persona hanno un metodo isBabyBoomer che dice se la persona è un "babyboomer ", in altre parole ,
se la persona è nata tra il 1946 e il 1964 :
public class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
// DON'T DO THIS!
public boolean isBabyBoomer() {
// Unnecessary allocation of expensive object
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
}
Il metodo isBabyBoomer crea inutilmente un nuovo calendario , fuso orario , e due istanze Data ogni volta
che viene invocato . La versione che segue evita questa inefficienza con un inizializzatore statico :
class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
}
La versione migliorata della classe Person crea calendario , fuso orario , e istanze di data solo una volta ,
quando viene inizializzato , invece di creare loro ogni tempo isBabyBoomer viene richiamato . Ciò si traduce
in significativi miglioramenti delle prestazioni se il metodo viene richiamato frequentemente . Non solo
prestazioni migliorate , ma così lo è anche per la chiarezza . La modifica di boomStart e boomEnd da
variabili locali ai campi static final è chiaro che queste date sono trattati come costanti , rendendo il codice
più comprensibile . Il contrappunto a questo articolo è item 24 sulla copie difensive. In sintesi item 5
riassume: " Non creare un nuovo oggetto quando si deve riutilizzare uno esistente ", mentre Item 39 dice: "
Non riutilizzare un oggetto esistente quando si deve creare uno nuovo “. Nota che la penalità per il riutilizzo
di un oggetto quando la copia difensiva viene chiamata, è di gran lunga superiore aella pena di aver
inutilmente creato un oggetto duplicato . Non riuscendo a fare copie difensive dove necessario possono
portare a bug insidiosi e falle di sicurezza , la creazione di oggetti inutili influisce solo su stile e prestazioni .
Item 18 – Favor (piacere, favore) classi membri statiche sopra non statiche
Una classe nidificata è una classe definita all'interno di un'altra classe. Una classe nidificata dovrebbe
esistere solo per servire la sua classe che la contiene. Se volessi che una classe nidificata fosse utile in
qualche altro contesto, allora dovremmo definire una classe di livello superiore. Ci sono quattro tipi di classi
nidificate:
1) classi membri statiche
2) le classi membri non statiche
3) le classi anonime
4) classi locali
Tutti tranne il primo tipo sono noti come classi interne. Questa voce ti dice quando usare e quale tipo di
classe nidificata e perché. 1) Una classe membro statico è il tipo più semplice di classe nidificata. E’ meglio
pensare a come succede che una classe normale è dichiarata all'interno di un'altra classe e ha l'accesso a
tutti i membri della classe che contiene, anche quelli dichiarati privato. La classe membro statico è un
membro statico della classe che la contiene e obbedisce lo stesso alle regole d’accessibilità come gli altri
membri statici. Se è dichiarata privata, è accessibile solo all'interno della classe contenitrice, e così via.
Sintatticamente, l'unica differenza tra le classi dei membri statici e non statici è che le classi membro
statiche hanno il modificatore static nelle loro dichiarazioni. Nonostante la somiglianza sintattica, questi
due tipi di classi nidificate sono molto diversi. 2) Ogni istanza di una classe membro non statico è
implicitamente associata ad un istanza della sua classe di appartenenza. Se l'istanza di una classe nidificata
può esistere in isolamento da un'istanza della sua classe di inclusione , allora la classe nidificata deve essere
una classe membro statico : è impossibile creare un'istanza di una classe membro non statico senza
un'istanza che la racchiude . L'associazione tra un istanza di classe membro non statico e la sua inclusione
istanza viene stabilita quando il primo viene creato e non può essere modificato successivamente.
Normalmente, l'associazione è stabilita automaticamente richiamando un costruttore non statico della
classe membro all'interno di un metodo dell'istanza classe. Un uso comune di una classe membro non
statico è quello di definire un che permette ad un'istanza della classe esterna di essere vista come un
istanza di una classe indipendente. Per esempio,
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
... // Bulk of the class omitted
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
...
}
Se si dichiara una classe membro che non richiede l'accesso ad una istanza racchiusa, sempre dovrai metter
il modificatore static nella sua dichiarazione, rendendolo un statico piuttosto che una classe membro non
statico . Se si omette questo modificatore, ogni istanza dovrà avere un riferimento estraneo alla sua istanza
inclusa. Un uso comune delle classi membro statiche private è quello di rappresentare i componenti
dell’oggetto rappresentato dalla loro classe di inclusione. 3) Classi anonime sono differenti a qualsiasi altra
cosa nel linguaggio di programmazione Java. Come ci si aspetterebbe, una classe anonima non ha nome.
Non è un membro della sua classe contenitrice . Piuttosto deve essere dichiarata insieme ad altri membri ,
è contemporaneamente dichiarata e istanziata al punto di utilizzo . Le classi anonime sono ammesse in
qualsiasi punto del codice in cui l'espressione è legale. Le classi anonime hanno istanze incluse se e solo se
si verificano in un contesto non statico. Ma se si verificano in un contesto statico, non possono avere
membri statici. Ci sono molte limitazioni all'applicabilità classi anonime. Non si possono istanziare tranne
nel punto che stanno dichiarate. Non è possibile eseguire test instanceof o fare qualsiasi altra cosa che
richiede di denominare la classe. Non è possibile dichiarare una classe anonima per implementare
interfacce multiple , o per estendere una classe e implementare un'interfaccia allo stesso tempo . I clients
di una classe anonima non possono invocare nessun membro ad eccezione di quelli da cui si eredita il suo
supertipo. Un uso comune delle classi anonime è quello di creare oggetti funzionali (item 21 ) al volo. 4) Le
classi locali sono le meno utilizzate di frequente delle quattro tipi di classi nidificate. La classe locale può
essere dichiarata ovunque, una variabile locale può essere dichiarata e obbedisce al stesse regole. Classi
locali hanno attributi in comune con ciascuno degli altri tipi di classi nidificate. Come classi membro, hanno
nomi e possono essere utilizzate ripetutamente. Come classi anonime, racchiudono istanze solo se sono
definite in un contesto non statico, e non possono contenere membri statici ed inoltre come classi
anonime, dovrebbero essere brevi in modo da non danneggiare la leggibilità. Per ricapitolare, ci sono
quattro diversi tipi di classi nidificate, e ciascuno ha la sua particolarità. Se una classe nidificata deve essere
visibile dall'esterno di un singolo metodo o è troppo lunga per adattarsi all'interno di un metodo, utilizzare
una classe membro. Se ogni istanza della classe membro ha bisogno di un riferimento alla sua istanza
inclusa, la rendo non statica, altrimenti la rendo statica. Supponendo che la classe appartiene all'interno di
un metodo, se è necessario creare istanze di una sola posizione e c'è un tipo preesistente che caratterizza la
classe allora la rendo una classe anonima, in caso contrario faccio un classe locale.
Item 22 - Sostituire i puntatori a funzione con le classi e interfacce (non è presente nel libro solo slide)
C supporta i puntatori a funzione, tipicamente utilizzato per consentire al chiamante di una funzione di
specializzarsi nel suo comportamento passando un puntatore ad una seconda funzione, talvolta indicata
come un callback. Ad esempio l'operazione va ripetuta nella visita di un elenco, il comparatore passato a
quick sort. Tutto ciò risulta una sorta di modello di strategia. Java raggiunge la stessa funzionalità
utilizzando il linguaggio degli oggetti funzionali. Oggetti i cui metodi eseguono operazioni su altri oggetti,
passati esplicitamente
class StringLengthComparator {
public int compare(String s1, String s2) {return
s1.length() - s2.length();}
La classe StringLengthComparator è stateless. Può essere utile un singleton per risparmiare sui costi di
creazione di oggetti inutili (Item 2,4)
class StringLengthComparator {
private StringLengthComparator() { } // private
constructor
public static final StringLengthComparator INSTANCE = //
static final factory
new StringLengthComparator();
Tutto questo può essere utilizzato in un modello di strategia vera e propria. Definisci un’interfaccia( la
strategia del Abstract)
public interface Comparator {public int compare(Object o1,
Object o2); }
Sia StringLengthComparator una possibile implementazione del comparatore (uno dei tanti possibili
ConcreteStrategy)
class StringLengthComparator implements Comparator{
private StringLengthComparator() { } // private
constructor
public static final StringLengthComparator INSTANCE = //
static final factory
new StringLengthComparator();
public int compare(String s1, String s2) {return
s1.length() - s2.length();}
Le classi Concrete strategy sono spesso dichiarate utilizzando le classi anonime (Item 18)
// a statement invokes Arrays.sort passing the
reference to an array of String
// and the constructor of an implementation of
Comparator
// that is defined as a nested local anonymous class
// that defines the implementation for the
Comparator.compare method
Arrays.sort(stringArray, new Comparator() {
// anonymous class local to the constructor invokation
// sets up the concrete strategy for comparison
public int compare(Object o1, Object o2) {
String s1 = (String)o1;
String s2 = (String)o2;
return s1.length() -
s2.length();}
}); // end of the local anonymous class, and of the
Capitolo 7 – Metodi in questo capitolo vengono illustrati diversi aspetti del metodo di progettazione: come
trattare parametri e valori di ritorno, come progettare le firme dei metodi , e come fornire la
documentazione dei metodi. Come gli altri capitoli, questo capitolo si concentra sulla facilità d'uso,
robustezza, e flessibilità.
Item 23 - Parametri di controllo per la validità
La maggior parte dei metodi e costruttori hanno alcune restrizioni su valori i quali possono essere passati
nei loro parametri. Ad esempio, non è raro che i valori di indice deve essere non negativo e riferimenti a
oggetti devono essere non nullo . Si dovrebbe chiaramente documentare tutte le restrizioni e farle
rispettare con controlli all'inizio del il corpo del metodo. Questo è un caso particolare del principio generale
che si dovrebbe tentare di rilevare gli errori il più veloce dopo che si verifichino. Se un valore di un
parametro non valido viene passato ad un metodo e il metodo verifica i suoi parametri prima
dell'esecuzione, fallirà e creerà un appropriata eccezione. Se il metodo non riesce a controllare i suoi
parametri, possono accadere molte cose. Il metodo potrebbe fallire con un'eccezione errata, peggio
ancora, il metodo potrebbe funzionare normalmente, ma calcolare il risultato sbagliato. Peggio di tutto , il
metodo potrebbe funzionare normalmente ma lasciare qualche oggetto in uno stato compromesso,
causando un errori.
Per i metodi pubblici , utilizzare il Javadoc @throw tag per documentare l'eccezione che sarà generata se
una restrizione sui valori dei parametri viene violata tipicamente l’eccezione sarà IllegalArgumentException
e/o IndexOutOfBounds. Un esempio tipico :
/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
*
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("Modulus <= 0: " + m);
... // Do the computation
Per i metodi pubblici verificare la validità è necessario fino al chiamato. Per il package privato, potrebbe
anche essere una responsabilità del chiamante.
E ' particolarmente importante verificare la validità dei parametri che non sono utilizzati da un metodo, ma
sono conservati per un uso successivo . Ad esempio, si consideri il metodo factory statico, che prende un
array int e restituisce un elenco vista della matrice. Se un client di questo metodo dovesse passare nullo, il
metodo getterebbe una NullPointerException,. A quel punto, l'origine dell'istanza LIst potrebbe essere
difficile da determinare, potrebbe complicare notevolmente il compito di debugging . Costruttori
rappresentano un caso particolare del principio che si dovrebbe utilizzare per verificare la validità dei
parametri che devono essere riutilizzati per un uso successivo. È critico verificare la validità dei parametri
del costruttore per impedire la costruzione di un oggetto che viola le sue invarianti di classe . Ci sono
eccezioni alla regola che si dovrebbe verificare i parametri di un metodo prima di eseguire il suo calcolo .
Un'importante eccezione è il caso in cui il controllo di validità sarebbe costoso e poco pratico e viene
eseguito il controllo di validità implicitamente nel processo da fare per il calcolo. Ad esempio, si consideri
un metodo che ordina un elenco di oggetti, come Collections.sort ( List ) . Tutte gli oggetti nella lista devono
essere reciprocamente comparabili. Nel processo di ordinamento dell'elenco, ogni oggetto nella lista sarà
confrontato con qualche altro oggetto nella lista. Se l'oggetti non sono simili tra loro, uno di questi
confronti getterà una Class- CastException , che è esattamente ciò che il metodo di ordinamento dovrebbe
fare pertanto sarebbe inutile controllare in anticipo che gli elementi nella lista erano comparabili tra di loro.
Per riassumere, ogni volta che si scrive un metodo o un costruttore , si dovrebbe pensare in merito a ciò
che esiste sulle restrizioni dei suoi parametri . Si dovrebbe documentare queste restrizioni e ad applicarle
con controlli espliciti all'inizio del corpo del metodo. E' importante prendere l'abitudine di fare questo. Il
semplice lavoro che essa comporta sarà ripagato la prima volta che un controllo di validità non riesce.
Item 24 - Creare copie difensive in caso di necessità
Una cosa che rende Java piacevole da usare è che si tratta di un linguaggio sicuro. Questo significa che, in
assenza di metodi nativi è immune da sovraccarichi del buffer , superamenti di matrice, puntatori selvatici,
e altri errori di corruzione della memoria che affliggono pericolosamente linguaggi come C e C + + . In un
linguaggio sicuro, è possibile scrivere classi e sapere con certezza che i loro invarianti rimarranno veri, non
importa cosa accade in qualsiasi altra parte del sistema. Anche in un linguaggio sicuro non sei isolato dalle
altre classi senza che tu non faccia alcuna accortezza. È necessario programmare difensivamente, con il
presupposto che i clients della classe faranno del loro meglio per distruggere i tuoi invarianti. Questo può
effettivamente essere vero se qualcuno cerca di rompere la sicurezza del sistema, ma più probabilmente
l’implementazione delle nostre classi dovranno fare i conti con un comportamento imprevisto derivante da
errori onesti da parte dei programmatori che utilizzano la nostra API. In entrambi i casi, vale la pena di
prendere il tempo di scrivere classi che siano robuste per far fronte ai client mal educati :-)
A prima vista, questa classe può sembrare immutabile e fa rispettare l’invariante. Si tratta, tuttavia, di
violare facilmente questa invariante sfruttando il fatto che la Date è mutevole:
// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
Per proteggere l'interno di un'istanza Period da questo tipo di attacco, è essenziale fare una copia difensiva
di ciascun parametro mutevole al costruttore e utilizzare le copie come componenti dell'istanza Period al
posto del originali:
// Repaired constructor - makes defensive copies of parameters
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start +" after "+ end);
Con il nuovo costrutto, il precedente attacco non avrà alcun effetto sull'istanza Period. Si noti che le copie
difensive sono fatte prima di controllare la validità dei parametri e il controllo di validità viene eseguito
sulle copie piuttosto che sugli originali. Mentre questo può sembrare innaturale, è necessario. Protegge la
classe contro modifiche ai parametri da parte di un altro thread durante la "finestra di vulnerabilità",
periodo nel quale i parametri sono controllati e il momento in cui vengono copiati. Si noti, inoltre, che non
abbiamo usato il metodo clone di Date per fare le copie difensive, perché Date è non-final, il metodo clone
non è garantito per restituire un oggetto la cui classe è java.util.Date: potrebbe restituire una sottoclasse
non attendibile specificamente progettata per furberie. Tale sottoclasse potrebbe, per esempio, registrare
un riferimento per ogni istanza in un elenco statico privato al momento della sua creazione e consentire al
malintenzionato di accedere a questa lista. Ciò darebbe all'attaccante libero utilizzo su tutte le istanze. Per
evitare questo tipo di attacco, non utilizzare il clone metodo per fare una copia di difesa di un parametro il
cui tipo è sottoclassato da parte di soggetti non attendibili.
// Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
Per difendersi contro il secondo attacco, basta semplicemente modificare le funzioni di accesso per tornare
ad copie difensive di campi interni modificabili:
// Repaired accessors - make defensive copies of internal fields
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
Con il nuovo costruttore e le nuove funzioni di accesso, Period è veramente immutabile. Non importa
quanto dannoso o incompetente sia un programmatore, non vi è semplicemente alcun modo di violare
l'invariante. Questo è vero perché non c'è alcun modo per qualsiasi classe diversa da Period di guadagnare
l'accesso a uno dei campi modificabili in un'istanza Period. Questi campi sono veramente incapsulati
all'interno dell'oggetto. Le copie difensive dei parametri non sono solo per le classi immutabili. Le copie
difensive possono avere una riduzione delle prestazioni ad esse associate e non è sempre giustificato. Se
una classe si fida di suo chiamante che non modifica un componente interno, allora può essere opportuno
scartare le copie difensive. In sintesi, se una classe ha componenti mutevoli che si ottengono da o di
ritorno da i suoi client, la classe deve copiare difensivamente questi componenti. Se il costo del copia fosse
proibitivo e la classe si fida dei suoi client, che non modificano le componenti impropriamente, la copia
difensiva può essere sostituita da documentazione.
Item 5,12,13,14,15,16 + introil valore tra parentesi accanto al n° dell'item indica il n° corrispondente sul libro.
• Introduzione
Gli idiomi (di cui fanno parte gli “Item” che andremo a studiare) sono regole che contengono pratiche generalmente usate dai programmatori. Sono soluzioni ricorrentiper comuni problemi di programmazione. Mentre i Design Pattern sono di alto livelloe indipendenti dal linguaggio, gli idiomi sono pattern di basso livello per linguaggi specifici. Durante il design si usano Pattern, durante l'implementazione si usano gli idiomi poiché forniscono una più specifica soluzione.
• Item 5 (6): Eliminare riferimenti obsoleti a oggetti
Ci sono puntatori (tipo quando faccio POP nello Stack) che rimangono in vita anche se non li posso più usare. In casi come questo è utile (e a volte necessario) forzare l'utilizzo del Garbage Collector per evitare spreco di memoria. Le perdite di memoria in lingue dotate di Garbage Collector (più propriamente note come trattenute non intenzionali di oggetti) sono insidiose. Se un riferimento a un oggetto viene inavvertitamente mantenuto, non solo è escluso dalla raccolta dei rifiuti quell'oggetto,ma lo sono anche tutti gli oggetti a cui fa riferimento tale oggetto, e così via. Anche se solo pochi riferimenti agli oggetti vengono involontariamente conservati, a molti oggetti può essere impedito di essere ripuliti dal GC, con potenziali grandi effetti sulle prestazioni. La soluzione è semplice e si basa sull'evitare la ritenzione involontaria di un oggetto annullando riferimenti fuori uso: cioè quando so che non dovrò più usare un oggetto pongo il suo riferimento a Null. Quando i programmatori vengono a contatto per la prima volta con questo problema, possono compensare annullando ogni riferimento a un oggetto non appena il programma ha finito di usarlo. Questo non è né necessario né auspicabile, in quanto ingombra il programma inutilmente! Questa pratica, infatti, dovrebbe essere l'eccezione e non la norma, essa risulta necessaria in 3 casi:
1. ogni volta che una classe gestisce la propria memoria; 2. nella gestione della cache, una volta messo un riferimento a un oggetto in una
cache, è facile dimenticare che è lì e lasciarlo nella cache molto tempo dopo l'essere diventato irrilevante;
3. quando si ha a che fare con ascoltatori e callback,se si implementa una API in cui i clienti registrano le callback, ma non annullare la registrazione esplicitamente, si accumuleranno a meno che non si prende qualche precauzione: il modo migliore per garantire che i callback siano rifiuti raccolti prontamente è quello di memorizzarli solo tramite riferimenti deboli (weak pointer).
L'esempio dello Stack sopra citato rientra nel primo caso. Poiché le perdite di memoria tipicamente non manifestano fallimenti evidenti, possono rimanere presenti in un sistema per anni. Esse sono tipicamente scoperte solo
a seguito di un'attenta ispezione del codice o con l'ausilio di uno strumento di debug noto come heap profiler. Pertanto, è molto desiderabile l'imparare ad anticipare i problemi di questo tipo prima che si verifichino e impedire che accada.
• Item 12(13): Ridurre al minimo l'accessibilità delle classi e dei membri
Un modulo ben progettato nasconde tutti i suoi dettagli implementativi, separando nettamente la sua API dalla sua implementazione. Questo concetto, noto come Information hiding o incapsulamento, serve per produrre disaccoppiamento:
1. Facilita lo sviluppo in parallelo e la manutenzione, poiché rende il codice più facile da leggere. Inoltre permette di aumentare le performance potendo lavorare sui moduli che creano problemi senza intaccare la correttezza degli altri;
2. Rende il SW più riutilizzabile: non essendo i moduli molto legati a ciò che li circonda si possono rivelare utili anche in altre circostanze;
3. Mitiga il rischio di costruire grandi sistemi (singoli moduli possono rivelarsi di successo, anche se il sistema per cui sono stati costruiti non lo è).
La regola è semplice: rendere ogni classe o membro il più inaccessibile possibile. In altre parole, utilizzare il livello di accesso più basso possibile coerente con il corretto funzionamento del software che si sta scrivendo. Il controllo degli accessi è un importante strumento per l'information hiding. Per il top-level di classi e interfacce, ci sono solo due possibili livelli di accesso: package-private o public.
1. Package-private: si rendono parte della realizzazione e si può modificare, sostituire o eliminare in una versione successiva senza timore di danneggiare i client esistenti.
2. Public: si è obbligati a sostenerlo per sempre per mantenere la compatibilità.Per i membri (campi, metodi, classi annidate e interfacce annidate) ci sono quattro possibili livelli di accesso:
1. Privato: Il membro è accessibile solo dalla classe di primo livello in cui è dichiarata.
2. Pacchetto-privato: Il membro è accessibile da qualsiasi classe nel pacchetto in cui è dichiarata. Tecnicamente noto come accesso predefinito, questo è il livello di accesso che si ottiene se non viene specificato alcun modificatore di accesso.
3. Protetto: Il membro è accessibile da sottoclassi della classe in cui è dichiarata eda ogni classe del pacchetto in cui è dichiarata.
4. Pubblico: Il membro è accessibile da qualsiasi luogo
Se un metodo esegue l'override di un metodo della super-classe, non gli è permesso diavere un livello di accesso più basso nella sottoclasse di quanto non faccia nella super-classe. Ciò è necessario per garantire che un'istanza della sottoclasse è utilizzabile ovunque sia utilizzabile un'istanza della superclasse. Se si violano questa
regola il compilatore genera un messaggio di errore quando si tenta di compilare la sottoclasse. Nessuna variabile dovrebbe mai essere pubblica: si usano i metodi getter e setter per accedervi in modo controllato. Classi con campi modificabili pubblici nonsono thread-safe. Anche se un campo è final e si riferisce ad un oggetto immutabile, rendendo il campo pubblico si dà la flessibilità per passare ad una nuova rappresentazione interna dei dati in cui il campo non esiste. → Per riassumere, si dovrebbe sempre ridurre l'accessibilità, per quanto possibile. Dopo aver progettato con cura una API pubblica minimale, si dovrebbe evitare a ogniclasse, interfaccia o membro di diventare una parte della API. Con l'eccezione dei campi finali statici pubblici, le classi pubbliche non dovrebbero avere campi pubblici.Assicurarsi che gli oggetti referenziati da campi finali statici pubblici siano immutabili.
• Item 13(15): Favorire immutabilità (minimizzare mutabilità)
Una classe immutabile è semplicemente una classe le cui istanze non possono essere modificate. Ci sono molte buone ragioni per questo: le classi immutabili sono più facili da progettare, implementare e usare di classi mutevoli. Esse sono meno inclini aerrori e sono più sicure. Inoltre gli oggetti immutabili possono essere condivisi liberamente in quanto non richiedono la sincronizzazione. Gli oggetti dovrebbero essere fatti il più possibile immutabili. Oggetti immutabili una volta istanziati non cambiano. Per fare una classe immutabile vanno seguite 5 regole:
1. non esporre metodi che modifichino lo stato dell'oggetto, questo impedisce a sottoclassi imprudenti o malintenzionate di compromettere il comportamento immutabile della classe comportandosi come se lo stato dell'oggetto fosse cambiato;
2. assicurarsi che la classe non possa essere estesa;3. porre tutti i campi final, ciò esprime chiaramente l'intenzione in un modo che
viene applicato dal sistema;4. porre tutti i campi private, questo impedisce ai client di ottenere l'accesso agli
oggetti mutabili riferiti dai campi e di modificare direttamente questi oggetti;5. assicurarsi accesso esclusivo ad ogni componente mutabile.
L'approccio funzionale potrebbe apparire innaturale se non si ha familiarità con esso, ma consente l'immutabilità, che ha molti vantaggi. Oggetti immutabili sono semplici. Un oggetto immutabile può trovarsi in un solo stato, lo stato in cui è stato creato. Se si è sicuri che tutti i costruttori stabiliscono invarianti di classe, allora è garantito che questi invarianti rimarranno veri per tutto il tempo, senza ulteriore sforzo da parte vostra o da parte del programmatore che utilizza la classe. Oggetti mutabili, d'altro canto, possono contenere stati arbitrariamente complessi. Se la documentazione non fornisce una descrizione precisa delle transizioni di stato effettuate con metodi mutatori, può essere difficile o impossibile utilizzare una classe mutevole affidabile.Una classe immutabile può essere rilassata permettendo mutevole un campo che non influenza il comportamento esterno. L'unico vero svantaggio di classi immutabili è
che richiedono un oggetto separato per ogni valore distinto. Se una classe non può essere fatta immutabile, bisogna limitare la sua mutabilità il più possibile. Pertanto, fare ogni campo final a meno che non vi sia un motivo valido per farlo non-final.
• Item 14(16): Preferire la composizione all'eredità
Implementare l'ereditarietà è un buon modo per rendere un codice riutilizzabile. Infatti, oltre ad andare a braccetto con l'Open-Close Principle, l'ereditarietà è sicura all'interno dello stesso package e quando si eredità classi appositamente studiate per essere estese. Però l'ereditarietà può creare non pochi problemi, in particolare si può avere:
1. violazione dell'incapsulamento, una sottoclasse dipende dai dettagli implementativi della superclasse per il suo corretto funzionamento;
2. è pericoloso ereditare da classi concrete al di fuori del proprio package;3. classe base fragile;4. internal not documented detail: classe base e derivata possono essere fatte
bene, ma non specificando alcuni dettagli si possono creare conflitti problematici.
L'eredità si porta dietro delle rigidità. Infatti una classe base, per acquisire nuovi metodi nelle successive versioni deve soddisfare più requisiti: se la classe base cambia devono poterlo fare anche i suoi legami e una sottoclasse deve evolvere di pari passo con la sua super-classe. Per evitare alcuni problemi si potrebbe pensare chesia sicuro estendere una classe se si limita l'aggiunta di nuovi metodi e ci si astiene dal fare override di quelli esistenti. Questo tipo di estensione è molto più sicuro, non è priva di rischi. Per fortuna c'è un modo per evitare tutti i problemi visti: invece di estendere una classe esistente si può dare alla nuova classe un campo privato che fa riferimento a un'istanza della classe esistente. Questa tecnica è chiamata composizione poiché la classe esistente diventa una componente di quella nuova. Ogni metodo istanza nella nuova classe richiama il metodo corrispondente sulla istanza contenuta della classe esistente e restituisce i risultati. Questo è noto come forwarding, e i metodi della nuova classe sono noti come metodi di inoltro. La classe risultante sarà solida, senza dipendenze sui dettagli di implementazione della classe esistente. Anche l'aggiunta di nuovi metodi per la classe esistente non avrà alcun impatto sulla nuova classe. Un metodo pratico per i9mplementare la composizione è creare una classe wrapper: la nuova classe incapsula quella esistente.Anche questi ultimi metodi presentano piccoli problemi, detti Self problem: l'oggetto avvolto non conosce del suo involucro, in uno schema di callback esso passa un riferimento a se stesso, eludendo il wrapper; Non adatto per l'uso in callback framework, in cui gli oggetti passano riferimenti a se stessi ad altri oggetti per invocazioni successive (framework: codice già scritto che lascia dei punti di estensione i quali saranno poi definiti nel caso concreto. Può essere visto come complementare alle librerie, si ha un meccanismo di inversione del controllo). Inoltre si ha un leggero impatto sulle prestazioni nell'uso di forwarding e wrapper, di solito non è un problema. È un po' noioso per scrivere i metodi di forwarding, parzialmente
compensato dal fatto che si deve scrivere solo un costruttore.L'ereditarietà è appropriata solo quando la sottoclasse è davvero un sottotipo sella superclasse! Se si usa ereditarietà dove sarebbe appropriata la composizione si espone inutilmente dettagli di implementazione.→ Per riassumere, l'ereditarietà è potente, ma è problematico perché viola l'incapsulamento. È opportuno solo quando esiste un vero e proprio rapporto di sottotipo tra la sottoclasse e superclasse. Anche allora, l'ereditarietà può portare a fragilità se la sottoclasse è in un pacchetto differente dalla superclasse e la superclasse non è progettato per ereditarietà. Per evitare questa fragilità, utilizzare la composizione e forwarding invece di eredità, soprattutto se esiste un'interfaccia appropriata per attuare una classe wrapper. Non solo sono classi wrapper più robuste di sottoclassi, ma sono anche più potenti.
N.B. Il problema discusso in questo item non si applica alla estensione di interfacce.
• Item 15(17): Progettazione e documenti per eredità o altrimenti vietarla
Per fare ereditarietà è necessario fare le classi base robuste e documentare come fare ad ereditarle. Ogni classe deve documentare con precisione gli effetti dell'override su qualsiasi metodo:
1. documentare il self-use di metodi sottoponibili a override: quali metodi sovra scrivibili (non finali) si invoca, in quale sequenza, e come i risultati di ogni invocazione influenzano la successiva lavorazione.
2. documentare di eventuali circostanze in cui potrebbe invocare un metodo sottoponibile a override (ad esempio, le invocazioni di thread in background o inizializzatori statici).
Per convenzione un metodo che che ne invoca un altro sovra scrivibile contiene una descrizione di questa invocazione alla fine del suo commento documentativo. La descrizione di un metodo che richiama metodi sovra scrivibili inizia con "Questa implementazione" (“This implementation”), in modo da sottolineare che il comportamento documentato può risentire di override.
Esempio: in java.util.AbstractCollection, per public boolean remove(Object o)
Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that (o==null ? e==null : o.equals(e)), if thecollection contains one or more such elements. Returns true if the collection contained the specifiedelement (or equivalently, if the collection changed as a result of the call).This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator's remove method. Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection's iterator method does not implement the remove method
Non lascia alcun dubbio che l'override del metodo iteratore influenzerà il comportamento di rimozione, e descrive esattamente come il comportamento dell'Iterator restituito dal metodo iteratore influenzerà il comportamento del metodo
remove. Si noti che, poiché l'incapsulamento viene violato, la documentazione viola ilprincipio secondo il quale una buona la documentazione API descrive quello che un determinato metodo fa e non come lo fa. Per consentire ai programmatori di scrivere sottoclassi efficienti senza eccessivi problemi, una classe può fornire ganci al suo funzionamento interno, sotto forma di metodi protetti giudiziosamente scelti o, in raricasi, di campi protetti. Quali metodi devono essere esposti come protetti? Ogni metodo protetto rappresenta un impegno a un dettaglio implementativo. Ma, un metodo protetto mancante può rendere una classe inutilizzabile per l'eredità. Non si hanno “proiettili magici” (soluzioni perfette). L'unico modo per testare una classe progettata per l'ereditarietà è scrivere sottoclassi. Se si omette un membro protetto cruciale, cercaredi scrivere una sottoclasse renderà l'omissione ovvia. Al contrario, se diverse sottoclassi sono scritte e nessuna utilizza un membro protetto, probabilmente si dovrebbe renderlo privato. L'esperienza dimostra che tre sottoclassi sono in genere sufficienti per testare una classe estensibile. Una o più di queste sottoclassi dovrebbe essere scritta da una persona diversa dall'autore superclasse. È necessario testare la propria classe scrivendo sottoclassi prima di rilasciarla.Ci sono un paio di restrizioni a cui una classe deve obbedire per consentire l'ereditarietà. I costruttori non devono invocare metodi sottoponibili a override, direttamente o indirettamente: Il costruttore della superclasse viene eseguito prima del costruttore della sottoclasse; il metodo prevalente sarà invocato prima che il costruttore della sottoclasse sia eseguito, se il metodo sovrascritto dipende da una qualsiasi inizializzazione eseguita dal costruttore della sottoclasse, allora il metodo non si comporterà come previsto.
Esempio:public class Super {
public Super() {m(); }
public void m() {}
}
final class Sub extends Super {
private final Date date; // Blank final, set by
constructor
Sub() {date = new Date();}
public void m() {System.out.println(date);}
public static void main(String[] args) {Sub s =
new Sub(); s.m(); }
}
il metodo "m" viene richiamato dal costruttore "Super()" prima che il costruttore "Sub()" sia eseguito.La progettazione di una classe per eredità pone dei limiti sostanziali sulla classe. Questa non è una decisione da prendere con leggerezza. Ci sono alcune situazioni in
cui è chiaramente la cosa giusta da fare, come ad esempio le classi astratte, incluse le implementazioni di interfacce (item 18). Ci sono altre situazioni in cui è chiaramente la cosa sbagliata da fare, come le classi immutabili (item 15). La soluzione migliore aquesto problema è di vietare sottoclassi in classi che non sono state progettate e documentate per essere sottoclassate in modo sicuro. Si puòà fare in 2 modi: dichiarare la classe finale oppure fare tutti i costruttori private o package-private e aggiungere static factories pubblicche.Se una classe concreta non implementa un'interfaccia standard, allora si può creare disagi ad alcuni programmatori vietando eredità. Se ritieni che è necessario consentire l'ereditarietà da una classe, un approccio ragionevole è quello di garantire che la classe non richiama uno dei suoi metodi sottoponibili a override e per documentare questo fatto. In altre parole, eliminare il self-use della classe di metodi sottoponibili a override completamente. In tal modo, si creerà una classe che è ragionevolmente sicura nei confronti della sottoclasse. Sovrascrivere un metodo non potrà mai influenzare il comportamento di qualsiasi altro metodo. È possibile eliminare il self-use di una classe di metodi sottoponibili a override meccanicamente, senza modificare il suo comportamento. Spostare il corpo di ciascun metodo sottoponibile a override ad un "metodo di supporto" privato e dfar sì che ogni metodo sottoponibile ad override richiama il suo metodo di supporto privato. Poi sostituire ogni self-use di un metodo sottoponibile a override con una invocazione diretta del metodo di supporto privato del metodo sottoponibile a override.
• Item 16(18): Preferire interfacce alle classi astratte
Interfacce e classi astratte sono molto simili. La differenza più evidente tra i due meccanismi è che le classi astratte sono autorizzati a contenere le implementazioni di alcuni metodi, mentre le interfacce no. In linea di principio si preferisce le interfacce:
1. Le classi astratte sono autorizzate a contenere le implementazioni di alcuni metodi, ma, per implementare una classe astratta, una classe deve ereditare da essa (e Java permette solo l'ereditarietà singola). Mentre una classe che implementa l'interfaccia può essere ovunque nella gerarchia delle classi.
2. Classi esistenti possono essere facilmente adattate a implementare una nuova interfaccia mentre non possono, in generale, essere adattate per estendere una nuova classe astratta. (Se due classi devono implementare la stessa classe astratta, questo dovrebbe essere collocato in alto nella gerarchia dei tipi in modo da essere un antenato comune e tutti i discendenti intermedi dovrebbero implementare la nuova classe astratta pure).
3. Le interfacce sono ideali per fare mixin: un mixin è un tipo che una classe può implementare in aggiunta alla sua "tipo primario" per dichiarare che fornisce un certo comportamento facoltativo. Ad esempio Comparable è un'interfaccia mixin che consente a una classe di dichiarare che le sue istanze sono ordinate rispetto ad altri oggetti tra loro comparabili. Le classi astratte non possono essere utilizzati per definire mixins a causa dell'ereditarietà singola.
4. Le interfacce permettono la costruzione di strutture di tipo non gerarchiche, molti concetti non rientrano ordinatamente in una gerarchia rigida.
5. Le interfacce consentono wrapper class idiom (item 14) per migliorare le funzionalità. Utilizzando le classi astratte per definire i tipi, il programmatore non può scegliere la composizione invece che l'eredità.
È possibile combinare le virtù di interfacce e classi astratte, fornendo lo scheletro implementativo di una classe stratta per le interfacce esportate: superando così l'inconveniente delle interfacce, che non possono fornire un'implementazione parziale. Per convenzione, chiamare AbstractInterface lo scheletro implementativo delle interfacce (ad esempio AbstractCollection, AbstractSet, AbstractList …).Tecnica per simulare l'ereditarietà multipla: la classe che implementa l'interfaccia inoltra invocazioni di metodi di interfaccia a un'istanza contenuta di una classe interna privata che estende lo scheletro implementativo. Strettamente legato al wrapper class idiom, fornisce la maggior parte dei benefici di ereditarietà multipla, evitando le insidie.Utilizzo di classi astratte per definire i tipi che consentono molteplici implementazioni ha un grande vantaggio rispetto all'utilizzo di interfacce: è molto piùfacile far evolvere una classe astratta di un'interfaccia. Se, in una versione successiva,si desidera aggiungere un nuovo metodo di una classe astratta, è sempre possibile aggiungere un metodo concreto contenente un'implementazione predefinita ragionevole. Tutte le implementazioni esistenti della classe astratta forniranno quindi il nuovo metodo. Questo non funziona per le interfacce: se viene aggiunto un nuovo metodo di un'interfaccia, tutte le implementazioni devono essere estese. Mentre se viene aggiunto a una classe astratta, può essere fornita qualche implementazione di default. In tal modo, le classi astratte sono più flessibili delle interfacce che devono essere progettate con cura in modo da non avere bisogno di riaprirle. Infatti una volta rilasciato ed implementato un interfaccia è quasi impossibile cambiarla.→ Per riassumere, l'interfaccia è generalmente il modo migliore per definire un tipo che consente più implementazioni. Un'eccezione a questa regola è il caso in cui la facilità di evoluzione è ritenuta più importante di flessibilità e potenza. In queste circostanze, è necessario utilizzare una classe astratta per definire il tipo, ma solo se si capisce e si può accettare le limitazioni. Se si esporta un interfaccia non banale, si dovrebbe prendere in seria considerazione il fornire una implementazione scheletrica con essa. Infine, è necessario progettare tutte le interfacce pubbliche con la massima cura e testare a fondo scrivendo implementazioni multiple.
Abstract Factory (C)
Intento: Presenta un’interfaccia per la creazione di famiglie di prodotti, in modo tale che il cliente che gli
utilizza non abbia conoscenza delle loro concrete classi. Questo consente:
- Assicurarsi che il cliente crei soltanto prodotti vincolati fra di loro.
- L’utilizzo di diverse famiglie di prodotti da parte dello stesso cliente.
Motivazione: esempio widget factory che produce diversi tipi di widget, che sono composti da
window,scroolbar ecc. il client usa solo astrazioni.
Applicabilità: Il pattern “Abstract Factory” si basa sulla creazione di interfacce per ogni tipo di prodotto. Ci
saranno poi concreti prodotti che implementano queste interfacce, stesse che consentiranno ai clienti di
fare uso dei prodotti. Le famiglie di prodotti saranno create da un oggetto noto come factory. Ogni famiglia
avrà una particolare factory che sarà utilizzata dal Cliente per creare le istanze dei prodotti. Siccome non si
vuole legare al Cliente un tipo specifico di factory da utilizzare, le factory implementeranno una interfaccia
comune che sarà dalla conoscenza del Cliente.
Struttura:
Partecipanti:
AbstractFactory: Dichiara una interfaccia per le operazioni che creano e restituiscono i prodotti. Nella
dichiarazione di ogni metodo, i prodotti restituiti sono dei tipi AbstractProduct.
ConcreteFactory: Implementa l’AbstractFactory, fornendo le operazioni che creano e restituiscono oggetti
corrispondenti a prodotti specifici (ConcreteProduct).
AbstractProduct: Dichiarano le operazioni che caratterizzano i diversi tipi generici di prodotti.
ConcreteProduct: Definiscono i prodotti creati da ogni ConcreteFactory.
Client: Utilizza l’AbstractFactory per rivolgersi alla ConcreteFactory di una famiglia di prodotti. Utilizza i prodotti
tramite la loro interfaccia AbstractProduct.
Conseguenze: isola le classi concrete. Abbiamo che l’abstract factory è senza stato. Facile cambiare il tipo
famiglia oggetto creato, basta cambiare il concrete factory usato. +) abbiamo consistenza tra i prodotti da
usare insieme, gli raggruppiamo in famiglie. - ) Ogni nuovo tipo di prodotto implica di rifare tutte le factory
Osservazioni: Dovuto al fatto che né l’AbstractFactory né gli AbstractProduct implementano operazioni, in
Java diventa più adeguato codificarli come interfacce piuttosto che come classi astratte.
Builder (C)
Intento: Separa la costruzione di un oggetto complesso dalla sua rappresentazione, in modo che lo stesso
processo di costruzione consenta la creazione di diverse rappresentazioni.
Motivazione: Si tratta di un pattern creazionale basato su oggetti e viene utilizzato per creare un oggetto
senza doverne conoscere i suoi dettagli implementativi. Questo pattern consente di utilizzare un Client che
non debba essere a conoscenza dei passi necessari al fine della creazione di un oggetto ma tali passaggi
vengono delegati ad un Director che sa cosa e come fare.
Applicabilità: Il “Builder” pattern propone di separare la “logica del processo di costruzione” dalla
“costruzione stessa”. Per fare ciò si utilizza un oggetto Director, che determina la logica di costruzione del
prodotto, e che invia le istruzioni necessarie ad un oggetto Builder, incaricato della sua realizzazione.
Siccome i prodotti da realizzare sono di diversa natura, ci saranno Builder particolari per ogni tipo di
prodotto, ma soltanto un unico Director, che nel processo di costruzione invocherà i metodi del Builder
scelto secondo il tipo di prodotto desiderato (i Builder dovranno implementare un’interfaccia comune per
consentire al Director di interagire con tutti questi). Potrebbe capitare che per ottenere un prodotto
particolare alcune tappe del processo di costruzione non debbano essere considerate da alcuni Builder (ad
esempio, il Builder che costruisce i “modelli non orientati”, deve trascurare il nome della relazione e il grado
della partecipazione minima).
Struttura:
Partecipanti:
Builder: Dichiara una interfaccia per le operazioni che creano le parti dell’oggetto Product. Implementa il
comportamento default per ogni operazione.
ConcreteBuilder: Forniscono le operazioni concrete dell’interfaccia corrispondente al Builder. Costruiscono e
assemblano le parti del Product. Forniscono un metodo per restituire il Product creato.
Director: Costruisce il Product invocando i metodi dell’interfaccia del Builder. Product: Rappresenta l’oggetto complesso in costruzione. I ConcreteBuilders costruiscono la rappresentazione interna del Product. Include classi che definiscono le parti costituenti del Product.
Conseguenze: consente di cambiare la rappresentazione interna del prodotto: il Builder non conosce la
rappresentazione interna del prodotto che può essere cambiata semplicemente costruendo un nuovo
Builder.
isolamento tra Builder: ogni Builder è indipendente dall’altro pertanto è possibile aumentare la modularità.
controllo accurato del processo di creazione: la creazione avviene step-by-step e questo consente di stabilire
passo dopo passo cosa effettuare.
Osservazioni: La classe astratta Builder dichiara il metodo getModel che i ConcreteBuilders devono
implementare, con il codice necessario per restituire ogni particolare tipo Product. Il tipo di ritorno del
metodo getModel è indicato come Object, dato che a priori non si ha conoscenza della specifica tipologia di
Product. In questo modo si abilita la possibilità di restituire qualunque tipo d’oggetto (perché tutte le classi
Java, in modo diretto o indiretto, sono sottoclassi di Object). Si fa notare che il “method overloading” di Java
non consente modificare la dichiarazione del tipo di valore di ritorno di un metodo di una sottoclasse,
motivo per il quale i ConcreteBuilders devono anche dichiarare Object come valore di ritorno, nei propri
metodi getModel.
Factory Method (C)
Intento: Definisce un’interfaccia per creare oggetti, ma lascia alle sottoclassi la decisione del tipo di classe a
istanziare.
Motivazione: Framework deve poter creare diversi documenti ma non sa il tipo concreto quindi uso il factory
Applicabilità: una classe non può anticipare classi concrete da creare oppure vuole che subclassi decidendo
cosa creare. Le classi delegano la responsabilità a subclassi e vogliono localizzare di chi è delegato. Il pattern
“Factory Method” suggerisce il portare via dal framework la creazione di ogni particolare tipo di Elemento.
Per fare ciò, verrà delegato alle sottoclassi dello Strumento, che specializzano le funzioni di gestione di ogni
tipo di Elemento, il compito di creare le particolari istanze di classi che siano necessarie.
Stuttura:
Partecipanti
Product: Definisce l’interfaccia di tutti gli elementi da utilizzare nell’applicazione.
ConcreteProduct: Implementano i concreti prodotti.
Creator: Dichiara il factory method che restituisce un oggetto della classe Product. Richiama il factory
method per creare i Product.
ConcreteCreator: Redefine il factory method per restituire una istanza di ConcreteProduct.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
rappresenta un gancio alle sotto-classi: tramite il Creator è possibile scegliere quale classe concreta utilizzare
e decidere di cambiarla senza avere nessun impatto verso il Client
consente di collegare gerarchie di classi in modo parallelo: i ConcreteCreator possono collegarsi con i
ConcreteProduct e generare un collegamento parallelo tra gerarchie diverse.
Osservazioni: Si vuole rendere noto che il factory method (newElement) dichiara come tipo da restituire al
punto di chiamata, sia nel Creator (ElementHandler), sia in ogni ConcreteCreator (PlaceHandler e
ConnectorHandler), un oggetto di tipo Product (MapElement), invece dei particolari tipi da produrre (Place e
Connector). Questo è dovuto al fatto che le sottoclassi che redefiniscono un metodo devono esplicitare
lo stesso tipo di ritorno che quello indicato nella dichiarazione del metodo nella superclasse.
Adapter (S)
Intento: Specifica i tipi di oggetti a creare, utilizzando un’istanza prototipo, e crea nuove istanze tramite la
copia di questo prototipo.
Motivazione: Si tratta di un pattern strutturale basato su classi o su oggetti in quanto è possibile ottenere
entrambe le rappresentazioni. Viene utilizzato quando si intende utilizzare un componente software ma
occorre adattare la sua interfaccia per motivi di integrazione con l’applicazione esistente. Questo comporta
la definizione di una nuova interfaccia che deve essere compatibile con quella esistente in modo tale da
consentire la comunicazione con l’interfaccia da “adattare”. Come abbiamo accennato, tale pattern può
essere basato sia su classi che su oggetti pertanto l’instanza della classe da adattare può derivare da
ereditarietà oppure da associazione.
Applicabilità: L’Adapter pattern offre due soluzioni possibili, denominate Class Adapter e Object Adapter,
che si spiegano di seguito:
- Class Adapter: la classe esistente si estende in una sottoclasse che implementa la desiderata
interfaccia. I metodi della sottoclasse mappano le loro operazioni in richieste ai metodi e attributi della classe
di base.
- Object Adapter: si crea una nuova classe che implementa l’interfaccia richiesta, e che
possiede al suo interno un’istanza della classe a riutilizzare. Le operazioni della nuova classe fanno
invocazioni ai metodi dell’oggetto interno.
Struttura:
Per il class adapter
Per Object Adapter
Partecipanti
TargetInterface: Specifica l’interfaccia che il Client utilizza.
Client: Comunica con l’oggetto interessato tramite la TargetInterface.
Adaptee: Implementa una interfaccia che deve essere adattata.
Adapter: Adatta l’interfaccia dell’Adaptee verso la TargetInterface.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
Class Adapter: prevede un rapporto di ereditarietà tra Adapter e Adaptee, in cui Adapter specializza Adaptee,
pertanto non è possibile creare un Adapter che specializzi più Adaptee. Se esiste una gerarchia di Adaptee
occorre creare una gereachia di Adapter.
Object Adapter : prevede un rapporto di associazione tra Adapter e Adaptee, in cui Adapter instanzia
Adaptee, pertanto è possible avere un Adapter associato con più Adaptee.
Osservazioni: La strategia di costruire un Class Adapter è possibile soltanto se l’Adaptee non è stato
dichiarato come final class.
Bridge (S)
Intento: Separa un’astrazione dalla sua implementazione, in modo che entrambe possano variare
indipendentemente.
Motivazione: Si tratta di un pattern strutturale basato su oggetti che viene utilizzato per disaccoppiare dei
componeti software. In questo modo è possibile effettuare uno switch a Run-Time, garantire il
disaccoppiamento, nascondere l’implementazione, estendere la specializzazione delle classi.
Applicabilità: Il “Bridge” pattern suggerisce la separazione dell’astrazione dall’implementazione, in gerarchie
diverse, legando oggetti della seconda a quelli della prima, tramite un relazione di conmposizione.
Struttura:
Partecipanti
Abstraction: Specifica l’interfaccia dell’astrazione. Gestisce un riferimento ad un oggetto Implementor.
RefinedAbstraction: Implementano l’interfaccia definita dall’Abstraction.
Implementor: Specifica l’interfaccia definita per le classi di implementazione.
ConcreteImplementor: Implementano l’interfaccia Implementor.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
disaccoppia l’interfaccia dall’implementazione: disaccoppiando Abstraction e Implementor è possibile gestire
i cambiamenti delle classi concrete senza cablare nel codice dei riferiementi diretti
migliora l’estendibilità: è possibile estendere la gerarchia di Abstraction e Implementor senza problemi
nasconde l’implementazione al client: il Client non si deve porre il problema di conoscere l’implementazione
delle classi concrete.
Composite (S-B)
Intento: Consente la costruzione di gerarchie di oggetti composti. Gli oggetti composti possono essere
conformati da oggetti singoli, oppure da altri oggetti composti. Questo pattern è utile nei casi in cui si vuole:
- Rappresentare gerarchie di oggetti tutto-parte.
- Essere in grado di ignorare le differenze tra oggetti singoli e oggetti composti.
Motivazione: Si tratta di un pattern strutturale basato su oggetti che viene utilizzato quando si ha la
necessità di realizzare una gerarchia di oggetti in cui l’oggetto contenitore può detenere oggetti elementari
e/o oggetti contenitori. L’obiettivo è di permettere al Client che deve navigare la gerarchia, di comportarsi
sempre nello stesso modo sia verso gli oggetti elementari e sia verso gli oggetti contenitori.
Applicabilità: Il pattern “Composite” definisce la classe astratta componente che deve essere estesa in due
sottoclassi: una che rappresenta i singoli componenti, e un’altra che rappresenta i componenti composti, e
che si implementa come contenitore di componenti. Il fatto che quest’ultima sia un contenitore di
componenti, li consente di immagazzinare al suo interno, sia componenti singoli, sia altri contenitori (dato
che entrambi sono stati dichiarati come sottoclassi di componenti).
Struttura:
Partecipanti
Component: Dichiara una interfaccia comune per oggetti singoli e composti. Implementa le operazioni di
default o comuni tutte le classi.
Leaf: Estende la classe Component, per rapperesentare gli oggetti che non sono composti (foglie).
Implementa le operazioni per questi oggetti.
Composite: Estende la classe Component, per rappresentare gli oggetti che sono composti.Immagazzina al
suo interno i propri componenti. Implementa le operazioni proprie degli oggetti composti, e
particolarmente quelle che riguardano la gestione dei propri componenti.
Client: in questo esempio sarà il programma principale quello che farà le veci di cliente. Utilizza gli oggetti
singoli e composti tramite l’interfaccia rappresentata dalla classe astratta Component.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
Definisce la gerarchia: Gli oggetti della gerarchia possono essere composti da oggetti semplici e/o da oggetti
contenitori che a loro volta sono composti ricorsivamente da altri oggetti semplici e/o da oggetti contenitori .
Semplifica il client: il Client tratta gli oggetti semplici e gli oggetti contenitori nello stesso modo. Questo
semplifica il suo lavoro il quale astrae dalla specifica implementazione.
Semplifica la modifica dell’albero gerarchico: l’alberatura è facilmente modificabile
aggiungendo/rimuovendo foglie e contenitori.
Decorator (S-B)
Intento: Aggiunge dinamicamente responsabilità addizionali ad un oggetto. In questo modo si possono
estendere le funzionalità d’oggetti particolari senza coinvolgere complete classi.
Motivazione: Si tratta di un pattern strutturale basato su oggetti che viene utilizzato per aggiungere a
RunTime delle funzionalità ad un oggetto. In Java, e più in generale nella programmazione ad oggetti, per
aggiungere delle funzionalità ad una classe viene utilizzata l’ereditarietà che prevede la creazione di classi
figlie che specializzano il comportamento della classe padre ma tutto ciò avviene a CompileTime. Pertanto se
in sede di definizione della struttura delle classi non vengono previste delle specifiche funzionalità, queste
non saranno disponibili a RunTime. Al fine di superare questo limite, attraverso la decorazione è possibile
aggiungere nuove funzionalità senza dover alterare la struttura delle classi ed i rapporti di parentela in
quanto è possibile agire a RunTime per modificare il comportamento di un oggetto.
Applicabilità: Il pattern suggerisce la creazione di wrapper classes (Decorator) che racchiudono gli oggetti
ai quali si vuole aggiungere le nuove responsabilità. Questi ultimi oggetti, insieme ai Decorator devono
implementare una interfaccia comune, in modo che l’applicazione possa continuare ad interagire con gli
oggetti decorati. Per una stessa interfaccia possono esserci più Decorator, ad esempio, per investire i ruoli di
capoufficio e di responsabile di un progetto. Il fatto che Decorator e oggetti decorati implementino la stessa
interfaccia, consente anche l’applicazione di un Decorator ad un altro oggetto già decorato, ottenendo in
questo modo la sovrapposizione di funzioni (ad esempio, un impiegato potrebbe essere investito come
capoufficio e responsabile di un progetto contemporaneamente).
Struttura:
Partecipanti
Component: Specifica l’interfaccia degli oggetti che possono avere delle responsabilità aggiunte
dinamicamente.
ConcreteComponent: Implementa l’oggetto in cui si possono aggiungere nuove responsabilità.
Decorator: Possiede un riferimento all’oggetto Component e specifica una interfaccia concordante con
l’interfaccia Component.
ConcreteDecorator: Aggiunge nuove responsabilità al Component.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
maggiore flessibilità rispetto alla eredità: permette di aggiungere funzionalità in modo molto più semplice
rispetto all’ereditarietà
funzionalità solo se richieste: consente di aggiungere delle funzionalità solo se occorrono realmente senza
ereditare una struttura di classi che prevede un insieme di funzionalità di cui se ne utilizzeranno olo una
parte. Nel caso in cui tali funzionalità sono anche a pagamento, consente di scegliere solo quelle
strettamente necessarie da acquistare, coprendo esigenze di budget.
aumento di micro-funzionalità: la presenza di molte classi Decorator di cui ognuna di esse aggiunge una
micro funzionalità, può creare problemi in fase di comprensione o di debug del codice.
Facade (S)
Intento: Fornisce una interfaccia unificata per un insieme di interfacce di un sottosistema, rendendo più
facile l’uso di quest’ultimo.
Motivazione: Si tratta di un pattern strutturale basato su oggetti che viene utilizzato per nascondere la
complessità del sistema e ridurre la comunicazione e la dipendenza del Client. L’utilizzo di questo pattern
prevede di esporre una interfaccia per l’invocazione di un Sistema tale da semplificare l’invocazione ad opera
del Client.
Applicabilità: Il “Facade” pattern suggerisce la creazione di un oggetto che presentia un’interfaccia
semplificata al cliente, ma in grado di gestire tutta la complessità delle interazioni tra gli oggetti delle diverse
classi per compiere l’obbiettivo desiderato.
Struttura:
Partecipanti:
Facade: Ha conoscenza delle funzionalità di ogni classe del sottosistema. Delega agli appropriati
oggetti del sottosistema ogni richiesta pervenuta dall’esterno.
Subsystem classes: Implementano le funzionalità del sottosistema. Gestiscono le attività assegnate dal
Facade. Non hanno riferimenti verso il Facade.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
riduce il numero di associazioni: disaccoppiando il Client dal Sistema è possibile ridurre il numero di
associazioni effettuate tra questi 2 attori, riducendo le interazioni.
agevola il cambiamento : il basso accoppiamento rende possibile le modifiche al Sistema senza dover
modificare anche il Client.
non esclude l’uso diretto del Sistema : il Client può comunque utilizzare direttamente il Sistema se lo ritiene
necessario. L’esistenza del Facade non esclude la possibilità di farlo: sempre che sappia come fare.
Proxy (S-B)
Intento: Fornisce una rappresentazione di un oggetto di accesso difficile o che richiede un tempo importante
per l’accesso o creazione. Il Proxy consente di posticipare l’accesso o creazione al momento in cui sia
davvero richiesto
Motivazione: Si tratta di un pattern strutturale basato su oggetti che viene utilizzato per accedere ad un un
oggetto complesso tramite un oggetto semplice.
Questo pattern può risultare utile se l’oggetto complesso: richiede molte risorse computazionali. richiede
molto tempo per caricarsi. è locato su una macchina remota e il traffico di rete determina latenze ed
overhead. non definisce delle policy di sicurezze e consente un accesso indiscriminato. non viene mantenuto
in cache ma viene rigenerato ad ogni richiesta. In tutti questi casi è possibile disposte delle politiche di
gestione e/o di ottimizzazione. A seconda del contesto, viene aggiunto un prefisso per descrivere il caso di
riferimento:
Virtual Proxy Pattern: ritarda la creazione e l’inizializzazione dell’oggetto poiché richiede grosse risorse (es:
caricamento immagini )
Remote Proxy Pattern: fornisce una rappresentazione locale dell’oggetto remoto (es: accesso ad oggetto
remoto tramite RMI )
Protection Proxy Pattern: fornisce un controllo sull’accesso dell’oggetto remoto (es: richiesta
username/password per l’accesso)
Smart Proxy Pattern: fornisce una ottimizzazione dell’oggetto (es: caricamento in memoria dell’oggetto)
Il proxy espone gli stessi metodi dell’oggetto complesso che maschera e questo permette di adattare
facilmente l’oggetto senza richiedere modifiche.
Applicabilità: l “Proxy” pattern suggerisce l’implemetazione di una classe (ProxyFileHandler) che offra la stesa
interfaccia della classe originale (FileHandler), e che sia in grado di risolvere le richieste più “semplici”
pervenute dall’applicativo, senza dover utilizzare inutilmente le risorse (ad esempio, restituire il nome del
file). Solo al momento di ricevere una richiesta più “complessa” (ad esempio, restituire il testo del file), il
proxy andrebbe a creare il vero FileHandler per inoltrare a esso le richieste. In questo modo gli oggetti più
pesanti sono creati solo al momento di essere necessari. Il proxy che serve a questa finalità spesso viene
chiamato “virtual proxy”.
Struttura:
Partecipanti:
Proxy: Mantiene un riferimento per accedere al RealSubject. Implementa una interfaccia identica a quella del
RealSubject, in modo che può sostituire a esso. Controlla l’acceso al RealSubject, essendo responsabile della
sua istanziazione e gestione di riferimenti. Come virtual proxy pospone la istanziazione del RealSubject,
tramite la gestione di alcune informazioni di questo.
Subject: Fornisce l’interfaccia comune per il RealSubject e il Proxy, in modo che questo ultimo possa essere
utilizzato in ogni luogo dove si aspetta un RealSubject.
RealSubject: Implementa l’oggetto vero e proprio che il RealSubject rappresenta.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
un Proxy Remoto: nasconde il fatto che un oggetto appartiene ad un diverso spazio di indirizzamento
un Proxy Virtuale: ottimizza la creazione di un oggetto solo nel momento in cui è realmente necessario
un Protection Proxy ed uno Smart Proxy: aggiungono ulteriori comportamenti quando si accede ad un
oggetto
Chain of Responsibilty (B)
Intento: Consente di separare il mittente di una richiesta dal destinatario, in modo di consentire a più di un
oggetto di gestire la richiesta. Gli oggetti destinatari vengono messi in catena, e la richiesta trasmessa dentro
questa catena fino a trovare un oggetto che la gestisca.
Motivazione: Si tratta di un pattern comportamentale basato su oggetti e viene utilizzato quando si ha la
necessità di disaccoppiare il mittente di una richiesta dal destinatario. Nel caso in cui il destinatario preveda
che le richieste debbano essere gestite da una serie di attori ognuno dei quali con diversa responsabilità e tra
loro collegati in modo gerarchico, siamo in presenza di una catena di responsabilità. A fronte della ricezione
di una richiesta, il destinatario gestirà la risposta propagando la richiesta nella catena fino ad individuare il
responsabile. La gerarchia solitamente parte dal generale al particolare pertanto una richiesta destinata
all’ultimo elemento della catena verrà propagata verso l’alto fino a raggiungere l’incaricato responsabile che
avrà il compito di gestire/eseguire l’azione richiesta. Il mittente non è tenuto a conoscere chi materialmente
dovrà gestire/eseguire la richiesta, l’unica cosa che dovrà sapere è a chi dovrà inviare la richiesta. Sarà cura
del destinatario organizzarsi in modo efficiente per recuperare il responsabile. Pensiamo per esempio ad un
call-center che deve gestire le richieste delle proprie utenze, qualora il personale non è in grado di risolvere il
problema, propaga la richiesta al servizio di secondo livello che proverà a gestire/risolvere il problema
altrimenti propaga a sua volta il problema. In Java la propagazione delle eccezioni è un esempio di catena di
responsabilità. Quando si verifica un errore, il gestore dell’errore, se non riesce a gestire l’eccezione in corso,
propaga l’errore nella catena.
Applicabilità: Il “Chain of responsibility” pattern propone la costruzione di una catena di oggetti responsabili
della gestione delle richieste pervenute dai clienti. Quando un oggetto della catena riceve una richiesta,
analizza se corrisponde a lui gestirla, o, altrimenti, inoltrarla al seguente oggetto dentro la catena. In questo
modo, gli oggetti che iniziano la richiesta devono soltanto interfacciarsi con l’oggetto più basso della catena
di responsabilità.
Struttura:
Partecipanti
Handler: Specifica una interfaccia per la gestione delle richieste. In modo opzionale, implementa un
riferimento a un oggetto successore.
ConcreteHandler: Gestiscono le richieste che corrispondono alla propria responsabilità. Accedono ad
un successore (se è possibile), nel caso in cui la richiesta non corrisponda alla propria gestione.
Client: inoltra una richiesta a un ConcreteHandler della catena.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
riduce l’accoppiamento: il richiedente deve solo sapere che la propria richiesta verrà gestita
“adeguatamente”, non ha bisogno di sapere chi sarà realmente a gestire la richiesta.
aggiungere flessibilità nell’assegnazione delle responsabilità degli oggetti: la catena della responsabilità può
essere modificata senza condizionare il richiedente.
risposta non garantita: la richiesta viene presa in carico e propagata nella gerarchia ma potrebbe non
individuare il responsabile e non riuscire a dare una risposta. Oppure se la gerarchia delle responsabilità è
errata, la richiesta non viene gestita.
Osservazioni: Un altro approccio nell’utilizzo di questo design pattern può osservarsi nella nel meccanismo di
Java per la gestione delle eccezioni: ogni volta che viene sollevata una eccezione, questa può essere
gestita nello stesso metodo in cui si presenta (tramite le istruzioni “try…catch”), oppure essere lanciata
verso il metodo precedente nello stack di chiamate. A sua volta, questo metodo potrebbe gestire l’eccezione
oppure continuare a lanciarlo al successivo. Finalmente, se il metodo main non gestisce l’eccezione, la Java
Virtual Machine ne tiene cura di esso interrompendo l’esecuzione del programma e stampando le
informazioni riguardanti l’eccezione.
Iterator (B)
Intento: Fornisce un modo di accedere sequenzialmente agli oggetti presenti in una collezione, senza esporre
la rappresentazione interna di questa.
Motivazione: Si tratta di un pattern comportamentale basato su oggetti e viene utilizzato quando, dato un
aggregato di oggetti, si vuole accedere ai suoi elementi senza dover esporre la sua struttura. L’obiettivo di
questo pattern è quello di disaccoppiare l’utilizzatore e l’implementatore dell’aggregazione di dati, tramite
un oggetto intermedio che esponga sempre gli stessi metodi indipendentemente dall’aggregato di dati. E’
costituito da 3 soggetti: l’Utilizzatore dei dati, l’Iteratore che intermedia i dati e l’Aggregatore che detiene i
dati secondo una propria logica.
Applicabilità: Il pattern “Iterator” suggerisce l’implementazione di un oggetto che consenta l’acceso e
percorso della collezione, e che fornisca una interfaccia standard verso chi è interessato a percorrerla e ad
accede agli elementi.
Struttura:
Partecipanti:
Iterator: Specifica l’interfaccia per accedere e percorrere la collezione.
ConcreteIterator: Implementa la citata interfaccia. Tiene traccia della posizione corrente all’interno
della collezione.
Aggregate: specifica una interfaccia per la creazione di oggetti Iterator.
ConcreteAggregate: Crea e restituisce una istanza di iterator.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
unica interfaccia di accesso: l’accesso ai dati avviene tramite l’Iterator che espone un’unica interfaccia e
nasconde le diverse implementazioni degli Aggregator
diversi iteratori di accesso: l’Aggregator può essere attraversato tramite diversi Iterator in cui ogni Iterator
nasconde un algoritmo diverso
Osservazioni: In una situazione di acceso concorrente ad una collezione, diventa necessario fornire adeguati
meccanismi di sincronizzazione per l’iterazione su di essa, come si spiega nel Java Tutorial.
Mediator (B)
Intento: Definisce un oggetto che incapsula il modo di interagire di un gruppo d’oggetti, consentendo il
disaccoppiamento tra questi, in forma tale di poter variare facilmente le interazioni fra di loro.
Motivazione: Si tratta di un pattern comportamentale basato su oggetti e viene utilizzato per permettere lo
scambio di messaggi tra diversi attori tramite un intermediario. In questo modo gli attori sono collegati
indirettamente tramite un intermediario. Disaccoppiare gli attori consente di gestire meglio una serie di
problematiche come: centralizzazione delle connessioni, modifiche più rapide, semplificazione dei protocolli.
Nel gergo del pattern Mediator usiamo il termine Colleghi per indicare gli attori.
Applicabilità: Questo pattern propone la creazione di incapsulare il comportamento collettivo delle diversi
classi componeti il sistema (colleagues), tramite una classe separata denominata Mediator. Il Mediator
diventa l’agente d’intermediazione tra i diversi oggetti, i quali soltanto devono interfacciarsi con
esso, riducendo il numero di interconnessioni.
Struttura:
Partecipanti
Mediator: Specifica una interfaccia per la comunicazione da parte dei Colleagues.
ConcreteMediator: Implementa il comportamento cooperativo tramite la coordinazione dei
Colleagues. Possiede riferimenti verso i Colleagues.
Colleague: Possiede un riferimento al Mediator. Implementa un metodo di notifica di eventi al Mediator.
ConcreteColleague: Comunica gli eventi al Mediator invece di comunicarli ad altri Colleagues.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
Disaccoppiare i colleghi: i colleghi dialogano tra di loro iin modo indiretto passando per il Mediatore e questo
facilita la gestione delle comunicazioni
Semplificare le connessioni: il Mediatore consente di ridurre le connessioni dei Colleghi da many-to-many a
one-to-many
Controllo centralizzato: il controllo delle comunicazioni è centralizzato e questo consente di avere una
visione complessiva del sistema ed una gestione più efficente delle modifiche
Single Point of Failure: nel caso di malfunzionamento del Mediatore l’intero sistema sarà coinvolto ed in caso
di fermo del Mediatore, i Colleghi resteranno isolati
Osservazioni: Dato che il Mediator dichiara ma non implementa operazioni, questo viene specificato come
una interfaccia in Java.
Observer (B)
Intento: Consente la definizione di associazioni di dipendenza di molti oggetti verso di uno, in modo che se
quest’ultimo cambia il suo stato, tutti gli altri sono notificati e aggiornati automaticamente.
Motivazione: Si tratta di un pattern comportamentale basato su oggetti che viene utilizzato quando si vuole
realizzare una dipendenza uno-a-molti in cui il cambiamento di stato di un soggetto venga notificato a tutti i
soggetti che si sono mostrati interessati.
Applicabilità: Il pattern “Observer” assegna all’’oggetto monitorato (Subject) il ruolo di registrare ai suoi
interni un riferimento agli altri oggetti che devono essere avvisati (ConcreteObservers) degli eventi del
Subject, e notificarli tramite l’invocazione a un loro metodo, presente nella interfaccia che devono
implementare (Observer).
Struttura:
Partecipanti
Subject: Ha conoscenza dei propri Observer, i quali possono esserci in numero illimitato. Fornisce operazioni
per l’addizione e cancellazione di oggetti Observer. Fornisce operazioni per la notifica agli Observer.
Observer: Specifica una interfaccia per la notifica di eventi agli oggetti interessati in un Subject.
ConcreteSubject: Possiede uno stato dell’interesse dei ConcreteSubject. Invoca le operazioni di notifica
ereditate dal Subject, quando devono essere informati i ConcreteObserver.
ConcreteObserver: Implementa l’operazione di aggiornamento dell’Observer.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
Astratto accoppiamento tra Subject e Observer: il Subject sa che una lista di Observer sono interessati al suo
stato ma non conosce le classi concrete degli Observer, pertanto non vi è un accoppiamento forte tra di loro.
Notifica diffusa: il Subject deve notificare a tutti gli Observer il proprio cambio di stato, gli Observer sono
responsabili di aggiungersi e rimuoversi dalla lista.
Strategy (B)
Intento: Consente la definizione di una famiglia d’algoritmi, incapsula ognuno e gli fa intercambiabili fra di
loro. Questo permette modificare gli algoritmi in modo indipendente dai clienti che fanno uso di essi.
Motivazione: Si tratta di un pattern comportamentale basato su oggetti e viene utilizzato per definire una
famiglia di algoritmi, incapsularli e renderli intercambiabili. Il client definisce l’algoritmo da utilizzare,
incapsulandolo in un contesto, il quale verrà utilizzato nella fase di elaborazione. Il contesto detiene i
puntamenti alle informazioni necessarie al fine della elaborazione, cioè dati e funzione: solita equazione
y=f(x)!
Applicabilità: Lo “Strategy” pattern suggerisce l’incapsulazione della logica di ogni particolare algoritmo, in
apposite classi (ConcreteStrategy) che implementano l’interfaccia che consente agli oggetti MyArray
(Context) di interagire con loro. Questa interfaccia deve fornire un accesso efficiente ai dati del Context,
richiesti da ogni ConcreteStrategy, e viceversa.
Struttura:
Partecipanti
Strategy: Dichiara una interfaccia comune per tutti gli algoritmi supportati. Il Context utilizza questa
interfaccia per invocare gli algoritmi definiti in ogni ConcreteStrategy.
ConcreteStrategy: Implementano gli algoritmi che usano la interfaccia Strategy
Context: Viene configurato con un oggetto ConcreteStrategy e mantiene un riferimento verso esso. Può
specificare una interfaccia che consenta alle Strategy accedere ai propri dati.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
Un’alternativa all’ereditarietà: è possibile estendere il Context per specializzare il suo comportamento ma si
rischia di legare troppo il Context con lo Strategy
Le strategie eliminano i blocchi condizionali: le strategie consentono di eliminare i blocchi condizionali che
determinano il comportamento voluto.
Collaborazione tra Strategy e Context dispendiosa: le strategie possono richiedere maggiori o minori
informazioni di contesto di quello disponibili dal Context pertanto occorre definire una modalità di scambio
informazioni efficiente tra Context e Strategy. Un modo per evitare l’overloading dei metodi nelle classi di
Strategy per ricevere parametri diversi, è di inserire i parametri nel Context per poi passarlo alla classe
Strategy.
Visitor (B)
Intento: Rappresenta una operazione da essere eseguita in una collezione di elementi di una struttura.
L’operazione può essere modificata senza alterare le classi degli elementi dove opera.
Motivazione: Si tratta di un pattern comportamentale basato su oggetti e viene utilizzato per eseguire delle
operazioni sugli elementi di una struttura. L’utilizzo di questo pattern consente di definire le operazioni di un
elemento senza doverlo modificare.
Applicabilità: La soluzione consiste nella creazione di un oggetto (ConcreteVisitor), che è in grado di
percorrere la collezione, e di applicare un metodo proprio su ogni oggetto (Element) visitato nella collezione
(avendo un riferimento a questi ultimi come parametro). Per agire in questo modo bisogna fare in modo che
ogni oggetto della collezione aderisca ad un’interfaccia (Visitable), che consente al ConcreteVisitor di essere
“accettato” da parte di ogni Element. Poi il Visitor, analizzando il tipo di oggetto ricevuto, fa l’invocazione alla
particolare operazione che in ogni caso si deve eseguire.
Struttura:
Partecipanti
Visitor: Specifica le operazioni di visita per ogni classe di ConcreteElement.
ConcreteVisitor: Specifica le operazioni di visita per ogni classe di ConcreteElement. La firma di ogni
operazione identifica la classe che spedisce la richiesta di visita al ConcreteVisitor, e in questo modo il visitor
determina la concreta classe da visitare. Finalmente il ConcreteVisitor accede agli elementi direttamente
tramite la sua interfaccia.
Element: Dichiara l’operazione accept che riceve un riferimento a un Visitor come argomento.
ConcreteElement: Implementa l’interfaccia Element.
ObjectStructure: Offre la possibilità di accettare la visita dei suoi componenti.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
Facilità nell’aggiungere nuovi Visitor: definendo un nuovo Visitor sarà possibile aggiungere una nuova
operazione ad un Element
Difficoltà nell’aggiungere nuovi Element: definire un nuovo Element comporterà la modifica dell’interfaccia
Visitor e di tutte le implementazioni
Separazione tra stato ed algoritmi: gli algoritmi di elaborazioni sono nascosti nelle classi Visitor e non
vengono esposti nelle classi Element.
Iterazione su struttura eterogenea: la classe Visitor è in grado di accedere a tipi diversi, senza la necessità
che tra di essi ci sia un vincolo di parentela. In poche parole, il metodo visit() può definire come parametro
un tipo X oppure un tipo Y senza che tra di essi ci sia alcuna relazione di parentela, diretta o indiretta.
Accumulazione dello stato: un Visitor può accumulare delle informazioni di stato a seguito
dell’attraversamento degli Element.
Violazione dell’incapsulamento: i Visitor devono poter accedere allo stato degli Element e questo può
comportare la violazione dell’incapsulamento.
Una libreria: è un insieme di funzioni che è possibile chiamare, al giorno d’oggi è solitamente organizzata in
classi. Ogni chiamata fa un certo lavoro e restituisce il controllo al client.
Un Framework: rappresenta un certo disegno astratto, con più comportamenti integrati. Per utilizzarlo,
inserisci le tue metodologie nei vari luoghi del framework sia per sottoclassarlo, sia per inserirlo nelle tue
classi. Il codice del framework chiama poi il codice in questi punti.
“Una caratteristica importante di un framework è che i modi definiti dall'utente , che permettono di
adattare un framework, saranno spesso ri-chiamati all’interno dello stesso framework, piuttosto che dal
codice dell'applicazione utente. Il framework gioca spesso il ruolo del programma principale nel
coordinamento e nel sequenziamento dell’attività dell'applicazione. Questa inversione di controllo
conferisce al framework il potere per funzionare come estensibile di scheletri.( Nel senso scheletri di
implementazioni). I metodi forniti dall’utente personalizzano i generici algoritmi definiti nel framework per
una particolare applicazione. " (Ralph Johnson e Brian Foote)
NB: L’inversione di controllo è un fenomeno comune che si incontra quando si estende i framework. Infatti
è spesso inteso come una caratteristica distintiva di un framework. "
Il passaggio dalle librerie ai framework è molto interessante per quanto riguarda il fenomeno
dell’inversione di controllo nel ambito di chi-chiama-chi contro chi-riusa-chi.
a) nuovo codice invoca oggetti riutilizzati di una libreria (Inversione di controllo)
b) nuovo codice viene invocato dagli oggetti, in un framework riutilizzato (principio di Hollywood)
NB: Nell’ingegneria del software, il principio di Hollywood è indicato come "non ci chiamare, ti chiameremo
noi."
Adattatori pluggable: Si consideri un framework che fornisce un'implementazione astratta di responsabilità
applicabile ad una varietà di tipi. Esempio: avvolgere un payload all'interno di un involucro, inviare
l’involucro, involucro di che cosa?
Il framework può predisporre l'installazione di un adattatore per facilitare l’istanziazione vera e propria. Si
noti che qui l'adattatore è progettato in anticipo, non come aggiornamento. Vi sono 2 schemi:
1) Abstract Operations
2) Delegate Objects.
Abstract Operations: si divide in
FrameworkClass: è parte del framework, definisce una virtuale ristretta interfaccia: un insieme minimo di
metodi che possono assumersi tutte le responsabilità che dipendono dal tipo specifico di oggetti gestiti.
FrameworkAdapter: invoca i metodi concreti nel dominio specifico, serve per attuare operazioni
nell'interfaccia ristretta
Delegate Objects: si divide in
Delegate: specifica un’interfaccia ristretta includendo il contesto di dipendenza
FrameworkClassObj: delega le operazioni, in base al contesto, al LibClassDelegate. Espone un metodo
setDelegate () che consente l'installazione del delegato
FrameworkAdapterObj: Implementa il Delegate utilizzando SpecificClass. Si installa in FrameworkClassObj
come delegato (ad esempio nel costruttore)
UML (Unifiel modelling language)
Language: una notazione, più tardi ne daremo un metodo di utilizzo
Modelling: adatto per l’astrazione, maggiormente per l’object oriented
Unifield: una suite di diagrammi per approcci differenti, i quali con certe regole (core diagrams)
Class diagram: un core diagram nella suite UML, nativamente inteso in modo da catturare un OO
implementation ma equamente rilevante in altri livelli di astrazione e per intenti differenti. Serve per
catturare l’organizzazione delle classi, provvede ad una vista statica, infatti apre a differenti istanzazioni e
caratteristiche potenziali. Una classe è intesa come un costrutto del OO language.
La visibilità: + public, # protected, ~ package – protected, - private.
Posso indicare anche I tipi di parametri e del valore di ritorno. Es: init (size:int):void -> ho un parametro in
ingresso int e ritorna un void. I metodi virtuali li indico con il corsivo, = 0, apponendo << virtual >>. Gli
stereotipi in accordo con alcuni profili aggiungono sematica specializzata. Un esempio è << persistent >>.
Lo scopo finale è la comunicazione tra gli sviluppatori, si documentano le relazioni strutturali tra due classi.
L’oggetto di una classe mantiene un riferimento ad un oggetto dell’altra classe, e viceversa.
Indica una direzione privilegiata, il medico ha un riferimento ad assistito, infatti è di interesse sapere quanti
assistiti ha un medico e non quanti medici ha un assistito.
Il medico avrà un elenco di assistiti, in cui c’è riferimento agli assistiti.
La * indica che potrei inserire anche un numero.
Documenta la situazione in cui metodi di una classe (cliente) istanziano un’oggetto di un’altra classe
(ordine).
Documenta la situazione dove metodi di una classe dipendono da metodi o attributi di un’altra. È diverso
dall’associazione poiché “uses” non corrisponde ad una variabile della classe ma piuttosto a qualche
parametro passato in qualche invocazione.
Quindi la differenza tra associazione e uso è che la prima documenta una relazione strutturale tra due tipi e
non un riferimento temporaneo entro i limiti di un’operazione. Quindi il link tra un medico e un paziente è
un’associazione, il link tra un medico e un paziente iscritto per una visita è una relazione di uso. Quindi la
prima è un datamember con valore nell’intera computazione, la seconda è un puntatore ricevuto come
parametro nell’invocazione di un metodo.
Aggregazione: quando morto uno non muore l’altro, cioè se uno studente smette, non finisce il suo
corso di laurea.
Composizione: uno esiste solo grazie ad un altro. Se lo studente smette, il libretto universitario
sparisce.
La relazione di generalizzazione documenta sostituibilità (per le interfacce) e eredità nello stesso modo.
Un pediatra può sostituire un medico, il contrario no.
Un package lo posso indicare in due modi.
Posso rappresentare istanze di classi con un class diagram: nell’esempio un aereo ha due istanze diverse
dove gli attributi di flight assumono valore.
Object diagram:
Documenta l’organizzazione delle istanze delle classi in qualche fase significativa o esemplificativa
dell’esecuzione. Rappresenta oggetti con i loro attributi che hanno assunto valore e relazioni tra oggetti.
Non è un core diagram e serve solitamente per capire le conseguenze di un class diagram. È ancora uno
static diagram. Notiamo che i tipi, cioè le classi, si vedono quando scriviamo codice ma il run – time è fatto
di istanze. Abbiamo detto che gli stereotipi specializzano la semantica di qualche classe, per esempio <<
persistent >> indica che prendi da un database gli elementi e quando finisce riaggiorna il database. Gli
stereotipi si riuniscono in dei profili, spesso questi sono standardizzati in gruppi, ossia ci sono vari profili
standard: sysml, marte che specializzano l’uml attraverso stereotipi che prendono concetti dello specifico
dominio. Fare model driven development può essere interessante perché scritto l’uml e generato il codice
verifico se l’uml è corretto e allora mi dice che il codice è corretto. In principio un modello implementativo
potrebbe essere 1:1 con la scrittura del codice ma la giusta strada è tendente a una rappresentazione close
enough, ossia scrivere solo per capire il senso. Ho 3 livelli di astrazione:
1) prospettiva implementativa: rappresenta il modo in cui è realizzato un sistema sw. Un oggetto nella
rappresentazione corrisponde ad un oggetto nel linguaggio OO.
2) prospettiva specificativa: rappresenta le astrazioni che saranno realizzate in un sistema sw. Un oggetto
della specifica è realizzato attraverso più oggetti nell’implementazione.
3) prospettiva concettuale: descrive l’entità di un dominio applicativo che non per forza sono
rappresentate in una realizzazione sw.
Ho due intenti di rappresentazione: 1) specializzazione: di qualcosa che dovrebbe essere implementato, 2)
descrizione: di qualcosa che già esiste.
Il class diagram nella prospettiva di specificazione astrae dai costrutti idioms di un linguaggio specifico. È
essenziale per un progetto di alto livello. Utile per la descrizione di una architettura sw a differenti livelli di
granularità. Documenta l’organizzazione di un componente esistente. Pianifica l’integrazione e la
manutenzione.
L’association classes fornisce attributi e metodi per le associazioni (utilizzata nella prospettiva di
specializzazione e concettuale per guidare l’implementazione).
Una generalizzazione si può riferire a multiple direzioni ortogonali con un discriminatore per ogni direzione.
Ci sono altri costrutti
Vincoli: dentro alle parentesi graffe {}. Commenti: in dei riquadri contesto libero. Annotazioni: supportati
dai tool di modifica.
Il class diagram nella prospettiva concettuale: è indipendente dalle possibili implementazioni dentro un
sistema informatico. Primariamente è inteso per l’analisi e per la definizione dei requisiti. Abbiamo visto
che la vista statica è data da:
1) class diagram: tipi e relazioni tra gli oggetti nei tipi 2) object diagram: un particolare istanza di tipi.
Invece nella vista dinamica (da vedere): 1) collaboration diagram e sequence diagram documentano la
sequenza dei messaggi scambiati tra un insieme di oggetti in uno scenario di esecuzione. 2) activity
diagram: aggiunge concetti di parallelismo e concorrenza.
Il sequence diagram tipicamente è utilizzato per documentare la dinamica su un class diagram:
Quindi ci dà il tempo di vita e di attivazione degli oggetti attivati in uno scenario di esecuzione. I messaggi
che si scambiano sono creazione, invocazione di metodi e self reference. La direzione verticale è il tempo,
quella orizzontale gli oggetti. È utile per descrivere la dinamica associata a schemi statici. Pericoloso nella
progettazione e nell’analisi. Il collaboration diagram rappresenta l’ordine degli eventi numerandoli
esplicitamente ma è come il sequence diagram solo che privilegia la rappresentazione dell’organizzazione
degli oggetti.
Use case diagram e template
Requisiti funzionali: catturano il comportamento atteso dal system in termini di servizi, compiti e funzioni.
Ben inquadrati nel modello della specifica dei requisiti sw: srs – 232.
Use case: è la pratica dominante nella rappresentazione dei requisiti funzionali danno struttura e metodo
all’idea di catturare requisiti funzionali a partire da esempi e scenari di interazione e uso. Quindi sono un
insieme di interazioni tra il sistema e 1 + attori esterni finalizzati ad un obiettivo. Gli attori sono un’entità
esterna al sistema che interagisce con esso (può essere una classe di utenti, un ruolo, un altro sottosistema,
una parte di sistema non sviluppata). Si dividono in attori primari che richiedono il supporto del sistema e in
attori secondari di cui ne richiede assistenza il sistema. Quindi in caso d’uso è avviato da un attore con un
obiettivo e si conclude con successo con il raggiungimento di questo, cioè si cattura chi fa cosa con quale
obiettivo attraverso quali passi. Si deve descrivere le varie sequenze con cui si raggiunge l’obiettivo anche
quelle che conducono ad un fallimento per via di anomalie o errori. Uno scenario è una specifica sequenza
del caso d’uso. Le interazioni includono stimoli e risposte, l’use case non tratta di come il sistema è
realizzato quindi si realizza una lista completa di casi d’uso che specifica tutti i differenti modi di usare il
sistema (quindi definisce il comportamento richiesto delimitando lo scope del sistema).
Use case diagram: visione d’insieme, relazioni tra attori e casi d’uso, strutturazione dei casi d’uso.
Caso d’uso è un insieme di interazioni tra il sistema e uno o più attori esterni finalizzati ad un obiettivo, con
varie funzioni. Poi c’è l’attore (omino stilizzato) e la relazione di uso.
L’attore interagisce con il caso d’uso fornendo stimoli -> e ricevendo risposte <-. Un use case diagram non
documenta il flusso di informazioni né descrivere le relazioni di precedenza tra casi d’uso, secondo il
principio di break down funzionale lo svolgimento di un caso include quello di uno o più sottocasi. Quindi
per fare questo si utilizzano le relazioni di inclusione finalizzate a partizionare la complessità infatti aiuta il
programmatore che per arrivare a dimostrare il caso deve aver implementato i sottocasi e il costo del caso
aggrega quello dei sottocasi. Quindi ho:
<< include >>: sottocaso, parte di un caso più complesso
<< extende >>:punto di estensione
<< invokes >>: un caso ne invoca un altro: è uguale a include + extende
<< precedes >>: un caso deve terminare prima dell’avvio dell’altro
La generalizzazione (detta specializzazione) è applicabile sia agli attori che ai casi d’uso. Un system
baundaries delimita i contorni e le interfacce di un sottosistema. Ho 4 diversi livelli di astrazione:
1) high summary: non è una specifica sufficiente, definisce un ambito da analizzare
2) summary: fornisce visione d’insieme sui casi di livello user – goal
3) user goal: specifica un’interazione ed è il livello che conviene dettagliare con template testuali e mock –
up
4) function: specifica un dettaglio dell’interazione che ha qualche particolarità, di solito non documentato
in modo individuale
Use case template: i singoli casi d’uso oltre ai diagrammi vengono documentati in forma testuale non
ambigua e semplice. Il linguaggio è del dominio applicativo, meglio se riferito ad un modello concettuale
condiviso. In questa fase si deve abilitare un coinvolgimento di utenti ed esperti del dominio per seguire e
validare i casi d’uso e per definire i requisiti. Vediamo come si struttura.
Ucd:
Nome: deve suggerire l’obiettivo del caso d’uso.
Numero di riferimento: per essere referenziato in altri casi, strutturato in forma gerarchica
History: chi ha creato/modificato il caso, quando e perché
Source: identifica da dove è stato estratto il requisito, esempio derivato dal capitolo tecnico..
Livello: livello di astrazione utilizzato, tipicamente user – goal
Description: l’obiettivo descritto in un unico periodo nella prospettiva del contesto applicativo
Scope: organizzazione sistema componenti
Attori: attori coinvolti qualificati come primari e secondari. Esempio cliente (primary) sottosistema gestione
prenotazioni (secondary)
Precondition: le precondition necessarie a completare con successo il caso o comunque necessarie per non
dover fare riferimento a eventuali estensioni. Esempio il cliente ha già un account.
Postcondition: le condizioni che si hanno al completamento del caso d’uso con successo o fallimento
dell’obiettivo. Esempio viene messa una prenotazione è caricata la carta di credito.
Normal flow: trigger: azione che avvia il caso. Passi: sequenza di interazioni tra attore e sistema necessaria
a raggiungere l’obiettivo del caso i passi sono numerati descritti in un linguaggio naturale con eventuale uso
di alternative, ripetizioni e concorrenza.
Variazioni: specifica dove utile varianti con cui possono essere eseguiti singoli passi
Alternative flow: azioni che determinano diramazione rispetto alla sequenza comune dei passi che ne
seguono. Esempio 15.a il cliente abbandona la transazione.
Riferimenti: casi superordinati e/o subordinati
Requisiti non funzionali: requisiti architetturali o di qualità che però non sempre sono partizionabili sui
singoli casi d’uso. Esempio: performance 200 transazioni al minuto con tempi di attesa minori o uguali di 5
secondi.
Issues: elenco di aspetti che devono ancora essere chiariti con note su possibili strategie di
implementazione o sull’impatto verso altri casi, assunzioni fatte nello specificare il caso. Esempio il cliente
può applicare il procedimento ai treni regionali?
Priorità: criticità rispetto al piano di sviluppo
Data di consegna: data o incremento in cui è previsto o avvenuto il rilascio
Gli uses case non servono per progettare, descrivere procedure e partizionare il processo all’interno del
sistema ma servono a discutere e definire i requisiti funzionali, definire le interfacce di un sistema, a
partizionare le funzionalità. Possono essere utili per pianificare e monitorare lo sviluppo pianificando test.
I mock – ups sono prototipi delle interfacce grafiche esposte all’utente con struttura di navigazione delle
pagine e layout delle singole pagine. Si dà particolare enfasi sull’informazione e le funzionalità nella
interazione tra utente e sistema ci astraiamo comunque dai dettagli di design grafico. Favoriscono il
coinvolgimento di utenti ed esperti di dominio, infatti vi troviamo una rappresentazione concreta, a basso
costo di evoluzione, e uno stimolo alla discussione. Si può ritrovare in diverse fasi del progetto con
valutazioni di idee progettuali alternative all’analisi con la validazione dei requisiti fino ai test di
accettazione per il supporto della verifica di completezza.
Activity diagram
Una procedura è una sequenza di attività coordinate svolte in tempi diversi per effetto dell’intrinseca
durata del processo da uno o più attori per separare le competenze o le responsabilità. La procedura ha
uno stato, non si esaurisce in una transazione e si svolge in un arco di tempo, ciascuna attività si svolge nel
contesto creato dalla precedente e opera sui dati e documenti condivisi. C’è frammentazione di
responsabilità, infatti le attività sono svolte da più soggetti e quindi manca una visione globale e un
approccio centralizzato sul flusso degli eventi. Ciascuna procedura è eseguita su più casi specifici. La
procedura interseca punti di cooperazione quali sistemi informativi infrastrutturali o applicativi o sistemi
esterni. C’è cooperazione con sistemi verticali che gestiscano in maniera autonoma informazioni rilevanti
rispetto ai singoli passi della procedura.
Ci sono 3 livelli di capacità nella gestione:
1) processo esplicitato: esiste una rappresentazione documentata e condivisa della procedura. Sostiene
l’organizzazione amministrativa, la formazione e l’allocazione delle risorse umane.
2) processo attuato: esiste un sistema informativo che lo coordina. L’attuazione delle procedure, fornisce
efficienza e garantisce un livello di qualità.
3) processo ottimizzato: rispetto ai requisiti di qualità (efficacia) e sull’uso delle risorse (efficienza).
Del processo esplicitato la rappresentazione può essere espressa attraverso schemi visuali standard: uml,
uses case diagram, activy diagram, class diagram, ci manca da vedere l’activity diagram
L’activity diagram è adatto a descrivere procedure ed è in qualche modo correlato al data flow diagram
dell’analisi strutturata. Adatto all’analisi di un use case favorendo l’identificazione delle operazioni non
degli oggetti a cui allocarle.
Un po’ di simboli:
● :punto di partenza
⨀: activity final node, punto di arrivo, termine dell’esecuzione
: nodo di decisione, si utilizza per una condizione e quindi deve esprime la mutua esclusione
│: fork node o join node attiva i flussi paralleli la sincronizzazione di più flussi paralleli che procedono in un
unico flusso
⨂ : flow final node termina l’esecuzione di un flusso lascia inalterato lo stato eventuale flussi paralleli
: manda il segnale x
: accetta il segnale x
Una subattività
Connettori
Per arrivare all’activity diagram utilizzo il class diagram + attori + use cases. I passi dell’analisi sono 1)
verifica preliminare (il testo scritto) 2) identificazione classe attori e caso d’uso 3) organizzazione di
un’activity diagram
Schede crc (class – responsability – collaboration) si identifica un insieme di classi. Per ciascuna si elencano
le responsabilità salienti e per ciascuna si elencano le collaborazioni necessarie ad assolvere le
responsabilità. Sono utilizzate nell’allocazione e identificazione delle responsabilità.
Design pattern
I) structured programming: tutti i costrutti che posso trovare in un linguaggio come il c
II) structured designed: è l’organizzazione di funzioni, allocazione di responsabilità ai moduli, gerarchia
chiamato chiamante attraverso i moduli. In modo più generale i dati sono decorazioni nelle funzioni
lo structured design è astratto dalla procedura per enfatizzare le relazioni gerarchiche tra le funzioni.
Lo structured design utilizza un artefatto di modellazione: la carta strutturata, basata nelle metriche
di accoppiamento e coesione.
Carta strutturata è un albero fatto di moduli e couples, dove i moduli sono funzioni e i couples sono
parametri legati alla chiamata a funzione, si dividono in data couple (tutto ciò che riguarda i dati
indicati con (immagine)) e i control couple (tutto ciò che può far cambiare il flusso di controllo)
indicati con (immagine). I couple sono associati alle frecce che indicano se in input o output (ricordo
che in c ci si passa l’indirizzo del valore per produrre un side – effect). Poi ci possono essere variabili
condivise, che possono essere variabili globali, database, network.
Poi posso vedere annotazioni procedurali,
ripetizioni (while, for, …) segnati con
guardie (if, switch, …) segnati con
Metrica di accoppiamento: quanto più te devi conoscere del modulo x per modificare in modo sicuro
il modulo y, è una metrica di anti qualità infatti ostacola il mantenimento e l’evoluzione separata dei
moduli. Spinge verso l’integramento dei moduli, partizionamento topologico anche conosciuto come
refactoring. L’accoppiamento può derivare da vari fattori:
1) chiamata a funzione: dipende dalla complessità dell’interfaccia e si passa control couple,
soprattutto dal chiamante al chiamato.
2) aree comuni: tipo un database
3) riferimenti topologici: non esplicitamente supportati in c
4) accoppiamento: è più alto prima il tempo in cui si fa. Esempio: compilazione, link, run time
percorrendo questi 3 all’inverso ho che l’accoppiamento cresce, ossia da run time a compilazione.
L’accoppiamento è transitivo e le interfacce potrebbero servire per disaccoppiare.
Metrica di coesione: quanto due responsabilità allocate allo stesso modulo sono coesive l’un l’altra
(includendo le guardie sotto il modulo). È una metrica di qualità: 1) responsabilità coesive
evolveranno insieme e saranno modificate dalle persone con domini omogenei 2) evita
mantenimento parziale 3) riduce il numero di ragioni per modificare un modulo 4) promuove il riuso.
C’è un indice di livelli di coesione: 0) coincidenza: quando due righe di codice che appaiono
frequentemente chiuse una nell’altra 1) logico: operazioni simili 3) temporali: operazioni eseguite a
istanti chiusi di tempo 5) procedurali: step legati insieme dalla struttura della procedura ed alcuni
criteri virtuali di coesione 7) comunicazionali: due moduli operano nello stesso data element 9)
sequenziali: due moduli dove il primo produce dati per il secondo 10) funzionali: due funzioni che
sono entrambe essenziali per altre funzioni (le chiavi del motorino e il casco) quindi si controlla la
coesione di funzioni allocate a moduli nello stesso sottoalbero e l’accoppiamento attraverso moduli
differenti.
III) structured analysis: ci dice come si arriva a scrivere la carta strutturata. Voglio arrivare ad una
rappresentazione del flusso dell’informazione e delle sue trasformazioni come strumento di
modellazione useremo il data flow diagram e il metodo sarà l’analisi delle trasformazioni e delle
transazioni.
Data flow diagram: documenta flussi e trasformazioni:
data flow: un flusso di informazione scambiata
process: trasformazione di informazione
data store: un’informazione persistente (qualcosa che ci rimane in vita dopo aver tolto la corrente)
terminator: un componente esterno del sistema
Il context diagram: documenta l’interfaccia del sistema, è un unico processo “the system”, è indicato con il
numero 0.
Il levelling scompatto i vari dataflow diagram in unità più specifiche di ogni singolo processo (espansione):
data flow diagram 0: espande il processo 0, data flow diagram i: espande il processo i come rete di processi
i. Poi ad un certo punto non espando più un data flow diagram nel levelling e costituisco il process
specification (o anche detto pspec) che documenta la funzione di un processo non espanso in un data flow
diagram figlio. Associata ad un diagram ho sempre una documentazione. In questo caso avrò il data
dictionary (possono essere struct del c). Il dfd è un grafo e non un albero e si costruisce incrementalmente.
Non definisce un prima o un dopo né ripetizioni. In più non ci rappresenta informazioni sul controllo. I
segnali e le condizioni di attivazione sono trasparenti. Le euristiche per la costruzione: iniziano con il
naming il quale ci dice che ciò a cui non si riesce a dare un nome spesso non rispetta la semantica prevista.
Il processo è un predicato con complemento, il flusso è un sostantivo spesso il complemento oggetto del
processo ed ogni termine deve esprimere un concetto specifico ovvero un concetto atomico, quindi non
utilizzare un termine neutrale. Esempio file, archivio, dato, ….
Il ripartizionamento topologico (refactoring): ci dice di unire i processi in un unico dfd e poi ripartizionarli
per coesione e accoppiamento o per livello semantico omogeneo tenendo conto della regola 7+/-2.
Posso dividere i moduli in afferenti quando trasforma l’informazione dal livello fisico a quello logico e
modulo efferente viceversa. Si parla di rami afferenti (o efferenti) se si ha sequenza di moduli
afferenti/efferenti. Poi la trasformazione centrale è il punto di trasformazione al massimo livello logico
alimentata in ingresso da rami afferenti in uscita da rami efferenti.
Un executive module (main) coordina: un modulo per la trasformazione centrale, un modulo get per ogni
ramo afferente e un modulo put per ogni ramo efferente. Vediamo come è possibile espandere una
trasformazione centrale se ha uno pspec questo definisce la funzione da implementare nel modulo, se ha
un dfd figlio si ripete con la trasformazione centrale come executive del dfd figlio. Il modulo finale del ramo
afferente opera come trasformazione centrale e il modulo get alimenta con i dati ricevuti da un insieme di
modulo get. Il primo modulo del ramo efferente opera come trasformazione centrale e il modulo put
produce i flussi per un insieme di moduli put.
Si va incontro all’iperstrutturazione che può essere un limite nel mantenere visione. Si potrebbe pensare di
avere perdita di efficienza ma non è un gran problema dato che è più facile far andare veloce un sistema
che funziona piuttosto che far funzionare un sistema veloce. È limitato rispetto allo sviluppo ad oggetti
infatti è basato sul concetto di coesione funzionale, rigido rispetto all’evoluzione dei requisiti e scarsamente
orientato al riuso. Dato che è fondamentale per l’analisi delle procedure e flussi è incorporato come parte
dello sviluppo orientato ad oggetti.
Structured vs OODevelopment
Partiamo dalle differenze che ci sono tra OO program e structured program, non è solamente una
questione di sintassi e di semantica. Infatti è molto più interessata alla struttura del codice con
conseguenze positive e negative. Vediamo i tre maggiori problemi: 1) stato, visibilità e accesso
concorrenziale 2) polimorfismo 3) complessità del flusso di controllo
Infatti per la structured program (sp) la carta strutturata fornisce una chiave di visione di come gira il
controllo, infatti era facile poiché la gerarchia realizzava un insieme di funzioni smistate da uno o più centri
di transazione. Invece nel OOP la topologia su cui avviene la computazione è una rete di oggetti, la rete può
svolgere più corrispondenti a più scenari d’uso. Ciascuno corrispondente a un diverso sequence diagram.
Quindi non c’è un modello predefinito che ci dice come gira il controllo. Ciascun oggetto trasferisce il
controllo all’oggetto che detiene la responsabilità delegata e non ad un subordinato e quindi tra i due
spesso esiste una relazione di uno dinamica e non una relazione strutturale. È dimostrato che OOP
favorisce l’evolvibilità, il riuso, la produttività, infatti è capace di assorbire una varietà di responsabilità
coesive sui dati che tratta e in più posso sempre attraverso la rete di oggetti rispondere ad un nuovo caso
d’uso, grazie anche alla possibilità di meccanismi di interazione complessi (come forwarding o inversione di
responsabilità). Quindi è molto più flessibile del SP, dove l’intera gerarchia è finalizzata a svolgere una
funzione con responsabilità coesive in senso funzionale. Tutto questo complica ulteriormente il testing di
OOP. In SP come abbiamo visto la gerarchia è statica, invece in un OOP la topologia degli oggetti è
largamente dipendente da scelte prese al tempo di esecuzione. Infatti gli oggetti sono instanziati in modo
dinamico ed in tipi diversi, in più il prevalere dell’uso della delega rispetto all’eredità esacerba il problema.
Andando avanti noto che in SP le variabili locali terminano il tempo di vita con la funzione e si fa un limitato
utilizzo della direttiva static e delle variabili globali. Invece in OOP un oggetto incapsula attributi che
mantengono il valore anche quando il controllo non risiede in uno dei metodi dell’oggetto. Questo prende il
nome di stato dell’oggetto. In più gli attributi hanno visibilità globale entro la classe. Poi si registra
concorrenza dato che lo stesso oggetto può essere visto da oggetti diversi e anche di tipo diverso. Gli
oggetti in sostanza danno visibilità globale e sufficiente a conoscerne l’indirizzo, in sostanza si realizza un
uso pervasivo di quello che in c sarebbe un puntatore a funzione. (cosa che in c era limitato). In OOP la
stessa operazione può essere implementata polimorficamente, posso infatti avere diverse forme
nell’istanza concreta della classe. Poi si è ridotto l’utilizzo del costrutto if e poi ho diverse configurazioni
delle deleghe (vedi decorator).
A element of object oriented analysis:
L’analisi di OO è come caratterizzare alcuni domini concettuali in termini di classi, oggetti e relazioni. Spesso
è un primo step nel processo per costruire un’applicazione sw. Un modello ha uno scopo pragmatico. Il
diagramma delle classi uml dà una notazione effettiva e pratica ma il problema di fondo è:
1) come identificare le classi e gli oggetti da rappresentare (e i loro attributi espressioni e relazioni)
2) come identificare e allocare responsabilità in accordo ai principi di coesione e disaccoppiamento.
Il processo di object modelling tecnique inizia con un problem statement che dovrebbe essere fornito dal
cliente. Da questo è sviluppato un analysis model che è una coincisa ma non ambigua rappresentazione di
concetti essenziali, riguardanti la comprensione del problem statement. L’analysis model è composta da tre
livelli ortogonali:
1) object model: descrive la struttura statica e l’organizzazione delle classi
2) dinamic model: le funzioni eseguite e quindi come lo stato può evolvere
3) function model: gli algoritmi utilizzati per processare i dati
Vediamo come si divide i sw requirement specification (srs):
1) requisiti funzionali: cosa un sistema deve fare
2) requisiti architetturali: come è la struttura della soluzione
3) requisiti di qualità: la qualità nelle funzioni e la struttura (ISO 91 26).
La separazione delle prospettive funzionali, strutturali, e qualitative è cruciale, infatti è un modo per
ottenere la decomposizione ortogonale della complessità. La prima attenzione dovrebbe essere posta sui
requisiti funzionali. Le prospettive funzionali e strutturali hanno rilevanza diversa in fasi diversi e per ruoli
diversi, la prima (funzionale) è naturale per gli utenti e i clienti e prevale nella fase di analisi. La seconda
(strutturale) è per gli sviluppatori e prevale nella fase di progetto. Quindi dato un testo si scoprono gli
oggetti e le funzioni, poi le classi e gli oggetti sono per convenienza rappresentate in class diagram
accompagnate da testo che provvede ad una non ambigua base per la discussione.
Gli elementi centrali: 1) classi evitano classificazioni gerarchiche facili da aggiungere ma rigidi a permettere
evoluzioni 2) associazioni: caratterizzate da nome e cardinalità 3) attributi: pochi senza lo scopo di
compattezza, spesso rappresentano concetti che serviranno successivamente delegati a classi esterne.
Le operazioni emergono molto più tardi nel processo di analisi e progetto, infatti sono molto più relative
alle specifiche che all’analisi concettuale. Associati strettamente con l’use case e molto difficili da
identificare e allocare. L’identificazione di classi e oggetti è un compito per l’analista e non per il cliente che
oggetto e solitamente un nome come attributo, invece un metodo è un predicato e l’oggetto è il suo
complemento oggetto. Un oggetto può avere attributi e operazioni che elaborano invece una classe è un
tipo di uno o più oggetti e quindi può avere una singola istanza o molte. Ricordiamo che un valore correlato
ad un oggetto è un attributo e non un oggetto. Quindi se per esempio voglio sapere se catalogo è un
oggetto, mi posso domandare se ha uno stato o se questo può essere creato o modificato nelle operazioni.
Se un valore è caricato per essere un oggetto altrimenti è probabilmente un attributo o giusto un valore.
Le operazioni sono allocate agli oggetti che esse modificano. L’identificazione delle responsabilità e la loro
allocazione è il vero cuore dell’analisi OO. Cioè riguarda gli oggetti come entità capaci di assumere
responsabilità anche collaborando con altre entità.
Analysis pattern:
Accountability: ci dice come gestire la complessità di associazioni attraverso classi e oggetti. È applicata
quando una persona o organizzazione è responsabile di un’altra. È una notazione astratta che può
rappresentare molti problemi specifici. È applicabile quando il dominio ha complessità significativa nella
struttura delle relazioni. Un modello diretto è buono da disegnare ma non è scalabile, per seguire
l’evoluzione delle questioni. Un supertipo di una persona, organizzazione o anche di una posizione che
condivide vari attributi.
La parola è generalizzazione e le sue conseguenze sono il buono di riutilizzare attributi ma spesso gli
attributi dei sottotipi prevalgono nella complessità e serve più per delegare future associazioni per
supertipi. Lo schema è un party.
Le organizzazioni spesso hanno composizione gerarchica attraverso diversi tipi di componenti.
Il tipo di componente può essere generalizzato in un supertipo. Il buono è che l’associazione può essere
condivisa con il supertipo, il male è che i vincoli specifici potrebbero rompere la generalizzazione.
Questo può essere superato dividendo in accountability e in accountability type, giustifica il tipo di
relazione attraverso i componenti e può anche avvolgere le regole vincolanti.
Il knowledge level: ci dice qualcosa sulle regole generali di composizione, determina la struttura degli
oggetti per operation level ed evolve raramente.
L’operation level: ci sono istanze concrete di entità e le loro relazioni composte in accordo con gli oggetti
nel knowledge level e dunque evolve giorno per giorno. Nell’esempio sopra evolve con un new party
quando una persona è iscritta con new accountability quando una persona è assegnata ad una entità.
Invece il knowledge level cambia solo con un new accountability type, quando un nuovo concetto di
responsabilità è aggiunto nella struttura di organizzazione o con una nuova regola quando un nuovo vincolo
è introdotto nella composizione.
Utilizzare un party type permette anche al party di evolvere, questo andrà nel knowledge level: il valore di
party type specificherà il tipo di un party. Fare sottotipi di party potrebbe anche essere necessario per
attributi specifici. La consistenza tra il type di party e il valore del proprio party type deve essere ottenuta
attraverso creazioni giudiziose. Le regole nelle composizioni applicabili sono vincoli tra valori dei party types
che possono essere composti dentro un accountability type.
Con un modello di questo tipo posso rappresentare quasi tutto, nella slide 15 ripropongo un modo più
sofisticato del problema.
Observation and measurement: gestisce la complessità di attributi di un oggetto. Rappresentando una
misura come un attributo di un oggetto alla quale misura si riferisce. La misura potrebbe essere complessa
e condivisa da differenti tipi. Questo è il modo comune di fare.
Se parlo di una misura di quantità posso mettere l’attributo in un tipo così mi permette il riuso del tipo e la
separazione del valore dall’unità:
Poi posso delegare l’unità ad una classe esterna:
Rapporto di conversione tra due unità: il numero potrebbe essere assorbito come un attributo di
conversion ratio.
Adesso posso vedere la misura e il tipo di fenomeno, cioè porta fuori dalla misura il fenomeno che gli è
associato. Introduce un knowledge level i cui oggetti identificano i tipi di un sottointeso operation level.
Quindi si può salvare l’età associandolo ad una persona per una istanziazione che è associata ad una istanza
di phenomenom type come può essere il valore dell’età e una istanza di quantity con il valore 48 che
associato ad un’istanza di unit con il valore year.
L’obervation, una misura quantitativa spesso gioca lo stesso ruolo di un observation qualitativo per
esempio 48 età, cittadino europeo. Nel caso qualitativo la quantità è rappresentata da una categoria che
prende i valori dentro a una enumerazione finita. Chiama observation per il supertipo comune.
Vediamo che category observation è legata ad un qualitativo. “Il measurement” ad un quantitativo. In più la
stessa istanza di category può essere applicata a differenti phenomenom type (ma alte temperature non è
come alte pressioni). Posso aggiungere phenomenon a knowledge level per specificare l’intervallo di valori
di category observation. (Questo è associato al phenomenon)
La febbre la posso indicare in modo qualitativo attraverso il category observation associato con una istanza
di phenomenon type con valore alto associato con un phenomenon type con valore temperatura oppure in
maniera quantitativa attraverso il measurement associato con un’istanza di phenomenon type con valore
temperatura e associato ad un’istanza di quantity con valore 38 che è associato ad un’istanza di unit con
valore C°. Adesso posso aggiungere il protocollo, che caratterizza il modo in cui l’osservation è stata presa
cioè come è stata effettuata la misurazione della temperatura. Posso aggiungere anche il time record che
associa uno o più tempi di misura all’observation.
Un observation può essere un’ipotesi, una proiezione nel futuro, un’osservazione attiva: questo può essere
rifiutato da un altro observation.
Il ciclo di vita dei sistemi sw:
Un prodotto è un artefatto costruito nel corso dello sviluppo, ad esempio un programma eseguibile. Il
processo è un insieme organizzato di attività che conducono alla creazione del prodotto e quindi che ha a
che fare con come si fanno le cose più che con le cose.
Waterfall lifecycle (royce 1970) identifica attività specializzate separando competenze e ruoli. Si divide in:
1) studio di fattibilità: identificazione del problema e delle soluzioni. Si discute se il software si fa o si
compra
2) analisi e specifica dei requisiti: si produce il documento dei requisiti (srs), produco i class diagram per
descrivere il prodotto e gli use case per i concetti
3) progettazione: si definisce la struttura cioè l’architettura di sviluppo, l’architettura sw e il progetto di
dettaglio
4) implementazione e test di unità: si decidono i concetti rispetto al linguaggio cioè come mettere le classi e
i design pattern
5) integrazione e test di sistema: si integrano le varie parti che le costituiscono e si testa il sistema completo
6) deployment: si preparano i siti, faccio installazione e formazione sul nuovo software
7) manutenzione: può essere evoluta sviluppando funzionalità o sennò gestisco gli errori che potrebbero
avvenire
Nella separazione dei ruoli ciascuno sa fare una parte. Il modello a cascata consolida questa separazione
con artefatti intermedi, infatti al passaggio tra le fasi è passata la documentazione. Nel 2000 si è capito che
un progettista non può fare un lavoro separato completamente da un’analista, la divisione dei ruoli diventa
così un limite. Si arriva così all’evoluzione nel:
Modello a V: è molto utilizzato adesso per i sistemi industriali (difesa, avionica, ferrovie). Si riorganizza il
modello waterfall partendo da una visione d’insieme scendendo fino ad una visione di unità per poi iniziare
a risalire riallargando la visione iniziando a fare test e integrazione da lì in poi. Ho due dimensioni: 1) in
verticale: sistema/componente 2) in orizzontale: sviluppo/verifica. C’è feedback invece che tra livello
successivi tra livelli corrispondenti.
Sd1: analisi dei requisiti del sistema, cioè ci sono dei requisiti contestuali che quando si va a fare il test
devono essere soddisfatti (come una data velocità di rotazione di un carro armato)
Sd2: sistem design: produce architetture di sistema unisce fondamentalmente le parti elettroniche,
meccaniche, informatiche, cioè scompone in vari sottoparti da chi fa l’analisi di immagine a chi gestisce i
sensori e così via
Sd3: sw/hw requirements analysis:ognuno fa l’analisi dei requisiti per il suo singolo ambito, informatica lo
fa per i singoli pezzettini di software.
Sd4 – sw: preliminary software design: si fa design preliminare del software
Sd5 – sw: detailed software design: faccio design in modo molto più dettagliato delle parti sw a questo
punto mi sono ridotto a guardare l’unità, d’ora in poi inizia ad implementare e fare testing, mandare
feedback di verifica dall’altra parte
Sd6 – sw: sw implementation: faccio implementazione di ogni piccola parte e la testo
Sd7 – sw: sw integration: faccio integrazione delle varie unità
Sd8: system integration
Sd9: transition to utilization. Lo standard è mill – std 498 era nato per la difesa militare statunitense poi ha
assorbito in ambito civile. Richiede la compilazione di un insieme di 22 documenti che si dividono in vari
settori, caratterizzati per fasi. Ognuno di essi mi elenca i requisiti
Srs: per ogni sottosistema produce l’srs
Irs: identifico cosa i vari sottosistemi espongono dato che vanno integrati con altri sottosistemi
Poi ci sono quelli relativi al design:
ssdd: è la documentazione sulla progettazione del sottosistema.
Sdd: è la documentazione solo sulla parte software dei miei sottosistema
Poi c’è quello per il test e la qualifica:
Stp: sw test plain
Std: sw test description
Str: sw test report
Poi ci sono i manuali di utenti, i manuali di supporto. Vediamo cosa sono i requisiti di qualità di un sw, cioè
gli attributi in cui sono offerte le funzioni. Le standard ISO 91 26 divide le qualità dell’sw in sei
caratteristiche:
1) functionaly: se contiene le funzioni richieste più quelle intrinseche dell’oggetto (per esempio nel
telefono è intrinseca l’operazione chiamare)
2) efficiency: è la velocità diviso le risorse
3) maintability: quanto è facile da modificare
4) portability: quanto è facile trasferire un sw in un altro ambiente
5) reability: come è riutilizzabile il software
6) usabilità: quanto è facile da utilizzare
Questo standard oltre a dividere le qualità in sei caratteristiche le divide in 27 sottoclassi, con intento di
completezza e indipendenza.
Il merito del waterfall lifecycle è stato quello di essere stato il primo ad identificare attività specializzate.
Questo modulo piace al corporate management in quanto semplifica la fase di contratto e visita di gestione
prestandosi bene a prevedere una successione di artefatti che documentano l’avanzamento dello sviluppo.
Ma non piace al team degli sviluppatori, infatti non rappresenta i bisogni dell’engineering view, infatti porta
al congelamento dei requisiti e delle scelte del progetto con un innaturale completamento delle fasi. È
scarsamente flessibile rispetto all’evoluzione dei requisiti e alla comprensione incrementale del progetto
infatti non si può richiedere che l’analisi come qualunque altra fase possa terminare in un dato giorno.
Chiedere una fase prima potrà creare problemi dopo, in più si deve considerare per esempio che ci sono
aspetti di cui si deve fare l’analisi i momenti successivi dello sviluppo.
Un modello a spirale (bohem 1988)
Ogni iterazione percorre regioni di attività comuni producendo un prodotto di maggiore rilievo è
un’evoluzione del waterfall che include la gestione del processo.
Punta inizialmente ad elminare quello che potrebbe fallire dopo attaccando prima le cose più rischiose (se
anche elimino qualcosa che dopo non fallisce non elimino il problema) quindi punto a scegliere l’alternativa
rispetto a quella meno rischiosa. La lunghezza della spirale è proporzionale al tempo che sto impiegando,
invece l’area della sua spirale sono i soldi spesi. Quindi all’inizio della spirale conviene fare molti giri che
corrispondono a molto tempo ma a pochi costi con l’idea che quando inizio ad aprire la spirale inizia ad
avere area maggiore cioè iniziano le grandi spese ma spero di aver trovato la giusta strada. Questa modalità
si chiama risk driven cioè dove prototipizzo cioè dove rilasso qualche punto della qualità.
Il modello interattivo incrementale (mills 1980)
Svolgo parallelizzazione di attività diverse relative a diversi incrementi, quindi rilascio per incrementi
successivi ciascuno in sequenza waterfall e ciascuno operativo (immagine)
I vantaggi sono che gli utenti sono coinvolti per feedback e testing quando verosimilmente un sistema base
è funzionante. Poi posso consolidare il testing sui primi incrementi dando precedenza ai componenti più
critici. Faccio in parallelo le varie operazioni, cioè ho il team di analisi, design, codifica e test per tutte le
operazioni. Tutte le operazioni, questo crea difficoltà contrattuali (una sorta di rivincita del team di sviluppo
sul corporate management).
Unifield process: procede in maniera iterativa con incrementi operativi, incentrato su un’architettura
fissata al primo incremento e pilotato dai casi d’uso e dai fattori di rilascio. In un sistema complesso è
necessario partizionare le responsabilità, cioè separare i ruoli anche in relazione ai diversi livelli e località di
competenza per ridurre la complessità (questo è il problema del partizionamento). Questo partizionamento
non può essere mai perfetto e quindi resta l’accoppiamento, esistono sempre concetti nel modello di
dominio su cui intervengono più casi d’uso allocati ad applicazioni separate. Occorre una architettura di
integrazione che ci permette di disaccoppiare lo sviluppo e la manutenzione (e anche i contratti).
Il CART è un’architettura di integrazione prevalentemente EDA, cioè guidata dagli eventi.
Una executable architecture è un sistema di coordinazione publish e subscrive, anche coinvolta come
mediatore degli eventi. Equivalente architettura del patter observer. I partecipanti sono:
1) publisher: pubblica un evento su un topic del mediatore
2) subscriver: sottoscrive un topic sul mediatore, accetta notifiche dal mediatore
3) broker: accetta eventi e li inoltra a tutti i sottoscrittori del topic
Il bus è implementato con un message oriented middleware (mom).
Le applicazioni possono essere realizzate con tecnologie diverse ciascuna possibilmente supportata da un
messagging library. L’adapter mi serve per convertire i protocolli di interazione. L’applicazioni
infrastrutturali possono estendere il bus.
Le architetture real time task – set sono costituite da un set di task concorrenti, dove ciascuna task rilascia
dei jobs composti da chunks. I chunks sono sincronizzati su risorse (semafori). Il tipo di scheduling è priorità
guidata da azioni preventive. I chunks implementano funzioni c che possono chiamare altre funzioni ma
non modificano l’uso delle risorse. Nell’executable architecture vengono emulati i chunks da una funzione
busy – sleep, l’architettura viene implementata ma manca completamente la funzionalità. Il ciclo di vita è
articolato in 4 fasi ciascuna ha una pietra miliare:
1) avvio -> obiettivi: si fa lo studio degli obiettivi e delimitazione del progetto, cioè si parla di fattibilità,
requisiti essenziali, rischi maggiori, consenso manageriale
2) elaborazione -> architettura: si definiscono l’80% dei casi d’uso, si propone un’architettura di base a si
pianifica la costruzione, valutazione delle risorse necessarie e si definisce il perfezionamento dell’analisi dei
rischi e la definizione di attributi di qualità
3) costruzione -> capacità operativa: si completano i requisiti e il modello di analisi e si guadagnano le
capacità operative iniziali: beta test
4) transizione -> rilascio: in questa fase si formano i siti utenti, i manuali e si fa preparazione e consulenza.
Poi c’è la parte di correzione dei difetti e di adattamento.
Ciascuna fase è articolata in interazioni ciascuna delle quali è organizzata come un sottoprogetto,
tipicamente ci sono più iterazioni nelle fasi di costruzione. La durata per ciascuna iterazione è di 1/2/3 mesi.
Il processo avanza con attività che attraversano fasi e iterazioni: 1) requisiti, analisi, progettazione,
implementazione, test, rilascio 2) gestione, configurazione, infrastruttura di sviluppo. Dal grafico si nota un
diverso peso nelle diverse fasi per il flusso di lavoro, dove nelle prime iterazioni prevalgono i requisiti, nelle
ultime prevale il test. Quindi l’UP è un metaprocesso istanziato in ogni progetto in modo più o meno
cerimonioso per la pianificazione specifica di artefatti intermedi, ruoli, strumenti di sviluppo, discipline di
archiviazione, attività di gestione.
Extreme programming (back 1999)
È un processo agile e adatto al cambiamento poiché non c’è da perdersi in ogni passo dietro l’infinita
documentazione. È il passo estremo di supremazia del team di sviluppo sul corporate management. I
principi su cui si basa
1) rapid feedback: scrive un feedback rapido dell’utente su come è stato compreso il problema
2) assium semplicity: si deve mantenere le cose il più semplice possibile dal scrivere un pezzo di codice
all’utilizzare i design pattern
3) cambio incrementale: si migra tramite tante soluzioni intermedie con tante piccole variazioni
4) embrancing change: cioè abbraccia il cambiamento infatti non siamo ingegneri civili che dato il progetto
rimane quello ma possiamo cambiarlo e adattarlo alle modifiche
5) quality work: il lavoro deve essere di qualità quindi deve essere veloce ma anche funzionare bene ed
essere modificabile
I valori su cui si basa è la comunicazione tra gli sviluppatori e utente e il coraggio, deve cioè provare tutto se
uno è indeciso se una cosa funziona o no la prova lo stesso.
In teoria in una settimana dovrei fare tutti i colori cioè produco gli use case diagram preliminari: ne prendo
uno e lo analizzo, progetto, implemento e testo. Le conseguenze di questo modo di fare sono:
1) evitare di prevedere: evitando il rischio di fare overdesign con funzionalità ad oggi inutili
2) evitare separazione dei ruoli: ciascun programmatore combina coding testing listening design. Quindi si
ha una riduzione della specializzazione dei compiti con comunicazione e feedback rapido dallo sviluppatore,
dall’utente e dai test
3) si evita la produzione di semilavorati non strettamente necessari: sono molto usati gli use case diagram e
poco il resto. Si fa nuovo codice e test. La documentazione quindi sparisce data la vicinanza e condivisione
del lavoro.
4) il processo è controllato attraverso pratiche più che documenti
Le pratiche con cui si lavora in XP sono:
1) on site costumer: i committenti partecipano operando in sito nel corso di tutto lo sviluppo
2) planning game: l’utente definisce i requisiti attraverso storie, cioè racconta le funzionalità che vorrebbe
avere. Questi non sono casi d’uso ma è questo che vorrebbe avere. A questo punto gli sviluppatori stimano
il costo delle storia. Gli tenti scelgono le storie realizzate nel prossimo incremento secondo un
compromesso tra costo realizzato e tempo di rilascio. Gli sviluppatori scompongono i task con le storie
scelte e scelgono i task.
3) rilasci piccoli: il sistema è in produzione entro breve, un mese. I rilasci operativi sono ravvicinati ogni 2/3
settimane.
4) opersource: si fa bene a stare in spazi convidisi
5) integrazioni continue: il codice viene continuamente integrato in una visione comune
6) priorità collettiva: il codice è di tutti e tutti lo possono integrare
7) test: si dividono i due categorie: quelli di unità fatti dal programmatore perché sa dove si trovano le
classi che si possono rompere, invece quelle funzionali sono fatti dall’utente perché lui sa quello che vuole.
Il codice è integrato si passa gli unit test, invece il rilascio è chiuso quando passa i test funzionali
8) metafora: c’è una visione condivisa e coincisa degli obiettivi del progetto
9) simple design: modificare la struttura senza modificare il comportamento
10) coding standard: per aumentare la capacità autodocumentale del codice
11) programmare a coppie: le coppie sono formate di task in task da un fattore di continuità e
omogeneizzazione del team. Ovviamente il tutto può avere problemi di applicabilità dato il: a) rischio della
stima dei costi b) richiede la cooperazione degli utenti c) il contratto è difficile d) dimensione limitata del
team.
Le condizioni di migliore funzionamento sono in casi di progetti di breve durata (internet age) o processi
con requisiti instabili (per l’approccio esplorativo o come progetti di ricerca). La teoria classica di bohem
1976 ci dice che il costo di una modifica cresce esponenzialmente con l’avanzamento. Poi siamo arrivati al
claim di XP del 2000 che sostiene che grazie alle tecnologie di testing, di refactoring di versioning e grazie
all’object orientation si può dire il costo di una modifica cresce in modo logaritmico con l’avanzamento.