430 likes | 594 Views
Introduzione ai design pattern. 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
E N D
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
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
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
Poly SparsePoly DensePoly Poly Astratta e impl. multiple • Soluzione: definire una classe Poly e definire DensePoly e SparsePoly come sue estensioni (pure) 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)
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!
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.
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+bviene 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);}
Alternativa: Factory Class • A volte e' preferibile che il metodo statico sia in una classe a parte • Es. public class FabbricaDiPolypublic 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
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
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
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
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 }
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
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.
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 }
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
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
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); } }
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; }
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; } }
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); } ... }
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
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
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
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!
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()); ...
“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
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
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
Documentazione UML del pattern Proxy : Client : Proxy : Server 1: request( ) 2: preProcess( ) 3: 4: request( ) 5: 6: postProcess( ) Some private processing operations 7:
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
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”
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
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
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.
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;} }
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...