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
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Item 1 - Considerare metodi statici factory invece dei costruttori
Il modo normale per una classe per consentire a un client di ottenere un'istanza di se stessa è fornire un
costruttore pubblico . Vi è un'altra tecnica che dovrebbe essere parte di ogni toolkit di un programmatore .
Una classe può fornire un metodo pubblico statico factory , che è semplicemente un metodo statico che
restituisce un'istanza della classe . Esempio : Questo metodo converte un valore primitivo booleano in un
riferimento a un oggetto booleano:
public static Boolean valueOf ( boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE ;}
Si noti che un metodo statico factory non è lo stesso del modello Factory Method dei Design Patterns. Il
metodo statico factory descritto in questo item non ha rilevanza con quello dei Design Patterns. Una classe
può fornire i propri clienti attraverso metodi statici factory invece di, o in aggiunta a , costruttori . Fornire
un metodo statico factory invece di un pubblico costruttore presenta sia vantaggi che svantaggi .
1) Un vantaggio dei metodi statici factory è che, a differenza costruttori , essi hanno nomi . Se i parametri
di un costruttore non li hanno , di per sé ,descrivono l'oggetto che viene restituito , un metodo statico
factory con un nome ben scelto è più facile da utilizzare e il codice client risultante è più facile da leggere .
2) Un secondo vantaggio di metodi statici factory è che, a differenza costruttori , essi non sono tenuti a
creare un nuovo oggetto ogni volta che vengono invocati . Questo consente classi immutabili ( item 15 ) per
utilizzare le istanze precostruite, per evitare di creare oggetti duplicati inutili . Il metodo Boolean.valueOf (
boolean) illustra questa tecnica : essa non crea mai un oggetto. Si può migliorare notevolmente le
prestazioni , se oggetti equivalenti vengono richiesti spesso , soprattutto se sono costosi da creare .La
capacità dei metodi statici factory di restituire lo stesso oggetto da ripetere nelle invocazioni consente alle
classi di mantenere uno stretto controllo su quali siano le istanze in qualsiasi momento . Le classi che fanno
questo sono dette istanze controllate. Ci sono diversi motivi per scrivere classi di istanza controllata. Il
Controllo di istanza consente a una classe di garantire che si tratta di un singleton ( item 3) o non-
instantiable ( item 4) . Inoltre, permette una classe immutabile ( item 15 ) utile per rendere la garanzia che
non esistono due istanze uguali.
3) Un terzo vantaggio dei metodi statici factory è che, a differenza costruttori , essi possono restituire un
oggetto di qualunque sottotipo del loro tipo di ritorno . Questo vi dà grande flessibilità nella scelta della
classe dell'oggetto restituito . Una applicazione di questa flessibilità è l' API che può restituire oggetti senza
che essi faccino le classi pubbliche . Nascondere le classi di implementazione in questo modo porta a una
API molto compatta . Questa tecnica si presta a interfacce – based framework. (item 18) , dove le interfacce
forniscono tipi di ritorno naturali per i metodi statici factory .Le interfacce non possono disporre di metodi
statici, così per convenzione , i metodi statici factory perun'interfaccia denominata Type sono messi in una
classe non-instantiable ( item 4 ) chiamata Types.
4) Un quarto vantaggio di metodi statici factory è che riducono il livello di dettaglio per creare istanze di tipi
parametrizzati . Purtroppo , è necessario specificare i parametri di tipo quando si richiama il costruttore di
una classe parametrizzata anche se sono evidenti dal contesto . Questo di solito richiede di fornire i
parametri di tipo due volte in rapida successione :
Map<String, List<String>> m =
new HashMap<String, List<String>>();
Lo svantaggio principale di fornire metodi statici factory è solo che classi senza costruttori pubblici o
protetti non possono essere sottoclassate . Lo stesso vale per le classi non pubbliche restituite da factories
statiche pubbliche . Per esempio , è impossibile sottoclassare qualsiasi delle classi di implementazione
convenienti nel Collection Framework. Probabilmente questo può essere una benedizione sotto mentite
spoglie , in quanto incoraggia i programmatori ad usare la composizione invece di eredità ( item 16), che è
un male. Un secondo inconveniente dei metodi statici factory è che essi non sono facilmente distinguibili
da altri metodi statici . Essi non si distinguono, nella documentazione API, nel modo in cui i costruttori lo
fanno e quindi può essere difficile da capire come creare un'istanza di una classe che fornisce metodi statici
factory invece di costruttori . Lo strumento Javadoc potrebbe un giorno attirare l'attenzione su metodi
statici factory . Intanto , si può ridurre questo svantaggio , richiamando l'attenzione ai metodi factory statici
in classe o in commenti d’interfaccie , e aderendo alle convenzioni di denominazione comuni .
In sintesi , metodi statici factory e costruttori pubblici entrambe hanno i loro usi , e vale la pena di capire i
loro meriti . Spesso i metodi factory statici sono preferibili, in modo da evitare per riflesso di fornire
costruttori pubblici.
Item 2 - Applicare la proprietà Singleton con un costruttore privato
Un singleton è semplicemente una classe che viene istanziata esattamente una sola volta. Il Singleton in
genere rappresenta le componenti di un sistema che sono intrinsecamente uniche , come window manager
o il file system. L’esecuzione di una classe singleton può rendere difficile testare i propri clients , in quanto è
impossibile sostituire una implementazione finta per un singleton a meno che implementi un'interfaccia
che sia adatta al suo tipo. Ci sono due modi per implementare singleton. Entrambi sono basati sul
mantenimento del costruttore privato e sull'esportazione di un membro statico pubblico, utile per fornire
un accesso alla sola istanza.
Nel primo approccio, il membro è un campo finale :
/ / Singleton con campo finale pubblico
public class Elvis {
public static final Elvis INSTANCE = new Elvis ( ) ;
private Elvis ( ) { ... }
public void leaveTheBuilding ( ) { ... }
}
Il costruttore privato viene chiamato una sola volta , per inizializzare il campo finale public static
Elvis.INSTANCE . La mancanza di un costruttore pubblico o protetto garantisce un ambiente " monoelvistic”:
esattamente un'istanza Elvis esisterà, una sola volta la classe Elvis viene inizializzata - né più, né meno .
Niente di tutto ciò che un clients non può cambiare, con un avvertimento : un clients privilegiato può
richiamare il costruttore privato riflessivamente con l' aiuto del metodo AccessibleObject.setAccessible . Se
si necessità di difendersi da questo attacco, modificare il costruttore per fargli gettare un un'eccezione se è
chiesto di creare una seconda istanza .
Nel secondo approccio dell’attuazione del singleton , il membro pubblico è un metodo factory static :
/ / Singleton con la factory statica
public class Elvis {
private static final Elvis INSTANCE = new Elvis ( ) ;
private Elvis ( ) { ... }
public static Elvis getInstance ( ) {return INSTANCE ; }
public void leaveTheBuilding ( ) { ... }
Tutte le chiamate verso Elvis.getInstance restituiscono lo stesso riferimento all'oggetto , e nessun altra
istanza Elvis sarà mai creata ( con la stessa avvertenza di cui sopra ) .
Il principale vantaggio dell'approccio campo pubblico (numero 1) è che le dichiarazioni fanno si che la
classe è un singleton : il campo statico pubblico è definitivo , quindi sarà sempre limitato allo stesso
riferimento all'oggetto.
Un vantaggio dell'approccio metodo factory è che ti dà la flessibilità di cambiare idea sul fatto che la classe
dovrebbe essere un singleton senza cambiare la sua API . Il metodo factory restituisce l'unica istanza , ma
potrebbe essere facilmente modificato per tornare indietro. Un secondo vantaggio, riguarda i tipi generici.
Spesso nessuno di questi vantaggi è rilevante, e l'approccio campo pubblico è più semplice.
Item 3 - Applicare non-instantiability con un costruttore privato
Di tanto in tanto ti consigliamo di scrivere una classe che è solo un raggruppamento di metodi statici e
campi statici . Tali classi hanno acquisito una cattiva reputazione perché alcune persone abusano di loro per
evitare di pensare in termini di oggetti , ma essi hanno usi corretti. Essi possono essere usati per
raggruppare metodi sui valori primitivi o matrici , in mododi java.lang.Math o java.util.Arrays , possono
anche essere utilizzati per creare un gruppo statico dimetodi, o per gli oggetti che implementano una
particolare interfaccia , in maniera di java.util.Collections . Infine , possono essere utilizzati raggruppare i
metodi su una classe finale , invece di estendere la classe . Tali classi di utilità non sono stati progettate per
essere istanziate : un'istanza sarebbe priva di senso . In assenza di costruttori espliciti , tuttavia , il
compilatore fornisce un costuttore pubblico , senza parametri, chiamato costruttore predefinito . Per un
utente, questo costruttore è indistinguibile da qualsiasi altro . Il tentativo di imporre non-instantiability
facendo una classe astratta non funziona . La classe può essere sottoclassata e la sottoclasse instanziata .
Inoltre, ciò inganna l'utente facendogli credere la classe è stata progettata per l'ereditarietà (item 17). Vi è,
tuttavia , un semplice “tocco di stile” per assicurare non-instantiability . Un costruttore di default viene
generato solo se una classe non contiene esplicitamente costruttori , quindi una classe può essere fatta
non-instantiable includendo un costruttore privato :
/ / Noninstantiable classe di utilità
public class UtilityClass {
/ / Elimina costruttore predefinito per non-instantiability
Private UtilityClass ( ) {
throw new AssertionError ( ) ;
} ... / / Resto omesso }
Poiché il costruttore esplicito è privato , è inaccessibile al di fuori del classe . L’AssertionError non è
strettamente necessario , ma fornisce assicurazione in caso il costruttore viene accidentalmente richiamato
dall'interno della classe . Garantisce che la classe non verrà mai creata un'istanza in nessun caso . Questo
“tocco di classe” è leggermente contro intuitivo, così come il costruttore viene fornito espressamente così
che non può essere invocato . È pertanto consigliabile includere un commento , come mostrato sopra.
Come effetto collaterale, questo trucchetto impedisce anche alla classe di essere sottoclassata. Tutti i
costruttori devono invocare un costruttore della superclasse , esplicitamente o implicitamente , e una
sottoclasse non avrebbe alcun costruttore della superclasse accessibile da richiamare .
Item 4 - Evitare la creazione di oggetti non necessari
Spesso è opportuno riutilizzare un singolo oggetto invece di creare un nuovo oggetto equivalente ogni volta
che è richiesto. Il riutilizzo può essere sia più veloce e più elegante. Un oggetto può sempre essere
riutilizzato se è immutabile (item 15) . Come esempio estremo di cosa non fare, prendiamo in
considerazione questa dichiarazione :
String s = new String ( " stringette " ) ; / / NON FARE QUESTO !
L'istruzione crea una nuova istanza String ogni volta che viene eseguita, e nessuna di quelle creazioni di
oggetti è necessario. L'argomento al costruttore String ( " stringette " ) è di per sé una istanza String ,
funzionalmente identico a tutte le oggetti creati dal costruttore . Se questo utilizzo si verifica in un ciclo o in
un frequente metodo richiamato, milioni di istanze String possono essere create inutilmente. La versione
migliorata è semplicemente il seguente :
String s = " stringette " ;
Questa versione utilizza una singola istanza String , piuttosto che crearne uno nuova ogni volta che viene
eseguita. Inoltre è garantito che l'oggetto sarà riutilizzata da qualsiasi altro codice in esecuzione nella stessa
macchina virtuale che mantenga la stessa stringa letterale. Spesso è possibile evitare di creare oggetti non
necessari utilizzando metodi factory statici (item 1 ), preferendo costruttori di classi immutabili che
forniscono entrambi. Ad esempio, il metodo factory static Boolean.valueOf ( String ) è quasi sempre
preferibile alla booleano di costruzione ( String) . Il costruttore crea un nuovo oggetto ogni volta che viene
chiamato, mentre il metodo factory statico non viene mai richiesto a farlo. Oltre al riutilizzo di oggetti
immutabili, è anche possibile riutilizzare oggetti mutabili se si sa che non saranno modificati. Qui la
situazione è un po’ più sottile , e molto più comune , esempio di cosa non fare . Si tratta di oggetti Date
mutabili che non vengono mai modificati una volta sono stati calcolati i loro valori . Questi modelli di classe
di un persona hanno un metodo isBabyBoomer che dice se la persona è un "babyboomer ", in altre parole ,
La versione migliorata della classe Person crea calendario , fuso orario , e istanze di data solo una volta ,
quando viene inizializzato , invece di creare loro ogni tempo isBabyBoomer viene richiamato . Ciò si traduce
in significativi miglioramenti delle prestazioni se il metodo viene richiamato frequentemente . Non solo
prestazioni migliorate , ma così lo è anche per la chiarezza . La modifica di boomStart e boomEnd da
variabili locali ai campi static final è chiaro che queste date sono trattati come costanti , rendendo il codice
più comprensibile . Il contrappunto a questo articolo è item 24 sulla copie difensive. In sintesi item 5
riassume: " Non creare un nuovo oggetto quando si deve riutilizzare uno esistente ", mentre Item 39 dice: "
Non riutilizzare un oggetto esistente quando si deve creare uno nuovo “. Nota che la penalità per il riutilizzo
di un oggetto quando la copia difensiva viene chiamata, è di gran lunga superiore aella pena di aver
inutilmente creato un oggetto duplicato . Non riuscendo a fare copie difensive dove necessario possono
portare a bug insidiosi e falle di sicurezza , la creazione di oggetti inutili influisce solo su stile e prestazioni .
Item 18 – Favor (piacere, favore) classi membri statiche sopra non statiche
Una classe nidificata è una classe definita all'interno di un'altra classe. Una classe nidificata dovrebbe
esistere solo per servire la sua classe che la contiene. Se volessi che una classe nidificata fosse utile in
qualche altro contesto, allora dovremmo definire una classe di livello superiore. Ci sono quattro tipi di classi
nidificate:
1) classi membri statiche
2) le classi membri non statiche
3) le classi anonime
4) classi locali
Tutti tranne il primo tipo sono noti come classi interne. Questa voce ti dice quando usare e quale tipo di
classe nidificata e perché. 1) Una classe membro statico è il tipo più semplice di classe nidificata. E’ meglio
pensare a come succede che una classe normale è dichiarata all'interno di un'altra classe e ha l'accesso a
tutti i membri della classe che contiene, anche quelli dichiarati privato. La classe membro statico è un
membro statico della classe che la contiene e obbedisce lo stesso alle regole d’accessibilità come gli altri
membri statici. Se è dichiarata privata, è accessibile solo all'interno della classe contenitrice, e così via.
Sintatticamente, l'unica differenza tra le classi dei membri statici e non statici è che le classi membro
statiche hanno il modificatore static nelle loro dichiarazioni. Nonostante la somiglianza sintattica, questi
due tipi di classi nidificate sono molto diversi. 2) Ogni istanza di una classe membro non statico è
implicitamente associata ad un istanza della sua classe di appartenenza. Se l'istanza di una classe nidificata
può esistere in isolamento da un'istanza della sua classe di inclusione , allora la classe nidificata deve essere
una classe membro statico : è impossibile creare un'istanza di una classe membro non statico senza
un'istanza che la racchiude . L'associazione tra un istanza di classe membro non statico e la sua inclusione
istanza viene stabilita quando il primo viene creato e non può essere modificato successivamente.
Normalmente, l'associazione è stabilita automaticamente richiamando un costruttore non statico della
classe membro all'interno di un metodo dell'istanza classe. Un uso comune di una classe membro non
statico è quello di definire un che permette ad un'istanza della classe esterna di essere vista come un
istanza di una classe indipendente. Per esempio,
// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
... // Bulk of the class omitted
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
...
}
Se si dichiara una classe membro che non richiede l'accesso ad una istanza racchiusa, sempre dovrai metter
il modificatore static nella sua dichiarazione, rendendolo un statico piuttosto che una classe membro non
statico . Se si omette questo modificatore, ogni istanza dovrà avere un riferimento estraneo alla sua istanza
inclusa. Un uso comune delle classi membro statiche private è quello di rappresentare i componenti
dell’oggetto rappresentato dalla loro classe di inclusione. 3) Classi anonime sono differenti a qualsiasi altra
cosa nel linguaggio di programmazione Java. Come ci si aspetterebbe, una classe anonima non ha nome.
Non è un membro della sua classe contenitrice . Piuttosto deve essere dichiarata insieme ad altri membri ,
è contemporaneamente dichiarata e istanziata al punto di utilizzo . Le classi anonime sono ammesse in
qualsiasi punto del codice in cui l'espressione è legale. Le classi anonime hanno istanze incluse se e solo se
si verificano in un contesto non statico. Ma se si verificano in un contesto statico, non possono avere
membri statici. Ci sono molte limitazioni all'applicabilità classi anonime. Non si possono istanziare tranne
nel punto che stanno dichiarate. Non è possibile eseguire test instanceof o fare qualsiasi altra cosa che
richiede di denominare la classe. Non è possibile dichiarare una classe anonima per implementare
interfacce multiple , o per estendere una classe e implementare un'interfaccia allo stesso tempo . I clients
di una classe anonima non possono invocare nessun membro ad eccezione di quelli da cui si eredita il suo
supertipo. Un uso comune delle classi anonime è quello di creare oggetti funzionali (item 21 ) al volo. 4) Le
classi locali sono le meno utilizzate di frequente delle quattro tipi di classi nidificate. La classe locale può
essere dichiarata ovunque, una variabile locale può essere dichiarata e obbedisce al stesse regole. Classi
locali hanno attributi in comune con ciascuno degli altri tipi di classi nidificate. Come classi membro, hanno
nomi e possono essere utilizzate ripetutamente. Come classi anonime, racchiudono istanze solo se sono
definite in un contesto non statico, e non possono contenere membri statici ed inoltre come classi
anonime, dovrebbero essere brevi in modo da non danneggiare la leggibilità. Per ricapitolare, ci sono
quattro diversi tipi di classi nidificate, e ciascuno ha la sua particolarità. Se una classe nidificata deve essere
visibile dall'esterno di un singolo metodo o è troppo lunga per adattarsi all'interno di un metodo, utilizzare
una classe membro. Se ogni istanza della classe membro ha bisogno di un riferimento alla sua istanza
inclusa, la rendo non statica, altrimenti la rendo statica. Supponendo che la classe appartiene all'interno di
un metodo, se è necessario creare istanze di una sola posizione e c'è un tipo preesistente che caratterizza la
classe allora la rendo una classe anonima, in caso contrario faccio un classe locale.
Item 22 - Sostituire i puntatori a funzione con le classi e interfacce (non è presente nel libro solo slide)
C supporta i puntatori a funzione, tipicamente utilizzato per consentire al chiamante di una funzione di
specializzarsi nel suo comportamento passando un puntatore ad una seconda funzione, talvolta indicata
come un callback. Ad esempio l'operazione va ripetuta nella visita di un elenco, il comparatore passato a
quick sort. Tutto ciò risulta una sorta di modello di strategia. Java raggiunge la stessa funzionalità
utilizzando il linguaggio degli oggetti funzionali. Oggetti i cui metodi eseguono operazioni su altri oggetti,
passati esplicitamente
class StringLengthComparator {
public int compare(String s1, String s2) {return
s1.length() - s2.length();}
La classe StringLengthComparator è stateless. Può essere utile un singleton per risparmiare sui costi di
creazione di oggetti inutili (Item 2,4)
class StringLengthComparator {
private StringLengthComparator() { } // private
constructor
public static final StringLengthComparator INSTANCE = //
static final factory
new StringLengthComparator();
Tutto questo può essere utilizzato in un modello di strategia vera e propria. Definisci un’interfaccia( la
strategia del Abstract)
public interface Comparator {public int compare(Object o1,
Object o2); }
Sia StringLengthComparator una possibile implementazione del comparatore (uno dei tanti possibili
ConcreteStrategy)
class StringLengthComparator implements Comparator{
private StringLengthComparator() { } // private
constructor
public static final StringLengthComparator INSTANCE = //
static final factory
new StringLengthComparator();
public int compare(String s1, String s2) {return
s1.length() - s2.length();}
Le classi Concrete strategy sono spesso dichiarate utilizzando le classi anonime (Item 18)
// a statement invokes Arrays.sort passing the
reference to an array of String
// and the constructor of an implementation of
Comparator
// that is defined as a nested local anonymous class
// that defines the implementation for the
Comparator.compare method
Arrays.sort(stringArray, new Comparator() {
// anonymous class local to the constructor invokation
// sets up the concrete strategy for comparison
public int compare(Object o1, Object o2) {
String s1 = (String)o1;
String s2 = (String)o2;
return s1.length() -
s2.length();}
}); // end of the local anonymous class, and of the
Capitolo 7 – Metodi in questo capitolo vengono illustrati diversi aspetti del metodo di progettazione: come
trattare parametri e valori di ritorno, come progettare le firme dei metodi , e come fornire la
documentazione dei metodi. Come gli altri capitoli, questo capitolo si concentra sulla facilità d'uso,
robustezza, e flessibilità.
Item 23 - Parametri di controllo per la validità
La maggior parte dei metodi e costruttori hanno alcune restrizioni su valori i quali possono essere passati
nei loro parametri. Ad esempio, non è raro che i valori di indice deve essere non negativo e riferimenti a
oggetti devono essere non nullo . Si dovrebbe chiaramente documentare tutte le restrizioni e farle
rispettare con controlli all'inizio del il corpo del metodo. Questo è un caso particolare del principio generale
che si dovrebbe tentare di rilevare gli errori il più veloce dopo che si verifichino. Se un valore di un
parametro non valido viene passato ad un metodo e il metodo verifica i suoi parametri prima
dell'esecuzione, fallirà e creerà un appropriata eccezione. Se il metodo non riesce a controllare i suoi
parametri, possono accadere molte cose. Il metodo potrebbe fallire con un'eccezione errata, peggio
ancora, il metodo potrebbe funzionare normalmente, ma calcolare il risultato sbagliato. Peggio di tutto , il
metodo potrebbe funzionare normalmente ma lasciare qualche oggetto in uno stato compromesso,
causando un errori.
Per i metodi pubblici , utilizzare il Javadoc @throw tag per documentare l'eccezione che sarà generata se
una restrizione sui valori dei parametri viene violata tipicamente l’eccezione sarà IllegalArgumentException
e/o IndexOutOfBounds. Un esempio tipico :
/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger.
*
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("Modulus <= 0: " + m);
... // Do the computation
Per i metodi pubblici verificare la validità è necessario fino al chiamato. Per il package privato, potrebbe
anche essere una responsabilità del chiamante.
E ' particolarmente importante verificare la validità dei parametri che non sono utilizzati da un metodo, ma
sono conservati per un uso successivo . Ad esempio, si consideri il metodo factory statico, che prende un
array int e restituisce un elenco vista della matrice. Se un client di questo metodo dovesse passare nullo, il
metodo getterebbe una NullPointerException,. A quel punto, l'origine dell'istanza LIst potrebbe essere
difficile da determinare, potrebbe complicare notevolmente il compito di debugging . Costruttori
rappresentano un caso particolare del principio che si dovrebbe utilizzare per verificare la validità dei
parametri che devono essere riutilizzati per un uso successivo. È critico verificare la validità dei parametri
del costruttore per impedire la costruzione di un oggetto che viola le sue invarianti di classe . Ci sono
eccezioni alla regola che si dovrebbe verificare i parametri di un metodo prima di eseguire il suo calcolo .
Un'importante eccezione è il caso in cui il controllo di validità sarebbe costoso e poco pratico e viene
eseguito il controllo di validità implicitamente nel processo da fare per il calcolo. Ad esempio, si consideri
un metodo che ordina un elenco di oggetti, come Collections.sort ( List ) . Tutte gli oggetti nella lista devono
essere reciprocamente comparabili. Nel processo di ordinamento dell'elenco, ogni oggetto nella lista sarà
confrontato con qualche altro oggetto nella lista. Se l'oggetti non sono simili tra loro, uno di questi
confronti getterà una Class- CastException , che è esattamente ciò che il metodo di ordinamento dovrebbe
fare pertanto sarebbe inutile controllare in anticipo che gli elementi nella lista erano comparabili tra di loro.
Per riassumere, ogni volta che si scrive un metodo o un costruttore , si dovrebbe pensare in merito a ciò
che esiste sulle restrizioni dei suoi parametri . Si dovrebbe documentare queste restrizioni e ad applicarle
con controlli espliciti all'inizio del corpo del metodo. E' importante prendere l'abitudine di fare questo. Il
semplice lavoro che essa comporta sarà ripagato la prima volta che un controllo di validità non riesce.
Item 24 - Creare copie difensive in caso di necessità
Una cosa che rende Java piacevole da usare è che si tratta di un linguaggio sicuro. Questo significa che, in
assenza di metodi nativi è immune da sovraccarichi del buffer , superamenti di matrice, puntatori selvatici,
e altri errori di corruzione della memoria che affliggono pericolosamente linguaggi come C e C + + . In un
linguaggio sicuro, è possibile scrivere classi e sapere con certezza che i loro invarianti rimarranno veri, non
importa cosa accade in qualsiasi altra parte del sistema. Anche in un linguaggio sicuro non sei isolato dalle
altre classi senza che tu non faccia alcuna accortezza. È necessario programmare difensivamente, con il
presupposto che i clients della classe faranno del loro meglio per distruggere i tuoi invarianti. Questo può
effettivamente essere vero se qualcuno cerca di rompere la sicurezza del sistema, ma più probabilmente
l’implementazione delle nostre classi dovranno fare i conti con un comportamento imprevisto derivante da
errori onesti da parte dei programmatori che utilizzano la nostra API. In entrambi i casi, vale la pena di
prendere il tempo di scrivere classi che siano robuste per far fronte ai client mal educati :-)
A prima vista, questa classe può sembrare immutabile e fa rispettare l’invariante. Si tratta, tuttavia, di
violare facilmente questa invariante sfruttando il fatto che la Date è mutevole:
// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // Modifies internals of p!
Per proteggere l'interno di un'istanza Period da questo tipo di attacco, è essenziale fare una copia difensiva
di ciascun parametro mutevole al costruttore e utilizzare le copie come componenti dell'istanza Period al
posto del originali:
// Repaired constructor - makes defensive copies of parameters
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start +" after "+ end);
Con il nuovo costrutto, il precedente attacco non avrà alcun effetto sull'istanza Period. Si noti che le copie
difensive sono fatte prima di controllare la validità dei parametri e il controllo di validità viene eseguito
sulle copie piuttosto che sugli originali. Mentre questo può sembrare innaturale, è necessario. Protegge la
classe contro modifiche ai parametri da parte di un altro thread durante la "finestra di vulnerabilità",
periodo nel quale i parametri sono controllati e il momento in cui vengono copiati. Si noti, inoltre, che non
abbiamo usato il metodo clone di Date per fare le copie difensive, perché Date è non-final, il metodo clone
non è garantito per restituire un oggetto la cui classe è java.util.Date: potrebbe restituire una sottoclasse
non attendibile specificamente progettata per furberie. Tale sottoclasse potrebbe, per esempio, registrare
un riferimento per ogni istanza in un elenco statico privato al momento della sua creazione e consentire al
malintenzionato di accedere a questa lista. Ciò darebbe all'attaccante libero utilizzo su tutte le istanze. Per
evitare questo tipo di attacco, non utilizzare il clone metodo per fare una copia di difesa di un parametro il
cui tipo è sottoclassato da parte di soggetti non attendibili.
// Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // Modifies internals of p!
Per difendersi contro il secondo attacco, basta semplicemente modificare le funzioni di accesso per tornare
ad copie difensive di campi interni modificabili:
// Repaired accessors - make defensive copies of internal fields
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
Con il nuovo costruttore e le nuove funzioni di accesso, Period è veramente immutabile. Non importa
quanto dannoso o incompetente sia un programmatore, non vi è semplicemente alcun modo di violare
l'invariante. Questo è vero perché non c'è alcun modo per qualsiasi classe diversa da Period di guadagnare
l'accesso a uno dei campi modificabili in un'istanza Period. Questi campi sono veramente incapsulati
all'interno dell'oggetto. Le copie difensive dei parametri non sono solo per le classi immutabili. Le copie
difensive possono avere una riduzione delle prestazioni ad esse associate e non è sempre giustificato. Se
una classe si fida di suo chiamante che non modifica un componente interno, allora può essere opportuno
scartare le copie difensive. In sintesi, se una classe ha componenti mutevoli che si ottengono da o di
ritorno da i suoi client, la classe deve copiare difensivamente questi componenti. Se il costo del copia fosse
proibitivo e la classe si fida dei suoi client, che non modificano le componenti impropriamente, la copia
difensiva può essere sostituita da documentazione.
Item 5,12,13,14,15,16 + introil valore tra parentesi accanto al n° dell'item indica il n° corrispondente sul libro.
• Introduzione
Gli idiomi (di cui fanno parte gli “Item” che andremo a studiare) sono regole che contengono pratiche generalmente usate dai programmatori. Sono soluzioni ricorrentiper comuni problemi di programmazione. Mentre i Design Pattern sono di alto livelloe indipendenti dal linguaggio, gli idiomi sono pattern di basso livello per linguaggi specifici. Durante il design si usano Pattern, durante l'implementazione si usano gli idiomi poiché forniscono una più specifica soluzione.
• Item 5 (6): Eliminare riferimenti obsoleti a oggetti
Ci sono puntatori (tipo quando faccio POP nello Stack) che rimangono in vita anche se non li posso più usare. In casi come questo è utile (e a volte necessario) forzare l'utilizzo del Garbage Collector per evitare spreco di memoria. Le perdite di memoria in lingue dotate di Garbage Collector (più propriamente note come trattenute non intenzionali di oggetti) sono insidiose. Se un riferimento a un oggetto viene inavvertitamente mantenuto, non solo è escluso dalla raccolta dei rifiuti quell'oggetto,ma lo sono anche tutti gli oggetti a cui fa riferimento tale oggetto, e così via. Anche se solo pochi riferimenti agli oggetti vengono involontariamente conservati, a molti oggetti può essere impedito di essere ripuliti dal GC, con potenziali grandi effetti sulle prestazioni. La soluzione è semplice e si basa sull'evitare la ritenzione involontaria di un oggetto annullando riferimenti fuori uso: cioè quando so che non dovrò più usare un oggetto pongo il suo riferimento a Null. Quando i programmatori vengono a contatto per la prima volta con questo problema, possono compensare annullando ogni riferimento a un oggetto non appena il programma ha finito di usarlo. Questo non è né necessario né auspicabile, in quanto ingombra il programma inutilmente! Questa pratica, infatti, dovrebbe essere l'eccezione e non la norma, essa risulta necessaria in 3 casi:
1. ogni volta che una classe gestisce la propria memoria; 2. nella gestione della cache, una volta messo un riferimento a un oggetto in una
cache, è facile dimenticare che è lì e lasciarlo nella cache molto tempo dopo l'essere diventato irrilevante;
3. quando si ha a che fare con ascoltatori e callback,se si implementa una API in cui i clienti registrano le callback, ma non annullare la registrazione esplicitamente, si accumuleranno a meno che non si prende qualche precauzione: il modo migliore per garantire che i callback siano rifiuti raccolti prontamente è quello di memorizzarli solo tramite riferimenti deboli (weak pointer).
L'esempio dello Stack sopra citato rientra nel primo caso. Poiché le perdite di memoria tipicamente non manifestano fallimenti evidenti, possono rimanere presenti in un sistema per anni. Esse sono tipicamente scoperte solo
a seguito di un'attenta ispezione del codice o con l'ausilio di uno strumento di debug noto come heap profiler. Pertanto, è molto desiderabile l'imparare ad anticipare i problemi di questo tipo prima che si verifichino e impedire che accada.
• Item 12(13): Ridurre al minimo l'accessibilità delle classi e dei membri
Un modulo ben progettato nasconde tutti i suoi dettagli implementativi, separando nettamente la sua API dalla sua implementazione. Questo concetto, noto come Information hiding o incapsulamento, serve per produrre disaccoppiamento:
1. Facilita lo sviluppo in parallelo e la manutenzione, poiché rende il codice più facile da leggere. Inoltre permette di aumentare le performance potendo lavorare sui moduli che creano problemi senza intaccare la correttezza degli altri;
2. Rende il SW più riutilizzabile: non essendo i moduli molto legati a ciò che li circonda si possono rivelare utili anche in altre circostanze;
3. Mitiga il rischio di costruire grandi sistemi (singoli moduli possono rivelarsi di successo, anche se il sistema per cui sono stati costruiti non lo è).
La regola è semplice: rendere ogni classe o membro il più inaccessibile possibile. In altre parole, utilizzare il livello di accesso più basso possibile coerente con il corretto funzionamento del software che si sta scrivendo. Il controllo degli accessi è un importante strumento per l'information hiding. Per il top-level di classi e interfacce, ci sono solo due possibili livelli di accesso: package-private o public.
1. Package-private: si rendono parte della realizzazione e si può modificare, sostituire o eliminare in una versione successiva senza timore di danneggiare i client esistenti.
2. Public: si è obbligati a sostenerlo per sempre per mantenere la compatibilità.Per i membri (campi, metodi, classi annidate e interfacce annidate) ci sono quattro possibili livelli di accesso:
1. Privato: Il membro è accessibile solo dalla classe di primo livello in cui è dichiarata.
2. Pacchetto-privato: Il membro è accessibile da qualsiasi classe nel pacchetto in cui è dichiarata. Tecnicamente noto come accesso predefinito, questo è il livello di accesso che si ottiene se non viene specificato alcun modificatore di accesso.
3. Protetto: Il membro è accessibile da sottoclassi della classe in cui è dichiarata eda ogni classe del pacchetto in cui è dichiarata.
4. Pubblico: Il membro è accessibile da qualsiasi luogo
Se un metodo esegue l'override di un metodo della super-classe, non gli è permesso diavere un livello di accesso più basso nella sottoclasse di quanto non faccia nella super-classe. Ciò è necessario per garantire che un'istanza della sottoclasse è utilizzabile ovunque sia utilizzabile un'istanza della superclasse. Se si violano questa
regola il compilatore genera un messaggio di errore quando si tenta di compilare la sottoclasse. Nessuna variabile dovrebbe mai essere pubblica: si usano i metodi getter e setter per accedervi in modo controllato. Classi con campi modificabili pubblici nonsono thread-safe. Anche se un campo è final e si riferisce ad un oggetto immutabile, rendendo il campo pubblico si dà la flessibilità per passare ad una nuova rappresentazione interna dei dati in cui il campo non esiste. → Per riassumere, si dovrebbe sempre ridurre l'accessibilità, per quanto possibile. Dopo aver progettato con cura una API pubblica minimale, si dovrebbe evitare a ogniclasse, interfaccia o membro di diventare una parte della API. Con l'eccezione dei campi finali statici pubblici, le classi pubbliche non dovrebbero avere campi pubblici.Assicurarsi che gli oggetti referenziati da campi finali statici pubblici siano immutabili.
Una classe immutabile è semplicemente una classe le cui istanze non possono essere modificate. Ci sono molte buone ragioni per questo: le classi immutabili sono più facili da progettare, implementare e usare di classi mutevoli. Esse sono meno inclini aerrori e sono più sicure. Inoltre gli oggetti immutabili possono essere condivisi liberamente in quanto non richiedono la sincronizzazione. Gli oggetti dovrebbero essere fatti il più possibile immutabili. Oggetti immutabili una volta istanziati non cambiano. Per fare una classe immutabile vanno seguite 5 regole:
1. non esporre metodi che modifichino lo stato dell'oggetto, questo impedisce a sottoclassi imprudenti o malintenzionate di compromettere il comportamento immutabile della classe comportandosi come se lo stato dell'oggetto fosse cambiato;
2. assicurarsi che la classe non possa essere estesa;3. porre tutti i campi final, ciò esprime chiaramente l'intenzione in un modo che
viene applicato dal sistema;4. porre tutti i campi private, questo impedisce ai client di ottenere l'accesso agli
oggetti mutabili riferiti dai campi e di modificare direttamente questi oggetti;5. assicurarsi accesso esclusivo ad ogni componente mutabile.
L'approccio funzionale potrebbe apparire innaturale se non si ha familiarità con esso, ma consente l'immutabilità, che ha molti vantaggi. Oggetti immutabili sono semplici. Un oggetto immutabile può trovarsi in un solo stato, lo stato in cui è stato creato. Se si è sicuri che tutti i costruttori stabiliscono invarianti di classe, allora è garantito che questi invarianti rimarranno veri per tutto il tempo, senza ulteriore sforzo da parte vostra o da parte del programmatore che utilizza la classe. Oggetti mutabili, d'altro canto, possono contenere stati arbitrariamente complessi. Se la documentazione non fornisce una descrizione precisa delle transizioni di stato effettuate con metodi mutatori, può essere difficile o impossibile utilizzare una classe mutevole affidabile.Una classe immutabile può essere rilassata permettendo mutevole un campo che non influenza il comportamento esterno. L'unico vero svantaggio di classi immutabili è
che richiedono un oggetto separato per ogni valore distinto. Se una classe non può essere fatta immutabile, bisogna limitare la sua mutabilità il più possibile. Pertanto, fare ogni campo final a meno che non vi sia un motivo valido per farlo non-final.
• Item 14(16): Preferire la composizione all'eredità
Implementare l'ereditarietà è un buon modo per rendere un codice riutilizzabile. Infatti, oltre ad andare a braccetto con l'Open-Close Principle, l'ereditarietà è sicura all'interno dello stesso package e quando si eredità classi appositamente studiate per essere estese. Però l'ereditarietà può creare non pochi problemi, in particolare si può avere:
1. violazione dell'incapsulamento, una sottoclasse dipende dai dettagli implementativi della superclasse per il suo corretto funzionamento;
2. è pericoloso ereditare da classi concrete al di fuori del proprio package;3. classe base fragile;4. internal not documented detail: classe base e derivata possono essere fatte
bene, ma non specificando alcuni dettagli si possono creare conflitti problematici.
L'eredità si porta dietro delle rigidità. Infatti una classe base, per acquisire nuovi metodi nelle successive versioni deve soddisfare più requisiti: se la classe base cambia devono poterlo fare anche i suoi legami e una sottoclasse deve evolvere di pari passo con la sua super-classe. Per evitare alcuni problemi si potrebbe pensare chesia sicuro estendere una classe se si limita l'aggiunta di nuovi metodi e ci si astiene dal fare override di quelli esistenti. Questo tipo di estensione è molto più sicuro, non è priva di rischi. Per fortuna c'è un modo per evitare tutti i problemi visti: invece di estendere una classe esistente si può dare alla nuova classe un campo privato che fa riferimento a un'istanza della classe esistente. Questa tecnica è chiamata composizione poiché la classe esistente diventa una componente di quella nuova. Ogni metodo istanza nella nuova classe richiama il metodo corrispondente sulla istanza contenuta della classe esistente e restituisce i risultati. Questo è noto come forwarding, e i metodi della nuova classe sono noti come metodi di inoltro. La classe risultante sarà solida, senza dipendenze sui dettagli di implementazione della classe esistente. Anche l'aggiunta di nuovi metodi per la classe esistente non avrà alcun impatto sulla nuova classe. Un metodo pratico per i9mplementare la composizione è creare una classe wrapper: la nuova classe incapsula quella esistente.Anche questi ultimi metodi presentano piccoli problemi, detti Self problem: l'oggetto avvolto non conosce del suo involucro, in uno schema di callback esso passa un riferimento a se stesso, eludendo il wrapper; Non adatto per l'uso in callback framework, in cui gli oggetti passano riferimenti a se stessi ad altri oggetti per invocazioni successive (framework: codice già scritto che lascia dei punti di estensione i quali saranno poi definiti nel caso concreto. Può essere visto come complementare alle librerie, si ha un meccanismo di inversione del controllo). Inoltre si ha un leggero impatto sulle prestazioni nell'uso di forwarding e wrapper, di solito non è un problema. È un po' noioso per scrivere i metodi di forwarding, parzialmente
compensato dal fatto che si deve scrivere solo un costruttore.L'ereditarietà è appropriata solo quando la sottoclasse è davvero un sottotipo sella superclasse! Se si usa ereditarietà dove sarebbe appropriata la composizione si espone inutilmente dettagli di implementazione.→ Per riassumere, l'ereditarietà è potente, ma è problematico perché viola l'incapsulamento. È opportuno solo quando esiste un vero e proprio rapporto di sottotipo tra la sottoclasse e superclasse. Anche allora, l'ereditarietà può portare a fragilità se la sottoclasse è in un pacchetto differente dalla superclasse e la superclasse non è progettato per ereditarietà. Per evitare questa fragilità, utilizzare la composizione e forwarding invece di eredità, soprattutto se esiste un'interfaccia appropriata per attuare una classe wrapper. Non solo sono classi wrapper più robuste di sottoclassi, ma sono anche più potenti.
N.B. Il problema discusso in questo item non si applica alla estensione di interfacce.
• Item 15(17): Progettazione e documenti per eredità o altrimenti vietarla
Per fare ereditarietà è necessario fare le classi base robuste e documentare come fare ad ereditarle. Ogni classe deve documentare con precisione gli effetti dell'override su qualsiasi metodo:
1. documentare il self-use di metodi sottoponibili a override: quali metodi sovra scrivibili (non finali) si invoca, in quale sequenza, e come i risultati di ogni invocazione influenzano la successiva lavorazione.
2. documentare di eventuali circostanze in cui potrebbe invocare un metodo sottoponibile a override (ad esempio, le invocazioni di thread in background o inizializzatori statici).
Per convenzione un metodo che che ne invoca un altro sovra scrivibile contiene una descrizione di questa invocazione alla fine del suo commento documentativo. La descrizione di un metodo che richiama metodi sovra scrivibili inizia con "Questa implementazione" (“This implementation”), in modo da sottolineare che il comportamento documentato può risentire di override.
Esempio: in java.util.AbstractCollection, per public boolean remove(Object o)
Removes a single instance of the specified element from this collection, if it is present (optional operation). More formally, removes an element e such that (o==null ? e==null : o.equals(e)), if thecollection contains one or more such elements. Returns true if the collection contained the specifiedelement (or equivalently, if the collection changed as a result of the call).This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator's remove method. Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection's iterator method does not implement the remove method
Non lascia alcun dubbio che l'override del metodo iteratore influenzerà il comportamento di rimozione, e descrive esattamente come il comportamento dell'Iterator restituito dal metodo iteratore influenzerà il comportamento del metodo
remove. Si noti che, poiché l'incapsulamento viene violato, la documentazione viola ilprincipio secondo il quale una buona la documentazione API descrive quello che un determinato metodo fa e non come lo fa. Per consentire ai programmatori di scrivere sottoclassi efficienti senza eccessivi problemi, una classe può fornire ganci al suo funzionamento interno, sotto forma di metodi protetti giudiziosamente scelti o, in raricasi, di campi protetti. Quali metodi devono essere esposti come protetti? Ogni metodo protetto rappresenta un impegno a un dettaglio implementativo. Ma, un metodo protetto mancante può rendere una classe inutilizzabile per l'eredità. Non si hanno “proiettili magici” (soluzioni perfette). L'unico modo per testare una classe progettata per l'ereditarietà è scrivere sottoclassi. Se si omette un membro protetto cruciale, cercaredi scrivere una sottoclasse renderà l'omissione ovvia. Al contrario, se diverse sottoclassi sono scritte e nessuna utilizza un membro protetto, probabilmente si dovrebbe renderlo privato. L'esperienza dimostra che tre sottoclassi sono in genere sufficienti per testare una classe estensibile. Una o più di queste sottoclassi dovrebbe essere scritta da una persona diversa dall'autore superclasse. È necessario testare la propria classe scrivendo sottoclassi prima di rilasciarla.Ci sono un paio di restrizioni a cui una classe deve obbedire per consentire l'ereditarietà. I costruttori non devono invocare metodi sottoponibili a override, direttamente o indirettamente: Il costruttore della superclasse viene eseguito prima del costruttore della sottoclasse; il metodo prevalente sarà invocato prima che il costruttore della sottoclasse sia eseguito, se il metodo sovrascritto dipende da una qualsiasi inizializzazione eseguita dal costruttore della sottoclasse, allora il metodo non si comporterà come previsto.
Esempio:public class Super {
public Super() {m(); }
public void m() {}
}
final class Sub extends Super {
private final Date date; // Blank final, set by
constructor
Sub() {date = new Date();}
public void m() {System.out.println(date);}
public static void main(String[] args) {Sub s =
new Sub(); s.m(); }
}
il metodo "m" viene richiamato dal costruttore "Super()" prima che il costruttore "Sub()" sia eseguito.La progettazione di una classe per eredità pone dei limiti sostanziali sulla classe. Questa non è una decisione da prendere con leggerezza. Ci sono alcune situazioni in
cui è chiaramente la cosa giusta da fare, come ad esempio le classi astratte, incluse le implementazioni di interfacce (item 18). Ci sono altre situazioni in cui è chiaramente la cosa sbagliata da fare, come le classi immutabili (item 15). La soluzione migliore aquesto problema è di vietare sottoclassi in classi che non sono state progettate e documentate per essere sottoclassate in modo sicuro. Si puòà fare in 2 modi: dichiarare la classe finale oppure fare tutti i costruttori private o package-private e aggiungere static factories pubblicche.Se una classe concreta non implementa un'interfaccia standard, allora si può creare disagi ad alcuni programmatori vietando eredità. Se ritieni che è necessario consentire l'ereditarietà da una classe, un approccio ragionevole è quello di garantire che la classe non richiama uno dei suoi metodi sottoponibili a override e per documentare questo fatto. In altre parole, eliminare il self-use della classe di metodi sottoponibili a override completamente. In tal modo, si creerà una classe che è ragionevolmente sicura nei confronti della sottoclasse. Sovrascrivere un metodo non potrà mai influenzare il comportamento di qualsiasi altro metodo. È possibile eliminare il self-use di una classe di metodi sottoponibili a override meccanicamente, senza modificare il suo comportamento. Spostare il corpo di ciascun metodo sottoponibile a override ad un "metodo di supporto" privato e dfar sì che ogni metodo sottoponibile ad override richiama il suo metodo di supporto privato. Poi sostituire ogni self-use di un metodo sottoponibile a override con una invocazione diretta del metodo di supporto privato del metodo sottoponibile a override.
• Item 16(18): Preferire interfacce alle classi astratte
Interfacce e classi astratte sono molto simili. La differenza più evidente tra i due meccanismi è che le classi astratte sono autorizzati a contenere le implementazioni di alcuni metodi, mentre le interfacce no. In linea di principio si preferisce le interfacce:
1. Le classi astratte sono autorizzate a contenere le implementazioni di alcuni metodi, ma, per implementare una classe astratta, una classe deve ereditare da essa (e Java permette solo l'ereditarietà singola). Mentre una classe che implementa l'interfaccia può essere ovunque nella gerarchia delle classi.
2. Classi esistenti possono essere facilmente adattate a implementare una nuova interfaccia mentre non possono, in generale, essere adattate per estendere una nuova classe astratta. (Se due classi devono implementare la stessa classe astratta, questo dovrebbe essere collocato in alto nella gerarchia dei tipi in modo da essere un antenato comune e tutti i discendenti intermedi dovrebbero implementare la nuova classe astratta pure).
3. Le interfacce sono ideali per fare mixin: un mixin è un tipo che una classe può implementare in aggiunta alla sua "tipo primario" per dichiarare che fornisce un certo comportamento facoltativo. Ad esempio Comparable è un'interfaccia mixin che consente a una classe di dichiarare che le sue istanze sono ordinate rispetto ad altri oggetti tra loro comparabili. Le classi astratte non possono essere utilizzati per definire mixins a causa dell'ereditarietà singola.
4. Le interfacce permettono la costruzione di strutture di tipo non gerarchiche, molti concetti non rientrano ordinatamente in una gerarchia rigida.
5. Le interfacce consentono wrapper class idiom (item 14) per migliorare le funzionalità. Utilizzando le classi astratte per definire i tipi, il programmatore non può scegliere la composizione invece che l'eredità.
È possibile combinare le virtù di interfacce e classi astratte, fornendo lo scheletro implementativo di una classe stratta per le interfacce esportate: superando così l'inconveniente delle interfacce, che non possono fornire un'implementazione parziale. Per convenzione, chiamare AbstractInterface lo scheletro implementativo delle interfacce (ad esempio AbstractCollection, AbstractSet, AbstractList …).Tecnica per simulare l'ereditarietà multipla: la classe che implementa l'interfaccia inoltra invocazioni di metodi di interfaccia a un'istanza contenuta di una classe interna privata che estende lo scheletro implementativo. Strettamente legato al wrapper class idiom, fornisce la maggior parte dei benefici di ereditarietà multipla, evitando le insidie.Utilizzo di classi astratte per definire i tipi che consentono molteplici implementazioni ha un grande vantaggio rispetto all'utilizzo di interfacce: è molto piùfacile far evolvere una classe astratta di un'interfaccia. Se, in una versione successiva,si desidera aggiungere un nuovo metodo di una classe astratta, è sempre possibile aggiungere un metodo concreto contenente un'implementazione predefinita ragionevole. Tutte le implementazioni esistenti della classe astratta forniranno quindi il nuovo metodo. Questo non funziona per le interfacce: se viene aggiunto un nuovo metodo di un'interfaccia, tutte le implementazioni devono essere estese. Mentre se viene aggiunto a una classe astratta, può essere fornita qualche implementazione di default. In tal modo, le classi astratte sono più flessibili delle interfacce che devono essere progettate con cura in modo da non avere bisogno di riaprirle. Infatti una volta rilasciato ed implementato un interfaccia è quasi impossibile cambiarla.→ Per riassumere, l'interfaccia è generalmente il modo migliore per definire un tipo che consente più implementazioni. Un'eccezione a questa regola è il caso in cui la facilità di evoluzione è ritenuta più importante di flessibilità e potenza. In queste circostanze, è necessario utilizzare una classe astratta per definire il tipo, ma solo se si capisce e si può accettare le limitazioni. Se si esporta un interfaccia non banale, si dovrebbe prendere in seria considerazione il fornire una implementazione scheletrica con essa. Infine, è necessario progettare tutte le interfacce pubbliche con la massima cura e testare a fondo scrivendo implementazioni multiple.
Abstract Factory (C)
Intento: Presenta un’interfaccia per la creazione di famiglie di prodotti, in modo tale che il cliente che gli
utilizza non abbia conoscenza delle loro concrete classi. Questo consente:
- Assicurarsi che il cliente crei soltanto prodotti vincolati fra di loro.
- L’utilizzo di diverse famiglie di prodotti da parte dello stesso cliente.
Motivazione: esempio widget factory che produce diversi tipi di widget, che sono composti da
window,scroolbar ecc. il client usa solo astrazioni.
Applicabilità: Il pattern “Abstract Factory” si basa sulla creazione di interfacce per ogni tipo di prodotto. Ci
saranno poi concreti prodotti che implementano queste interfacce, stesse che consentiranno ai clienti di
fare uso dei prodotti. Le famiglie di prodotti saranno create da un oggetto noto come factory. Ogni famiglia
avrà una particolare factory che sarà utilizzata dal Cliente per creare le istanze dei prodotti. Siccome non si
vuole legare al Cliente un tipo specifico di factory da utilizzare, le factory implementeranno una interfaccia
comune che sarà dalla conoscenza del Cliente.
Struttura:
Partecipanti:
AbstractFactory: Dichiara una interfaccia per le operazioni che creano e restituiscono i prodotti. Nella
dichiarazione di ogni metodo, i prodotti restituiti sono dei tipi AbstractProduct.
ConcreteFactory: Implementa l’AbstractFactory, fornendo le operazioni che creano e restituiscono oggetti
corrispondenti a prodotti specifici (ConcreteProduct).
AbstractProduct: Dichiarano le operazioni che caratterizzano i diversi tipi generici di prodotti.
ConcreteProduct: Definiscono i prodotti creati da ogni ConcreteFactory.
Client: Utilizza l’AbstractFactory per rivolgersi alla ConcreteFactory di una famiglia di prodotti. Utilizza i prodotti
tramite la loro interfaccia AbstractProduct.
Conseguenze: isola le classi concrete. Abbiamo che l’abstract factory è senza stato. Facile cambiare il tipo
famiglia oggetto creato, basta cambiare il concrete factory usato. +) abbiamo consistenza tra i prodotti da
usare insieme, gli raggruppiamo in famiglie. - ) Ogni nuovo tipo di prodotto implica di rifare tutte le factory
Osservazioni: Dovuto al fatto che né l’AbstractFactory né gli AbstractProduct implementano operazioni, in
Java diventa più adeguato codificarli come interfacce piuttosto che come classi astratte.
Builder (C)
Intento: Separa la costruzione di un oggetto complesso dalla sua rappresentazione, in modo che lo stesso
processo di costruzione consenta la creazione di diverse rappresentazioni.
Motivazione: Si tratta di un pattern creazionale basato su oggetti e viene utilizzato per creare un oggetto
senza doverne conoscere i suoi dettagli implementativi. Questo pattern consente di utilizzare un Client che
non debba essere a conoscenza dei passi necessari al fine della creazione di un oggetto ma tali passaggi
vengono delegati ad un Director che sa cosa e come fare.
Applicabilità: Il “Builder” pattern propone di separare la “logica del processo di costruzione” dalla
“costruzione stessa”. Per fare ciò si utilizza un oggetto Director, che determina la logica di costruzione del
prodotto, e che invia le istruzioni necessarie ad un oggetto Builder, incaricato della sua realizzazione.
Siccome i prodotti da realizzare sono di diversa natura, ci saranno Builder particolari per ogni tipo di
prodotto, ma soltanto un unico Director, che nel processo di costruzione invocherà i metodi del Builder
scelto secondo il tipo di prodotto desiderato (i Builder dovranno implementare un’interfaccia comune per
consentire al Director di interagire con tutti questi). Potrebbe capitare che per ottenere un prodotto
particolare alcune tappe del processo di costruzione non debbano essere considerate da alcuni Builder (ad
esempio, il Builder che costruisce i “modelli non orientati”, deve trascurare il nome della relazione e il grado
della partecipazione minima).
Struttura:
Partecipanti:
Builder: Dichiara una interfaccia per le operazioni che creano le parti dell’oggetto Product. Implementa il
comportamento default per ogni operazione.
ConcreteBuilder: Forniscono le operazioni concrete dell’interfaccia corrispondente al Builder. Costruiscono e
assemblano le parti del Product. Forniscono un metodo per restituire il Product creato.
Director: Costruisce il Product invocando i metodi dell’interfaccia del Builder. Product: Rappresenta l’oggetto complesso in costruzione. I ConcreteBuilders costruiscono la rappresentazione interna del Product. Include classi che definiscono le parti costituenti del Product.
Conseguenze: consente di cambiare la rappresentazione interna del prodotto: il Builder non conosce la
rappresentazione interna del prodotto che può essere cambiata semplicemente costruendo un nuovo
Builder.
isolamento tra Builder: ogni Builder è indipendente dall’altro pertanto è possibile aumentare la modularità.
controllo accurato del processo di creazione: la creazione avviene step-by-step e questo consente di stabilire
passo dopo passo cosa effettuare.
Osservazioni: La classe astratta Builder dichiara il metodo getModel che i ConcreteBuilders devono
implementare, con il codice necessario per restituire ogni particolare tipo Product. Il tipo di ritorno del
metodo getModel è indicato come Object, dato che a priori non si ha conoscenza della specifica tipologia di
Product. In questo modo si abilita la possibilità di restituire qualunque tipo d’oggetto (perché tutte le classi
Java, in modo diretto o indiretto, sono sottoclassi di Object). Si fa notare che il “method overloading” di Java
non consente modificare la dichiarazione del tipo di valore di ritorno di un metodo di una sottoclasse,
motivo per il quale i ConcreteBuilders devono anche dichiarare Object come valore di ritorno, nei propri
metodi getModel.
Factory Method (C)
Intento: Definisce un’interfaccia per creare oggetti, ma lascia alle sottoclassi la decisione del tipo di classe a
istanziare.
Motivazione: Framework deve poter creare diversi documenti ma non sa il tipo concreto quindi uso il factory
Applicabilità: una classe non può anticipare classi concrete da creare oppure vuole che subclassi decidendo
cosa creare. Le classi delegano la responsabilità a subclassi e vogliono localizzare di chi è delegato. Il pattern
“Factory Method” suggerisce il portare via dal framework la creazione di ogni particolare tipo di Elemento.
Per fare ciò, verrà delegato alle sottoclassi dello Strumento, che specializzano le funzioni di gestione di ogni
tipo di Elemento, il compito di creare le particolari istanze di classi che siano necessarie.
Stuttura:
Partecipanti
Product: Definisce l’interfaccia di tutti gli elementi da utilizzare nell’applicazione.
ConcreteProduct: Implementano i concreti prodotti.
Creator: Dichiara il factory method che restituisce un oggetto della classe Product. Richiama il factory
method per creare i Product.
ConcreteCreator: Redefine il factory method per restituire una istanza di ConcreteProduct.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
rappresenta un gancio alle sotto-classi: tramite il Creator è possibile scegliere quale classe concreta utilizzare
e decidere di cambiarla senza avere nessun impatto verso il Client
consente di collegare gerarchie di classi in modo parallelo: i ConcreteCreator possono collegarsi con i
ConcreteProduct e generare un collegamento parallelo tra gerarchie diverse.
Osservazioni: Si vuole rendere noto che il factory method (newElement) dichiara come tipo da restituire al
punto di chiamata, sia nel Creator (ElementHandler), sia in ogni ConcreteCreator (PlaceHandler e
ConnectorHandler), un oggetto di tipo Product (MapElement), invece dei particolari tipi da produrre (Place e
Connector). Questo è dovuto al fatto che le sottoclassi che redefiniscono un metodo devono esplicitare
lo stesso tipo di ritorno che quello indicato nella dichiarazione del metodo nella superclasse.
Adapter (S)
Intento: Specifica i tipi di oggetti a creare, utilizzando un’istanza prototipo, e crea nuove istanze tramite la
copia di questo prototipo.
Motivazione: Si tratta di un pattern strutturale basato su classi o su oggetti in quanto è possibile ottenere
entrambe le rappresentazioni. Viene utilizzato quando si intende utilizzare un componente software ma
occorre adattare la sua interfaccia per motivi di integrazione con l’applicazione esistente. Questo comporta
la definizione di una nuova interfaccia che deve essere compatibile con quella esistente in modo tale da
consentire la comunicazione con l’interfaccia da “adattare”. Come abbiamo accennato, tale pattern può
essere basato sia su classi che su oggetti pertanto l’instanza della classe da adattare può derivare da
ereditarietà oppure da associazione.
Applicabilità: L’Adapter pattern offre due soluzioni possibili, denominate Class Adapter e Object Adapter,
che si spiegano di seguito:
- Class Adapter: la classe esistente si estende in una sottoclasse che implementa la desiderata
interfaccia. I metodi della sottoclasse mappano le loro operazioni in richieste ai metodi e attributi della classe
di base.
- Object Adapter: si crea una nuova classe che implementa l’interfaccia richiesta, e che
possiede al suo interno un’istanza della classe a riutilizzare. Le operazioni della nuova classe fanno
invocazioni ai metodi dell’oggetto interno.
Struttura:
Per il class adapter
Per Object Adapter
Partecipanti
TargetInterface: Specifica l’interfaccia che il Client utilizza.
Client: Comunica con l’oggetto interessato tramite la TargetInterface.
Adaptee: Implementa una interfaccia che deve essere adattata.
Adapter: Adatta l’interfaccia dell’Adaptee verso la TargetInterface.
Conseguenze: Tale pattern presenta i seguenti vantaggi/svantaggi:
Class Adapter: prevede un rapporto di ereditarietà tra Adapter e Adaptee, in cui Adapter specializza Adaptee,
pertanto non è possibile creare un Adapter che specializzi più Adaptee. Se esiste una gerarchia di Adaptee
occorre creare una gereachia di Adapter.
Object Adapter : prevede un rapporto di associazione tra Adapter e Adaptee, in cui Adapter instanzia
Adaptee, pertanto è possible avere un Adapter associato con più Adaptee.
Osservazioni: La strategia di costruire un Class Adapter è possibile soltanto se l’Adaptee non è stato
dichiarato come final class.
Bridge (S)
Intento: Separa un’astrazione dalla sua implementazione, in modo che entrambe possano variare
indipendentemente.
Motivazione: Si tratta di un pattern strutturale basato su oggetti che viene utilizzato per disaccoppiare dei
componeti software. In questo modo è possibile effettuare uno switch a Run-Time, garantire il
disaccoppiamento, nascondere l’implementazione, estendere la specializzazione delle classi.
Applicabilità: Il “Bridge” pattern suggerisce la separazione dell’astrazione dall’implementazione, in gerarchie
diverse, legando oggetti della seconda a quelli della prima, tramite un relazione di conmposizione.
Struttura:
Partecipanti
Abstraction: Specifica l’interfaccia dell’astrazione. Gestisce un riferimento ad un oggetto Implementor.