Introduzione ai design pattern
1
Cosa sono i design pattern
• I problemi incontrati nello sviluppare grossi progetti
software sono spesso ricorrenti e prevedibili.
• I design pattern sono schemi utilizzabili nel progetto di un
sistema
• Permettono quindi di non inventare da capo soluzioni ai
problemi gia` risolti, ma di utilizzare dei “mattoni” di
provata efficacia
• Inoltre, un bravo progettista sa riconoscerli nella
documentazione o direttamente nel codice, e utilizzarli per
comprendere in fretta i programmi scritti da altri
– forniscono quindi un vocabolario comune che facilita la
comunicazione tra progettisti
2
Design pattern nella libreria Java
• I pattern sono utilizzati pervasivamente dalle
classi standard di Java, e sono alla base della
progettazione orientata agli oggetti
– Es. Iterator: fornisce un modo efficiente e uniforme
per accedere a elementi di collezioni
• Altri esempi presentati in queste slide:
– Abstract Factory, Singleton, Flyweight, State,
Strategy, Proxy, Adaptor e Decorator
3
Abstract Pattern
• Costruire implementazioni multiple di una
stessa classe
• Es. Poly densi e sparsi
– DensePoly: una implementazione di Poly adatta al caso in cui ci sono
pochi coefficienti nulli (ad es. quella vista per i Poly con un array per tutti
i coefficienti);
– SparsePoly: una diversa implementazione, efficiente quando molti
coefficienti sono nulli (es. lista a puntatori, in cui ogni nodo memorizza il
coeff. e il grado di ogni termine !=0).
• Poi però se Dense e Sparse sono tipi distinti dovremmo definire codice
diverso per ogni polinomio:
public static void DensePoly derivata (DensePoly p) …
public static void SparsePoly derivata (SparsePoly p) …
ma differenza fra Dense e Sparse è solo implementativa
4
Poly Astratta e impl. multiple
•
Soluzione: definire una classe Poly e definire DensePoly e SparsePoly come sue estensioni
(pure)
Poly
DensePoly
SparsePoly
Utilizzatore di Poly “vede” solo i metodi definiti in Poly.
//@ ensures (* \result == derivata di p *);
public static Poly derivata(Poly p)
Non importa se a runtime p sarà un DensePoly o uno SparsePoly.
•
Poly non contiene un rep (perche’ non vi molto in comune fra le due implementazioni):
saranno sottoclassi a stabilire modalità di memorizzazione
•
Quindi Poly deve diventare astratta: non è possibile fare add, ecc. senza il rep. Gerarchia di
tipi può essere utilizzata per fornire più implementazioni dello stesso tipo
•
Il tipo da implementare è di solito descritto con interfaccia (se nessuna operazione è
implementabile) o classe astratta (se alcune operazioni sono implementabili)
5
Creazione di oggetti?
• Il codice di un programma orientato agli oggetti non
dipende dalla precisa classe cui appartiene un certo
oggetto. I programmi richiedono a un oggetto solo il rispetto
del “contratto” corrispondente alla sua specifica (il suo tipo)
– Limitare le dipendenze dalle classi è desiderabile perché permette
di sostituire un’implementazione con un’altra. es si può usare Poly e
poi se si passa una DensePoly o una SparsePoly tutto funziona lo
stesso
• Eccezione: le chiamate ai costruttori: il codice utente che
chiama il costruttore di una determinata classe rimane
vincolato a quella classe
• Ci piacerebbe potere lasciare alla classe Poly stessa la scelta se il
tipo da costruire e' uno SparsePoly o un DensePoly!
6
Factory Method
• La soluzione è nascondere la creazione in un
metodo detto factory: restituisce un oggetto di
una classe senza essere costruttore di quella
classe
– Esempio: il metodo che restituisce l’oggetto
iteratore associato a un contenitore (nella
nomenclatura Liskov, oggetti generatori): e` un
esemplare di una classe che implementa
l’interfaccia Iterator, ma il metodo non e` un
costruttore;
• In Java le chiamate ai costruttori non sono
personalizzabili. Una factory può invece
scegliere la strategia di allocazione.
7
Factory (2)
• Il metodo può creare oggetti di classi diverse
a seconda dei parametri, ma tutti questi
oggetti avranno lo stesso tipo.
– Esempio: un polinomio del tipo axn+b viene
implementato da una classe SparsePoly, mentre il
polinomio generico è un esemplare di DensePoly.
public static Poly createPoly (int[] a) {
int degree = -1, numCoeffs = 0;
for (int n = 0; n < a.length; n++)
if (a[n] != 0){
numCoeffs++; degree = n; }
if ((numCoeffs == 2 && a[0] != 0) || numCoeffs == 1)
return new SparsePoly (degree, a[degree], a[0]);
return new DensePoly (degree, a);
8
}
Alternativa: Factory Class
• A volte e' preferibile che il metodo statico sia in una
classe a parte
• Es. public class FabbricaDiPoly
public static Poly createPoly (int[] a) {...
}
•
Ad es. puo' essere comodo per aggiungere operazioni che influenzano che cosa si vuole
fabbricare o per non consentire la costruzione di oggetti di tipo Poly a chi “vede” solo la
classe Poly
9
Abstract Factory
• La soluzione non è ottimale dal punto di vista
dell'estendibilita': cosa succede se aggiungiamo una
classe PolyMezzoDenso che implementa un Poly
per i casi intermedi ne' densi ne' sparsi?
• Dobbiamo modificare il metodo factory, violando
principio Open/Closed.
• Allora si può usare Abstract Factory
– La Factory Class è astratta: il metodo factory e' astratto
– C'e' un'erede concreta della Factory per ogni classe
concreta dell'implementazione, che implementa il metodo
giusto (FactoryDensePoly, FactorySparsePoly)
– Estendendo la classe Poly con PolyMezzoDenso ci basta
aggiungere una FactoryPolyMezzoDenso
10
Abstract Factory descritto in UML
11
Pattern per Ottimizzazioni comuni
• Alcuni pattern forniscono “trucchi” semplici e
funzionali per velocizzare un programma o ridurne
i requisiti di memoria.
• A volte l’utilizzo di questi pattern non fa parte del
progetto vero e proprio del sistema, ma un
programmatore competente sa riconoscere le
occasioni in cui usarli efficacemente
12
Singleton
• A volte una classe contiene per definizione un solo oggetto
• e.g., una tabella, un archivio in cui si assume che ogni
elemento sia individuato univocamente dal suo
identificatore (quindi se ci fossero piu` tabelle non si
avrebbe questa garanzia di unicità)
• Usare una normale classe con soli metodi statici non
assicura che esista un solo esemplare della classe, se viene
reso visibile il costruttore
• In una classe Singleton il costruttore e` protetto o privato
• Un metodo statico, o una factory, forniscono l’accesso alla
sola copia dell’oggetto
13
Singleton pattern: il tipico codice
public class SingletonClass {
private static SingletonClass s; //the single instance
public static SingletonClass getObject(){
//build the unique object only if it does not exist already
if (s == null) s = new SingletonClass();
return s;
}
private SingletonClass() { … } // the constructor
// other methods
}
14
Flyweight
• Quando molti oggetti identici (e immutabili) vengono
utilizzati contemporaneamente, e` utile costruire solo un
oggetto per ogni “classe di equivalenza di oggetti identici”
– gli oggetti condivisi vengono chiamati flyweight (pesi mosca)
perche` spesso sono molto piccoli
• Questo pattern va ovviamente usato solo se il numero di
oggetti condivisi e` molto elevato
• Gli oggetti flyweight devono essere immutabili per evitare
problemi di aliasing
15
Flyweight: implementazione del pattern
• Occorre una tabella per memorizzare gli oggetti flyweight
quando vengono creati
• Non si possono usare i costruttori
– un costruttore costruisce sempre una nuova istanza!
– naturale usare una factory class per creare gli oggetti;
• la factory deve controllare se l’oggetto richiesto esiste già nella
tabella prima di crearlo; se non esiste, chiama un costruttore
(privato!), altrimenti restituisce un reference all’oggetto esistente.
• Se necessario, occorre rimuovere gli oggetti dalla tabella
quando non sono più utilizzati
• Efficiente usare questo pattern se c’è un alto grado di
condivisione degli oggetti
– si risparmia memoria
– non si perde tempo a inizializzare oggetti duplicati
– si può usare == per il confronto al posto di equals.
16
UML per Flyweight
17
Esempio di pattern flyweight
classe Word per rappresentare parole immutabili in applicazioni di
elaborazione testi
Public class Word {
//OVERVIEW: Words are strings that provide
//methods to produce them in various forms; words are immutable; for
// each unique string there is at most one word
private static Hashtable t; //maps strings to words
public static makeWord(String s) //factory: returns the word for string s
private Word(String s) //constructor of the unique word for string s
public String mapWord(Context c)
//returns the string corresponding to this in the form
// suitable for context c
// other word methods
}
18
State
• A volte si vuole usare un'implementazione diversa dello stesso oggetto
durante la sua vita
– per esempio, una classe vettore può usare una rappresentazione diversa a
seconda del numero degli elementi. Se si usa una sola classe il codice
degli oggetti mutabili può diventare assai complicato e pieno di condizionali
• Razionalizzazione della struttura del codice: gli oggetti cambiano
configurazione a seconda dello stato in cui si trovano. Il pattern State
introduce un ulteriore strato tra il tipo implementato e
l’implementazione
– a un unico tipo si fanno corrispondere piu` classi che lo implementano, e
che corrispondono a diversi stati in cui possono trovarsi gli esemplari del
tipo
– nel corso della vita dell’oggetto, possono essere utilizzate diverse
implementazioni senza che l’utente se ne accorga
19
State (2)
Implementazione del pattern
• Si crea un’interfaccia o una classe astratta che
rappresenta le parti dell’oggetto che possono essere
sostituite nel corso della vita dell’oggetto
• Ciascuna delle possibili rappresentazioni (stati)
diventa un’implementazione dell’interfaccia o un
erede della classe astratta
• La classe principale conterrà il codice per scegliere la
rappresentazione più adatta e per delegare
l’implementazione alla sottoclasse piu`appropriata
per lo stato dell’oggetto
20
Esempio di State
Classe BoolSet, analogo dell’Intset : un insieme di boolean che
cambia implementazione a seconda del numero di elementi: si
usano due classi SmallBoolSet e BigBoolSet a seconda della
cardinalità dell’insieme
interface BoolSetState {
public boolean get (int n)
throws IndexOutOfBoundsException;
public BoolSetState set (int n, boolean val)
throws IndexOutOfBoundsException;
}
public class BoolSet {
BoolSetState s;
public BoolSet () { BoolSetState = new SmallBoolSet (); }
public final boolean get (int n)
throws IndexOutOfBoundsException { return s.get (n); }
public final void set (int n, boolean val)
throws IndexOutOfBoundsException { s = s.set (n, val); }
}
21
Esempio di State (2)
SmallBoolSet usa un singolo long per implementare set
i cui elementi sono tutti minori di 64.
class SmallBoolSet implements BoolSetState {
public static final long MAX_SIZE = 64;
long bitset;
public boolean get (int n)
throws IndexOutOfBoundsException {
if (n < 0)
throw new ArrayIndexOutOfBoundsException(n);
return n < MAX_SIZE && (bitset & (1 << n)) != 0;
}
22
Esempio di State (3)
Se si imposta a 1 un elemento oltre il 64-esimo,
viene creato un BigBoolSet.
public BoolSetState set (int n, boolean val)
throws IndexOutOfBoundsException {
if (n < 0)
throw new ArrayIndexOutOfBoundsException(n);
if (val) {
if (n >= MAX_SIZE)
return new BigBoolSet (this).set (n,
val);
bitset |= (1 << n);
}
else if (n < MAX_SIZE)
bitset &= ~(1 << n);
return this;
}
}
23
Esempio di State (4)
Per la classe BigBoolSet vediamo solo il metodo che
costruisce un BigBoolSet a partire da uno
SmallBoolSet:
class BigBoolSet implements BoolSetState {
...
public BigBoolSet (SmallBoolSet s) {
for (i = 0; i < s.MAX_SIZE; i++)
if (s.get (i))
set (i, true);
}
...
}
24
Procedure come oggetti
• Java non permette di utilizzare come oggetti
le chiamate a un metodo
• Questo, tuttavia, può essere utile per definire
astrazioni altamente generiche ed estendibili
(pluggable)
• L’unico modo di ottenere questo risultato è
definire classi o interfacce molto piccole.
Ci sono esempi nella libreria di classi di Java
– Comparable
– Runnable
– ActionListener
25
Strategy
• Il pattern Strategy fornisce un oggetto che compie
un’operazione precisa, richiesta dall’esterno
– Per esempio, stabilire un ordinamento tra oggetti
• L’operazione è esprimibile con clausole Requires e
Ensures
• Un esempio di questo pattern nell’interfaccia
Comparator di JDK 1.4
26
UML
27
Esempio di Strategy: ordinamento di
oggetti qualunque
•
•
•
Vogliamo ordinare un contenitore di oggetti (p.es. un array)
La procedura di ordinamento è sempre la stessa per tutti i tipi di oggetti
possibili…
vorremmo quindi fare un unico metodo per tutti i tipi. Qualcosa come
public static void sort(Object []s…
//@ensures (* s è ordinato *)
• … ma serve un modo per confrontare gli elementi in s! Object non ha un
metodo per il confronto e quindi occorre definirlo da qualche altra parte
• Idea: aggiungo come argomento al metodo un “oggettino” incaricato del
confronto.
• Per potere rendere il metodo sort applicabile a ogni tipo, l’oggetto sarà di tipo
interfaccia. Quindi:
–
–
–
definisco l'interfaccia Comparator (esiste peraltro in java.util), che definisce sintatticamente il
confronto di due oggetti
fornisco una implementazione di Comparator per il tipo che voglio ordinare (es.
IntegerComparator)
Passo anche un Comparator quando chiamo la procedura per confrontare gli elementi
28
Interface Comparator
interface Comparator { //OVERVIEW: immutabile … …
…public int compare (Object o1, Object o2)
throws ClassCastException,
NullPointerException;
/*@ensures (* se o1 e o2 non sono di tipi confrontabili
@ lancia ClassCastException
@ altrimenti: o1<o2  ret –1
@
o1==o2  ret 0
@
o1>o2  ret 1
}
NB:
interfaccia non è supertipo dei tipi i cui elementi vanno comparati!
29
metodo sort
• Argomento aggiuntivo: un oggetto di tipo
Comparator (uno solo per tutti gli elementi!).
• Esempio da java.util.Arrays:
public static void sort (Object[] a, Comparator c) {
…
if (c.compare(a.[i], a.[j])…
…
}
Es. di uso:
public class AlphabeticComparator implements Comparator{
public int compare(Object o1, Object o2) {
String s1 = (String)o1; String s2 = (String)o2;
return s1.toLowerCase().compareTo( s2.toLowerCase());
}
} ...String[] s = new String[30]; ...
Java.util.Arrays.sort(s, new AlphabeticComparator()); ...
30
“adattare” interfacce diverse: Proxy,
Adaptor e Decorator
• Molto spesso librerie diverse espongono interfacce
diverse… per fare la stessa cosa
– Windows e MacOS sono ambienti grafici incompatibili tra
loro
• Una stessa soluzione si adatta a svariati problemi
– si scrivono nuove classi che impongano una stessa
interfaccia e uno stesso insieme di precondizioni e
postcondizioni
• Gli esemplari delle nuove classi usano un oggetto
interno che contiene la vera implementazione
– esempio del motto “Every problem in computer science can
be solved by adding another level of indirection”
– l’oggetto visibile all’ esterno si chiama oggetto esterno
31
Adaptor
• La strategia delineata nella slide precedente prende il
nome di Adaptor quando l’interfaccia dell’oggetto
interno è diversa da quella dell’oggetto esterno
• L’oggetto esterno e’ l’Adapter, quello interno
l’Adaptee.
– le librerie di classi per l’interfaccia grafica, come AWT o
Swing, non sono altro che enormi raccolte di oggetti Adapter
– in Java, java.io.OutputStreamWriter permette di scrivere
caratteri a 16-bit (Unicode) su di un OutputStream che lavora
per byte
– gli skeleton di RMI mappano su di un protocollo binario i
metodi di un’interfaccia Java
32
UML
33
Proxy
• Quando l’oggetto interposto espone esattamente la
stessa interfaccia dell’oggetto separato, di cui fa le
veci, esso prende il nome di Proxy
– java.util.zip.DeflaterOutputStream comprime
automaticamente i dati scritti
• Scopo del Proxy:posporre o addirittura evitare
l‘istanziazione di oggetti “pesanti”, se non necessaria
– es. gli stub di RMI “sembrano” oggetti locali, ma si occupano
di serializzare i parametri, inviarli in rete, attendere il
risultato, ecc., senza però essere i “veri” oggetti
34
UML
35
Documentazione UML del pattern Proxy
: Client
: Proxy
: Server
1: request( )
2: preProcess( )
3:
4: request( )
5:
6: postProcess( )
Some private processing
operations
7:
36
Decorator
• Altre volte, invece, l’oggetto fornisce funzionalità
aggiuntive: prende allora il nome di Decorator
– java.util.zip.CheckedOutputStream calcola un checksum al
volo e possiede un metodo aggiuntivo per restituirlo
• La libreria di classi di Java (Stream, RMI, interfaccia
grafica) utilizza pesantemente Adaptor, Proxy e
Decorator
37
Conclusione
•
I pattern forniscono un vocabolario comune tra i progettisti, che facilita
la comprensione di un progetto esistente o lo sviluppo di uno nuovo
– Abbiamo visto solo un piccolo insieme di pattern:
– Factory, Singleton, Flyweight, State, Strategy, Proxy, Adaptor, Decorator
•
I pattern migliorano le prestazioni del codice e/o lo rendono più
flessibile
•
Tuttavia, il codice che utilizza i pattern potrebbe risultare più complesso
del necessario: occorre quindi valutare e confrontare costi e benefici
•
Svantaggio potenziale: pattern possono rendere la struttura del codice
piu`complessa del necessario: di volta in volta bisogna decidere se adottare
semplici soluzioni ad hoc o riutilizzare pattern noti
– pericolo di “overdesign”: ricordare i seguenti motti
• “when in doubt, leave it out”
• “keep it simple”
38
Esercizio: collezione di elementi con
somma
• Si implementi il tipo collezione di elementi con
somma (SumSet). Man mano che nuovi elementi
vengono aggiunti o tolti dalla collezione viene
aggiornata la somma degli elementi
• Quindi deve esistere l'operazione di somma per gli
elementi da inserire
• Si utilizzi il pattern Strategy, utilizzando un’
interfaccia Adder che definisce un metodo per la
somma
39
Interfaccia Adder
public interface Adder{ //OVERVIEW … … …
public Object add(Object x, Object y)
throws ClassCastException, NullPointerException;
public Object sub(Object x, Object y)
throws ClassCastException, NullPointerException;
public Object zero();
}
• NB: interfaccia Adder non è supertipo dei tipi i cui elementi vanno
sommati
• Serve, per ogni dato tipo che si voglia inserire nell’insieme a (definire
classi per) creare oggetti con metodi per sommare o sottrarre elementi
di quel tipo
• NB: si paga il prezzo della maggiore flessibilità con una maggior
quantità di definizioni (un nuovo tipo aggiuntivo per ogni tipo di
oggetto da inserire
• Obiettivo (non perdiamolo di vista!): ottenere classe SumSet polimorfa
che non deve essere modificata per inserire nuovi tipi di oggetti
40
Un’implementazione di Adder: PolyAdder
public class PolyAdder implements Adder {
private Poly z: // il Poly zero
public PolyAdder() { z = new Poly();}
public Object add (Object x, Object y)
throws NullPointerException, ClassCastException {
if ( x == null || y == null) throw new NullP….;
return ((Poly) x).add((Poly) y); }
public Object sub (Object x, Object y)
…………… // simile ad add
public Object zero () { return z;}
}
• NB: I metodi di PolyAdder (add e sub) sono distinti e diversi dai metodi
omonimi di Poly: signature diversa. Per inserire oggetti Integer in SumSet
occorrerebbe definire “IntegerAdder” con add e sub, che Integer non
possiede.
41
Classe SumSet (con implementazione parziale)
public class SumSet{ //OVERVIEW … … …
private Vector els; // contiene gli elementi
private Object sum; // contiene la somma
private Adder a; //oggetto per sommare e sottrarrre
public SumSet (Adder p) throws NullPointerException{
els = new Vector(); a = p; sum= p.zero(); }
public void insert (Object x) throws NullP…, ClassCastEx… {
…
… sum = a.add(sum, x);
…
}
public Object getSum(){return sum;}
}
42
Classe SumSet (cont.)
• Ogni oggetto SumSet definito in termini (corredato) di qualche oggetto
Adder
• Elementi di SumSet tutti omogenei
– ma ora tipo degli elementi determinato alla creazione
della collezione dall’oggetto Adder passato al
costruttore: non puo` cambiare
Adder a = new PolyAdder();
SumSet s = new SumSet(a);
s.insert(new Poly(3, 7));
s.insert(new Poly(4, 8));
Poly p = (Poly) s.sum(); // p e` 3x^7+4x^8
• NB: l’oggetto SumSet s può contenere solo oggetti Poly, perché
costruito con un PolyAdder. Verifica però fatta a run-time...
43
Scarica

Introduzione ai design pattern