Introduzione ai design pattern Cosa sono I design pattern



Scaricare 445 b.
02.01.2018
Dimensione del file445 b.


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

  • 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 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+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); }


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



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


Abstract Factory descritto in UML



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.


UML per Flyweight



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



UML



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



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


UML



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


UML



Documentazione UML del pattern Proxy



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







©astratto.info 2017
invia messaggio

    Pagina principale