Ingegneria del Software.pdf

59
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

Transcript of Ingegneria del Software.pdf

Page 1: 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

Page 2: Ingegneria del Software.pdf

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 ( ) { ... }

Page 3: Ingegneria del Software.pdf

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

Page 4: Ingegneria del Software.pdf

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;

}

}

Page 5: Ingegneria del Software.pdf

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

Page 6: Ingegneria del Software.pdf

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.

Page 7: Ingegneria del Software.pdf

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)

Page 8: Ingegneria del Software.pdf

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à.

Page 9: Ingegneria del Software.pdf

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 .

Page 10: Ingegneria del Software.pdf

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);

Page 11: Ingegneria del Software.pdf

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.

Page 12: Ingegneria del Software.pdf

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

Page 13: Ingegneria del Software.pdf

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

Page 14: Ingegneria del Software.pdf

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 è

Page 15: Ingegneria del Software.pdf

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

Page 16: Ingegneria del Software.pdf

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

Page 17: Ingegneria del Software.pdf

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

Page 18: Ingegneria del Software.pdf

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.

Page 19: Ingegneria del Software.pdf

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.

Page 20: Ingegneria del Software.pdf

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.

Page 21: Ingegneria del Software.pdf

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à.

Page 22: Ingegneria del Software.pdf

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

Page 23: Ingegneria del Software.pdf

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.

Page 24: Ingegneria del Software.pdf

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

Page 25: Ingegneria del Software.pdf

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.

Page 26: Ingegneria del Software.pdf

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à.

Page 27: Ingegneria del Software.pdf

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.

Page 28: Ingegneria del Software.pdf

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”.

Page 29: Ingegneria del Software.pdf

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

Page 30: Ingegneria del Software.pdf

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.

Page 31: Ingegneria del Software.pdf

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.

Page 32: Ingegneria del Software.pdf

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.

Page 33: Ingegneria del Software.pdf

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

Page 34: Ingegneria del Software.pdf

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.

Page 35: Ingegneria del Software.pdf

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.

Page 36: Ingegneria del Software.pdf

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?

Page 37: Ingegneria del Software.pdf

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)

Page 38: Ingegneria del Software.pdf

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.

Page 39: Ingegneria del Software.pdf

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.

Page 40: Ingegneria del Software.pdf

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).

Page 41: Ingegneria del Software.pdf

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.

Page 42: Ingegneria del Software.pdf

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

Page 43: Ingegneria del Software.pdf

<< 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.

Page 44: Ingegneria del Software.pdf

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.

Page 45: Ingegneria del Software.pdf

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.

Page 46: Ingegneria del Software.pdf

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

Page 47: Ingegneria del Software.pdf

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.

Page 48: Ingegneria del Software.pdf

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

Page 49: Ingegneria del Software.pdf

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.

Page 50: Ingegneria del Software.pdf

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.

Page 51: Ingegneria del Software.pdf

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:

Page 52: Ingegneria del Software.pdf

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

Page 53: Ingegneria del Software.pdf

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

Page 54: Ingegneria del Software.pdf

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

Page 55: Ingegneria del Software.pdf

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.

Page 56: Ingegneria del Software.pdf

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.

Page 57: Ingegneria del Software.pdf

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.

Page 58: Ingegneria del Software.pdf

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.

Page 59: Ingegneria del Software.pdf

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.