Lucidi - UniCam - Computer Science Division

Transcript

Lucidi - UniCam - Computer Science Division
Design Pattern
Rosario Culmone
18/11/2009
UNICAM - p. 1/246
Perchè
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 già 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. Quindi:
● forniscono un vocabolario comune che facilita la comunicazione tra
progettisti;
● svantaggio potenziale: possono rendere la struttura del codice più
complessa del necessario. Di volta in volta bisogna decidere se adottare
semplici soluzioni ad hoc o riutilizzare pattern noti;
● pericolo di "overdesign".
18/11/2009
UNICAM - p. 2/246
Vantaggi
▲
▲
▲
▲
▲
▲
Notevole aumento della capacità di produrre software riutilizzabile;
Si danno allo sviluppatore strumenti utili per la modellazione di nuovi sistemi;
Si aumenta la documentazione e la chiarezza;
Si aumenta la velocità di sviluppo;
Si aumenta la robustezza del software;
Si aumenta la flessibilità e l’eleganza del software.
18/11/2009
UNICAM - p. 3/246
Design pattern
▲
▲
▲
▲
▲
Il termine "pattern" fu introdotto dall’architetto austriaco Christopher
Alexander negli anni ’70 (per la pianificazione di costruzioni in ambienti
urbani).
Nel 1987 Cunningham e Beck adattarono l’idea di Alexander per guidare
programmatori inesperti in Smalltalk.
Erich Gamma, tesi di dottorato, 1988-1991.
Dal 1990 al 1992 la famosa Gang of Four (Gamma, Helm, Johnson,
Vlissides) incominciò la stesura di un catalogo di pattern.
Nel 1995 la Gang of Four pubblicò "Design Patterns - Elements of Reusable
Object-Oriented Software" con la descrizione di 23 pattern.
18/11/2009
UNICAM - p. 4/246
Anti-pattern
La definizione di antipattern è stata coniata dalla "Gang of Four" per indicare i
tipici problemi che incorrono i programmatori nella scrittura del codice. Vi è un
elenco esteso di "trappole" in cui cade il programmatore, eccone alcune:
quando il programmatore prevede un numero elevato di
funzionalità, la maggior parte inutile.
Coltellino svizzero
quando il programmatore presume la correttezza di un codice e non
prevede alcun controllo.
Fede cieca
quando elementi del programma che interagiscono sono posti
a distanza non controllando gli effetti della modifica di una parte sull’altra.
Azione a distanza
quando il programmatore rinuncia ad adattare un modulo
esistente riscrivendolo da capo e inserendo, presumibilmente, errori.
Reinventare la ruota
quando si un uso eccessivo di costrutti per il controllo del flusso
rendendo praticamente illeggibile un codice e propenso agli errori.
Spaghetti code
Secondo gli autori, l’uso dei pattern contribuisce nell’evitare queste trappole.
18/11/2009
UNICAM - p. 5/246
Definizione
Un pattern è l’astrazione di un problema che si verifica nel nostro dominio,
rappresentandone la soluzione in modo che sia possibile riutilizzarla per
numerosi altri contesti (Christopher Alexander).
▲
Descrizione di classi ed oggetti comunicanti adatti a risolvere un problema
progettuale generale in un contesto particolare.
(Gamma, Helm, Johnson, Vlissides, "Design Patterns: Elements of Reusable
Object-Oriented Software", Addison-Wesley).
18/11/2009
UNICAM - p. 6/246
Struttura di un pattern
Nel libro dei GoF ogni pattern è descritto nel seguente modo:
Descrizione: una breve descrizione dell’obiettivo del pattern.
Esempio: si presenta un problema la cui soluzione si ottiene tramite
l’applicazione del pattern.
Descrizione della soluzione offerta dal pattern: si descrive testualmente
l’architettura del pattern e come questa si applica al problema.
Struttura del pattern: diagramma di classi in UML della struttura generica del
pattern.
Applicazione del pattern: offre un diagramma UML delle classi del problema,
presenta l’abbinamento delle classi del problema con le classi che
descrivono la struttura concettuale del pattern, descrive l’implementazione
del codice Java, presenta e commenta gli output dell’esecuzione.
Osservazioni sull’implementazione in Java: presenta gli aspetti particolari
che riguardano l’implementazione del pattern in Java.
18/11/2009
UNICAM - p. 7/246
Proprietà
I pattern:
▲ Costituiscono un vocabolario comune per i progettisti;
▲ Sono una notazione abbreviata per comunicare efficacemente principi
complessi;
▲ Aiutano a documentare l’architettura software;
▲ Catturano parti critiche di un sistema in forma compatta;
▲ Mostrano più di una soluzione;
▲ Descrivono astrazioni software;
▲ NON costituiscono una soluzione precisa di problemi progettuali;
▲ NON risolvono tutti i problemi progettuali;
▲ NON si applicano solo alla progettazione OO, ma anche ad altri domini.
18/11/2009
UNICAM - p. 8/246
Nome
Per identificare un pattern si utilizza un nome.
▲ Il nome del pattern è molto utile per descrivere il problema, la sua soluzione
ed il suo uso;
▲ Esso è composto da una o due parole;
▲ Bisogna cercare di omogeneizzare i vocabolari personali di tutti i colleghi.
18/11/2009
UNICAM - p. 9/246
Problema
Un pattern si applica per risolvere un problema che si presenta nella fase di
modellazione.
▲ Descrive quando applicare un pattern definendo il contesto ed il dominio di
appartenenza;
▲ In generale include la lista di condizioni che devono essere valide per poter
giustificare l’uso di un determinato pattern.
18/11/2009
UNICAM - p. 10/246
Soluzione
Il pattern coinvolge componenti con particolari vincoli.
▲ Descrive gli elementi che verranno usati durante la modellazione;
▲ Descrive le relazioni e le responsabilità degli elementi;
▲ E’ importante capire che la soluzione non rappresenta una specifica
implementazione o caso d’uso ma un modello che si applica a differenti
situazioni.
18/11/2009
UNICAM - p. 11/246
Conseguenze
L’applicazione di un pattern mostra vantaggi e svantaggi.
▲ Raccoglie l’elenco dei tempi e dei risultati;
▲ E’ importante quando si devono prendere decisioni di modellazione;
▲ Descrive varie metriche, i costi ed i tempi in relazione ai benefici che il
pattern introdurrebbe.
18/11/2009
UNICAM - p. 12/246
Scelta
Esistono numerosi pattern più o meno adatti al problema da modellare.
▲ Esistono numerosi cataloghi di pattern;
▲ Solitamente sono descritti attraverso una notazione comune "Design
Language";
▲ E’ importante reperire il pattern adeguato per il proprio specifico dominio;
▲ Considerare come un pattern risolve il problema: ogni pattern affronta il
probelma con una soluzione originale;
▲ Considerare il suo intento: l’obiettivo del programma deve essere lo stesso
del pattern;
▲ Studiare le interazioni tra pattern: i pattern generalmente sono
composizionali;
▲ Considerare come deve variare il progetto: diversi approcci con diversi
pattern.
18/11/2009
UNICAM - p. 13/246
Design pattern nella libreria Java
I pattern sono utilizzati abbondantemente dalle classi standard di Java. Iteraror,
Observer e molti altri sono stati introdotti già dalle prime versioni. I pattern
calzano perfettamente e traggono i maggiori benefici dal polimorfismo e
dall’ereditarietà dei linguaggi di programmazione orientati agli oggetti.
18/11/2009
UNICAM - p. 14/246
Tipi di design pattern
I design patterns possono essere raggruppati secondo il pricipale contesto di
applicazione in:
Pattern di creazione delegati alla gestione della costruzione di oggetti;
Pattern di struttura delegati alla rappresentazione di oggetti;
Pattern di comportamento delegati al comportamento dinamico degli oggetti.
18/11/2009
UNICAM - p. 15/246
Pattern di creazione
Abstract Factory - Crea oggetti appartenenti a famiglie di classi senza
specificare le classi concrete.
Builder - Separa il processo di creazione di un oggetto dalla rappresentazione
definitiva.
Factory Method - Crea oggetti derivanti da diversi tipi di classi.
Prototype - Creazione di oggetti a partire da altri oggetti.
Singleton - Una classe di cui può esistere solo un singolo oggetto.
18/11/2009
UNICAM - p. 16/246
Pattern di struttura
Adapter - Realizza interfacce per differenti classi.
Bridge - Separa l’interfaccia di un oggetto dalla sua implementazione.
Composite - Oggetti composti da oggetti con la stessa struttura.
Decorator - Aggiunge dinamicamente funzionalità ad un oggetto.
Facade - Una singola classe che rappresenta un intero sottosistema.
Flyweight - Una rappresentazione fine di istanze efficentemente condivise.
Proxy - Un oggetto rappresenta un altro oggetto.
18/11/2009
UNICAM - p. 17/246
Pattern di comportamento
Chain of Responsibility - Un modo di passare richieste tra una catena di
oggetti.
Command - Incapsula richieste di comandi ad un oggetto.
Interpreter - Un modo di includere elementi di linguaggio in un programma.
Iterator - Scansione degli elementi di una collezione.
Mediator - Definisce un modo semplificato di comuncazione tra classi.
Memento - Congela e ripristina lo stato interno di una classe.
Observer - Un modo di notificare cambiamenti ad un insieme di classi.
State - Modifica del comportamento degli oggetti al cambiamento dello stato.
Strategy - Incapsulamento di algoritmi nelle classi.
Template Method - Rimanda l’esatto passo di elaborazione di un algoritmo ad
una sottoclasse.
Visitor - Aggiunge una nuova operazione senza cambiare la classe.
18/11/2009
UNICAM - p. 18/246
Pattern di creazione
Questi pattern permettono di descrivere come vengono creati gli oggetti.
L’idea è di astrarre il modo in cui sono creati gli oggetti per dover fare
esplicitamente new il meno possibile.
Un pattern di creazione aiuta a rendere un sistema indipendente da come gli
oggetti sono creati, composti e rappresentati.
Esistono due temi ricorrenti circa i pattern di creazione:
▲ Incapsulare la conoscenza circa quale classe concreta il sistema utilizzi;
▲ Nascondere il modo in cui istanze di classi siano create e messe assieme.
18/11/2009
UNICAM - p. 19/246
Factory Method
Separa la responsabilità di istanziare una classe dalla responsabilità di
scegliere quale classe istanziare.
Noto come: Virtual Constructor.
18/11/2009
UNICAM - p. 20/246
Factory Method, scopo
Il design pattern Factory Method definisce un’interfaccia (Creator) per ottenere
una nuova istanza di un oggetto (Product) delegando ad una classe derivata
(ConcreteCreator) la scelta di quale classe istanziare (ConcreteProduct).
La classe ConcreteCreator che determina quale classe ConcreteProduct
istanziare è stabilita a design-time attraverso l’ereditarietà, quindi questo
design pattern è classificato rispetto allo scopo come rivolto alle classi.
Rispetto al fine questo design pattern è classificato tra i pattern di creazione.
18/11/2009
UNICAM - p. 21/246
Factory Method, applicabilità
Un componente od un framework può aver bisogno di delegare al
programmatore che lo utilizza la scelta di quale classe istanziare. Ad esempio:
▲ si può lasciare al programmatore la scelta di quale classe istanziare tra
quelle di una lista predefinita di classi del framework (configurazione);
▲ si può lasciare al programmatore la scelta di istanziare una classe del
framework di default o una nuova classe da derivata da quella di default e
personalizzata dal programmatore stesso (personalizzazione);
▲ si può lasciare al programmatore la scelta di istanziare una nuova classe da
lui realizzata (estensione);
Questa necessità è assolta dal design pattern Factory Method. Esso infatti
invece di richiamare direttamente il costruttore della classe da istanziare
prevede l’uso di un metodo.
18/11/2009
UNICAM - p. 22/246
Factory Method, conseguenze
▲
▲
▲
Maggiore modularità: la concreta gestione delle operazioni di creazione e
gestione è confinata;
Maggiore elasticità: è possibile aggiungere altri oggetti di tipo diverso senza
cambiarne il loro uso;
Maggiore flessibilità: il cliente usa diversi tipi di oggetti nello stesso modo.
18/11/2009
UNICAM - p. 23/246
Factory: UML
Creator
Product
FactoryMethod()
AnOperation()
product=FactoryMethod()
ConcreteProduct
ConcreteCreator
FactoryMethod()
return new ConcreteProduct
18/11/2009
UNICAM - p. 24/246
Comportamento
Product
definisce l’interfaccia dell’oggetto creato dal factory method.
ConcreteProduct
implementa l’interfaccia di Product.
dichiara il factory method che produce un oggetto di tipo Product e lo
può invocare per creare un oggetto di tipo Product. Il creator può definire
un’implementazione del factory method che produce un oggetto
ConcreteProduct di default.
Creator
ridefinisce il factory method per produrre un’istanza di un
ConcreteProduct
ConcreteCreator
18/11/2009
UNICAM - p. 25/246
Factory Method. Esempio 1
Per esempio, ci sono due versioni A1 e A2 di una class A:
abstract class A { public abstract String getVal();
}
class A1 extends A {
private String val;
A1(String val) { this.val = val; }
public String getVal() { return "A1: " + val; }
}
class A2 extends A {
private String val;
A2(String val) { this.val = val; }
public String getVal() { return "A2: " + val; }
}
18/11/2009
UNICAM - p. 26/246
Factory Method. Esempio 2
La fattoria permette di astrarre come si fa la scelta fra A1 e A2:
class AFactory {
public static final int MAX_LENGTH = 3;
public AFactory() { } // Costruttore
public static boolean test(String s) {
return s.length() < MAX_LENGTH;
}
public static A get(String s) {
if (test(s)) { return new A1(s); }
return new A2(s);
}
}
18/11/2009
UNICAM - p. 27/246
Factory Method. Esempio 3
Adesso si creano gli oggetti di tipo A attraverso Factory
A a = AFactory.get("ab"), b = AFactory.get("abc");
System.out.println(a.getVal());
System.out.println(b.getVal());
È lo stesso codice, ma adesso a.getVal() produce A1: ab, invece
b.getVal() produce A2: abc.
Vantaggi
Abbiamo astratto la creazione di un oggetto di tipo A. Se si vuole cambiare
come sono creati gli oggetti di tipo A bisogna cambiare solamente la classe
AFactory.
18/11/2009
UNICAM - p. 28/246
Abstract Factory
Il design pattern Abstract Factory definisce un’interfaccia ("AbstractFactory")
tramite cui istanziare famiglie (famiglia 1, 2, ...) di oggetti (AbstractProductA,
AbstractProductB, ), tra loro correlati o comunque dipendenti, senza indicare
da quali classi concrete (ProductA1 piuttosto che ProductA2, ...). La scelta
delle classi concrete è delegata ad una classe derivata (ConcreteFactory1 per
la famiglia 1, ConcreteFactory2 per la famiglia 2, ...).
Noto come: kit.
18/11/2009
UNICAM - p. 29/246
Abstract Factory, scopo
Fornire un’interfaccia per creare famiglie di oggetti dipendenti senza specificare
le classi concrete.
18/11/2009
UNICAM - p. 30/246
Abstract Factory, applicabilità
▲
▲
▲
Realizzare un sistema indipendente da come i prodotti sono creati, composti
e rappresentati;
Il sistema deve essere configurato con famiglie multiple di prodotti;
Mettere a disposizione soltanto l’interfaccia, non l’implementazione, di una
libreria di classi.
18/11/2009
UNICAM - p. 31/246
Abstract Factory, conseguenze
▲
▲
▲
▲
Isola le classi concrete;
Facilita la portabilità;
Aumenta la consistenza tra i prodotti;
Per contro, inserire nuovi prodotti risulta complicato, in quanto implica
cambiamenti all’Abstract Factory.
18/11/2009
UNICAM - p. 32/246
Abstract Factory: UML
AbstractFactory
AbstractProductA
ProductA1
Client
+createProductA()
+createProductB()
ProductA2
ConcreteFactory1
ConcreteFactory2
+createProductA()
+createProductB()
+createProductA()
+createProductB()
AbstractProductB
ProductB1
18/11/2009
ProductB2
UNICAM - p. 33/246
Abstract Factory. Esempio 1
Per esempio, date due versioni delle classe A e B:
abstract class A { }
class A1 extends A {
private String val;
A1(String val) { this.val = val; }
}
class A2 extends A {
private String val;
A2(String val) { this.val = val; }
}
18/11/2009
UNICAM - p. 34/246
Abstract Factory. Esempio 2
abstract class B { }
class B1 extends B {
private int val;
B1(int val) { this.val = val; }
}
class B2 extends B {
private int val;
B2(int val) { this.val = val; }
}
18/11/2009
UNICAM - p. 35/246
Abstract Factory. Esempio 3
Una fattoria ha il compito di dare un insieme compatibile di A e B:
abstract class AbAbstractFactory {
public abstract A getA(String val);
public abstract B getB(int i);
}
18/11/2009
UNICAM - p. 36/246
Abstract Factory. Esempio 4
Nel nostro caso A1 corrisponde a B1 e A2 a B2.
Dunque ci sono due fattorie:
class AbAbstractFactory1 extends AbAbstractFactory {
public A getA(String val) { return new A1(val); }
public B getB(int i) { return new B1(i); }
}
e
class AbAbstractFactory2 extends AbAbstractFactory {
public A getA(String val) { return new A2(val); }
public B getB(int i) { return new B2(i); }
}
18/11/2009
UNICAM - p. 37/246
Abstract Factory. Esempio 5
Un esempio di utilizzo di queste fattorie è il seguente:
AbAbstractFactory f1 = new AbAbstractFactory1();
AbAbstractFactory f2 = new AbAbstractFactory2();
A a1 = f1.getA("ab");
// crea un oggetto di tipo
B b1 = f1.getB(1);
// crea un oggetto di tipo
A a2 = f2.getA("ab");
// crea un oggetto di tipo
B b2 = f2.getB(2);
// crea un oggetto di tipo
18/11/2009
A1
B1
A2
B2
UNICAM - p. 38/246
Singleton
Il design pattern Singleton permette di assicurare che una classe (Singleton)
abbia una unica istanza e che questa sia globalmente accessibile in un punto
ben noto.
Il modo più semplice di implementare questo pattern in Java è di definire una
class final con tutti i metodi statici.
18/11/2009
UNICAM - p. 39/246
Singleton, scopo
Costruire un unico oggetto di un determinato tipo.
Assicurarsi che una classe abbia soltanto un’istanza, e fornirne un unico punto
di accesso.
18/11/2009
UNICAM - p. 40/246
Singleton, applicabilità
Il Singleton andrebbe usato:
▲ Quando si vuole la garanzia che nel sistema vi sia una solo istanza di oggetti
di un determinato tipo;
▲ Quando non si vuole delegare ad altri il controllo di unicità di un oggetto;
▲ Quando più oggetti devono condividere un unico pool di dati;
▲ Deve esserci esattamente una singola istanza di una classe, e deve essere
accessibile da un punto di accesso ben preciso;
▲ Quando tale istanza deve essere estensibile tramite subclassing, i client
dovrebbero poter utilizzare l’istanza estesa senza modificare il proprio
codice.
18/11/2009
UNICAM - p. 41/246
Singleton, conseguenze
▲
▲
▲
▲
▲
▲
▲
▲
Accesso controllato all’istanza singola;
Name space ridotto;
Raffinamento di operazioni e rappresentazione delle stesse;
Possibilità di usare un numero variabile di istanze;
Maggiore flessibilità rispetto all’uso degli static member;
Quindi si ha:
Maggiore correttezza: anche volendo non è possibile produrre oggetti
distinti;
Maggiore modularità: la gestione dei dati gestisti dal singleton può essere
remotizzata;
Maggiore flessibilità: la modifica dei dati gestiti dal singleton non interferisce
con l’unicità dell’stanza.
18/11/2009
UNICAM - p. 42/246
Singleton: UML
Singleton
-uniqueInstance
-singletonData
-Singleton()
+getIstance()
+getSingletonData()
18/11/2009
return uniqueIstance
UNICAM - p. 43/246
Singleton
import java.io.*;
class EsempioDiSingleton { // System
public static void main(String[] args) {
PrintStream o1 = System.out, o2 = System.out;
if (o1 == o2) o1.println("Single istance");
}
}
oppure
class EsempioDiSingleton { // Math
public static void main(String[] args) {
final double x = 2.0;
System.out.printf("sqrt(%f)=%f",x,Math.sqrt(x));
}
}
18/11/2009
UNICAM - p. 44/246
Singleton. Esempio 1
Ponendo il costruttore con visibilità privata di fatto si impedisce la creazione di
oggetti. La creazione del singleton avviene solo con l’invocazione del metodo
getIstance() che produce sempre il riferimento all’unica istanza. Ad esempio:
class Singleton {
private static Singleton uniqueIstance = null;
private int singletonData;
private Singleton() { // generazione del dato unico
singletonData = (int)(Math.random()*10);
}
public static Singleton getSingleton() {
if (uniqueIstance==null) uniqueIstance = new Singleton();
return uniqueIstance;
}
public int getSingletonData() { retun singletonData; }
}
18/11/2009
UNICAM - p. 45/246
Builder
Il design pattern Builder separa la costruzione (il metodo Construct dell’oggetto
Director è l’algoritmo di costruzione che compone le parti e ogni metodo di
ConcreteBuilder costruisce una parte) di un oggetto complesso dalla sua
rappresentazione (Product) in modo tale che lo stesso processo di costruzione
(il metodo Construct dell’oggetto Director) possa essere usato per creare
diverse rappresentazioni (per ogni rappresentazione ci sarà un suo
ConcreteBuilder che produce un diverso Product).
Il builder è una variante della Abstract Factory in cui l’oggetto da produrre è
Composite. Dunque la chiamata di un Builder può implicare la creazione di
diversi oggetti. Il caso tipico è quello di un’applicazione con una interfaccia
grafica. In tal caso c’è sempre un Builder che deve preoccuparsi della
costruzione dell’interfaccia.
18/11/2009
UNICAM - p. 46/246
Builder, scopo
Separare la costruzione di un oggetto complesso dalla relativa
rappresentazione. Ovvero confina in una classe le operazioni per creare
oggetti complessi mascherando a chi vuole ottenere oggetti i complessi
meccanismi di costruzione.
18/11/2009
UNICAM - p. 47/246
Builder, applicabilità
▲
▲
L’algoritmo per la creazione di un oggetto complesso dovrebbe essere
indipendente dalle componenti dell’oggetto stesso;
Il processo di costruzione consente differenti rappresentazioni per l’oggetto.
18/11/2009
UNICAM - p. 48/246
Builder, conseguenze
▲
▲
▲
E’ possibile variare la rappresentazione interna di un prodotto;
Isola il codice per la costruzione e la rappresentazione: il Builder incapsula il
modo in cui un oggetto complesso è costruito;
Consente un miglior controllo sul processo di costruzione: il Builder consente
una costruzione step-by-step del prodotto, sotto il controllo del Director.
18/11/2009
UNICAM - p. 49/246
Builder: UML
Builder
Client
«instantiate»
Director
+costructProduct()
«instantiate»
ConcreteBuilder
+function()
18/11/2009
product Product
+function()
+getProduct()
Product
«call»
-attrib
+function()
UNICAM - p. 50/246
Struttura
Builder
specifica l’interfaccia astratta che crea le parti dell’oggetto Product.
costruisce e assembla le parti del prodotto implementando
l’interfaccia Builder; definisce e tiene traccia della rappresentazione che
crea.
ConcreteBuilder
Director
costruisce un oggetto utilizzando l’interfaccia Builder.
rappresenta l’oggetto complesso e include le classi che definiscono le
parti che lo compongono, includendo le interfacce per assemblare le parti
nel risultato finale.
Product
18/11/2009
UNICAM - p. 51/246
Esempio Builder 1
/* Product */
class Pizza {
private String base = "", salsa = "", condimento = "";
public void setBase(String b) { base = d; }
public void setSalsa(String s){ salsa = s; }
public void setCondimento(String c) { condimento = c; }
}
18/11/2009
UNICAM - p. 52/246
Esempio Builder 2
/* Abstract Builder */
abstract class PizzaBuilder {
protected Pizza pizza;
public Pizza getPizza() { return pizza; }
public void createNewPizzaProduct() { pizza = new Pizza(); }
public abstract void buildBase();
public abstract void buildSalsa();
public abstract void buildCondimento();
}
18/11/2009
UNICAM - p. 53/246
Esempio Builder 3
/* ConcreteBuilder */
class PizzaMargherita extends PizzaBuilder {
public void buildBase() { pizza.setBase("sottile"); }
public void buildSalsa() { pizza.setSauce("pomodoro"); }
public void buildCondimento() {
pizza.setCondimento("acciughe");
}
}
18/11/2009
UNICAM - p. 54/246
Esempio Builder 4
/* ConcreteBuilder */
class PizzaCapricciosa extends PizzaBuilder {
public void buildBase() { pizza.setBase("spessa"); }
public void buildSalsa() { pizza.setSalsa("salsa"); }
public void buildCondimento() {
pizza.setCondimento("uova+olive+carciofini");
}
}
18/11/2009
UNICAM - p. 55/246
Esempio Builder 5
/* Director */
class Cottura {
private PizzaBuilder pizzaBuilder;
public void setPizzaBuilder(PizzaBuilder pb) {
pizzaBuilder = pb;
}
public Pizza getPizza() {
return pizzaBuilder.getPizza();
}
public void constructPizza() {
pizzaBuilder.createNewPizzaProduct();
pizzaBuilder.buildBase();
pizzaBuilder.buildSalsa();
pizzaBuilder.buildCondimento();
}
}
18/11/2009
UNICAM - p. 56/246
Esempio Builder 6
/* Client */
class Cuoco {
public static void main(String[] args) {
Cottura cuoce = new Cottura(); // 1
PizzaBuilder pizzaMargherita = new PizzaMargherita(); // 2
PizzaBuilder pizzaCapricciosa = new PizzaCapricciosa(); // 3
cuoce.setPizzaBuilder(pizzaMargherita); // 4
cuoce.constructPizza(); // Attiva il costruttore // 5
Pizza pizza = cuoce.getPizza(); // 6
}
}
18/11/2009
UNICAM - p. 57/246
Esempio Builder 7
1. Il Cuoco (client), crea una istanza di Cottura indipendente da che cosa
cuocerà
2. Il Cuoco crea una istanza di pizzaMargherita (ConcreteBuilder)
3. Il Cuoco crea una istanza di pizzaCapricciosa (ConcreteBuider)
4. Il Cuoco assegna a cuoce, istanza di Cottura, la pizzaMargherita
5. Il Cuoco invoca il costruttore di cuoce per cuocere la pizzaMarcherita
assegnata prima
6. Il Cuoco invoca i metodi di cuoce per accedere a funzionalità di specifiche di
pizzaMargherita (Product)
18/11/2009
UNICAM - p. 58/246
Prototype
Il design pattern Prototype istanzia un oggetto clonandolo da un’istanza
esistente.
Esso si applica quando la creazione di un oggetto di tipo A è costosa in termini
di computazione ma può essere semplificata avendo già un oggetto di tipo A. In
Java, nella class Object è previsto un metodo clone.
18/11/2009
UNICAM - p. 59/246
Protopype, scopo
Specificare il tipo di oggetti da creare utilizzando un’istanza prototipale, e
creare nuovi oggetti copiando il prototipo.
18/11/2009
UNICAM - p. 60/246
Prototype, applicabilità
Il pattern andrebbe usato nel caso in cui un sistema dovrebbe essere
indipendente da come i prodotti sono creati, e:
▲ Quando le classi da istanziare sono specificate a run-time;
▲ Per evitare la creazione di gerarchie di factory parallele alle gerarchie di
prodotti;
▲ Quando le istanze di una classe possono trovarsi in soltanto una (o poche)
combinazioni di stati.
18/11/2009
UNICAM - p. 61/246
Prototype, conseguenze
(oltre a quelle dell’Abstract Factory e del Builder):
▲ Aggiungere o rimuovere prodotti a run-time;
▲ Specificare nuovi oggetti variando valori;
▲ Specificare nuovi oggetti variando strutture;
▲ Ridurre il sub-classing;
▲ Caricare classi dinamicamente nell’applicazione.
18/11/2009
UNICAM - p. 62/246
Prototype: UML
Prototype
Client
+clone()
«use»
+operation()
Object x = prototype.clone()
ConcretePrototype1
ConcretePrototype2
+clone()
+clone()
return this
18/11/2009
return this
UNICAM - p. 63/246
Protopype. Esempio 1
class Object {
// ...
protected Object clone() throws CloneNotSupportedException {
if (! (this instanceof Cloneable)) {
throw new CloneNotSupportedException();
}
// ....
}
}
18/11/2009
UNICAM - p. 64/246
Protopype. Esempio 2
Per essere duplicato un oggetto deve implementare l’interfaccia Cloneable
(che è vuota!). Il comportamento di default è di copiare l’oggetto ma non gli
attributi.
Per esempio, per permettere di copiare una class A fuori di A bisogna fare un
overriding del metodo clone:
class Element implements Cloneable {
private int i;
int getI() { return i; }
void setI(int i) { this.i = i; }
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
18/11/2009
UNICAM - p. 65/246
Protopype. Esempio 3
Adesso si può creare un prototipo:
class Use {
public operation() {
try {
Eelement [] array = new array[100];
Element one = new Element();
array[0] = one;
one.setI(0);
for(int i = 1, i < 100; i++) {
array[i]=one.clone(); array[i].setI(i);
}
} catch (CloneNotSupportedException e) {
}
}
18/11/2009
UNICAM - p. 66/246
Pattern di struttura
I pattern di struttura si applicano per descrivere come è stata organizzata la
struttura dei diversi oggetti. Essi riguardano il modo in cui classi e oggetti sono
legati tra loro in strutture più grandi.
Questa organizzazione può essere fatta usando l’ereditarietà (si parla allora di
pattern di classi) o usando oggetti che contengono altri oggetti (si parla allora
di pattern di oggetti).
Il consiglio è di preferire sempre la seconda soluzione.
18/11/2009
UNICAM - p. 67/246
Adapter
Il design pattern Adatper converte l’interfaccia di una classe o di un oggetto
(Adaptee) nell’interfaccia (Target) che l’utilizzatore (Client) si aspetta. Il design
pattern Adatper permette all’utilizzatore (Client) e alla classe o oggetto
(Adaptee) che altrimenti non potrebbero collaborare di lavorare insieme.
Esso si applica quando c’è bisogno di adattare il comportamento di un oggetto
B in modo tale che B propone la stessa interfaccia di un altro oggetto A.
Noto come: Wrapper.
18/11/2009
UNICAM - p. 68/246
Adapter, scopo
Convertire l’interfaccia di una classe in un’interfaccia differente richiesta dal
client: in tal modo è possibile l’interoperabilità tra classi aventi interfacce
incompatibili.
Si vuole usare una classe esistente senza modificarla. Tale classe è quella da
adattare (adaptee). Il contesto in cui si vuole usare la classe adattata richiede
un’interfaccia detta obbiettivo che è diversa dalla classe che si vuole adattare.
18/11/2009
UNICAM - p. 69/246
Adapter, applicabilità
E’ consigliabile utilizzare l’adapter quando:
▲ Occorre utilizzare classi esistenti, ma le interfacce non sono compatibili con
quella del client;
▲ Occorre creare una classe riusabile che coopera con altre classi scollegate
da essa, e dunque aventi interfacce diverse;
▲ Occorre utilizzare diverse sottoclassi esistenti, ma non è pratico adattare
l’interfaccia effettuando il subclassing di ognuna. Si può allora utilizzare un
object adapter;
▲ Non si vuole o non si può modificare la classe da adattare.
18/11/2009
UNICAM - p. 70/246
Adapter, conseguenze
▲
▲
▲
▲
Un Class Adapter non è in grado di adattare una classe e tutte le relative
sottoclassi;
Un Class Adapter consente l’override del comportamento dell’Adaptee;
Un Object Adapter consente ad un singolo Adapter di operare con diversi
Adaptee;
Tramite un Object Adapter l’overriding del comportamento dell’Adaptee
risulta maggiormente difficoltoso: richiede il sub-classing dell’Adaptee, e
richiede che l’Adapter punti a tali sub-classi piuttosto che all’Adaptee stesso.
18/11/2009
UNICAM - p. 71/246
Adapter: UML
Target
Adaptee
+request()
+specificRequest()
Client
Adapter
Nota
+request()
18/11/2009
UNICAM - p. 72/246
Adapter: UML
Target
Adaptee
+request()
+specificRequest()
Client
adaptee
Adapter
Nota
+request()
18/11/2009
UNICAM - p. 73/246
Adapter. Esempio 1
Per esempio, abbiamo costruito un’applicazione che permette di usare un
oggetto di tipo A:
class A {
public int getX() { ... }
public int getY() { ... }
}
e vogliamo che si possa usare anche un oggetto B che con qualche modifica
può fare tutto quello che fa A.
class B {
int getXPlusY() { ... }
int getXMinuxY() { ... }
}
18/11/2009
UNICAM - p. 74/246
Adapter. Esempio 2
Il primo passo è di rappresentare quello che può fare A come un’interfaccia:
interface ACapable {
int getX();
int getY();
}
e cambiare A in conseguenza:
class A implements ACapable {
....
}
18/11/2009
UNICAM - p. 75/246
Adapter. Esempio 3
Adesso esistono due possibilità:
Class Adapter
Si può usare l’ereditarietà per adattare B:
class AClassAdapter extends B implements ACapable {
public int getX() {
return (getXPlusY() + getXMinusY()) / 2;
}
public int getY() {
return (getXPlusY() - getXMinusY()) / 2;
}
}
18/11/2009
UNICAM - p. 76/246
Adapter. Esempio 4
Object Adapter
Si può creare un nuovo oggetto che contiene l’oggetto B:
class AObjectAdapter implements ACapable {
private B b;
public int getX() {
return (b.getXPlusY() + b.getXMinusY()) / 2;
}
public int getY() {
return (b.getXPlusY() - b.getXMinusY()) / 2;
}
}
Quest’ultima soluzione è da preferire.
18/11/2009
UNICAM - p. 77/246
In java
Adaptee InputStream
Target Reader
Adapter InputStreamReader
Client La classe che vuole leggere testo da un flusso in ingresso
targetMethod read
adapteeMethod read
18/11/2009
UNICAM - p. 78/246
Bridge
Il bridge pattern permette di separare l’interfaccia di una classe (che cosa si
può fare con la classe) dalla sua implementazione (come si fa). In tal modo si
può usare l’ereditarietà per fare evolvere l’interfaccia o l’implementazione in
modo separato ed autonomo e sia possibile sostituire l’implementazione senza
conseguenze per l’utilizzatore (Client).
Noto come: Handle/Body, Driver.
18/11/2009
UNICAM - p. 79/246
Bridge, scopo
Disaccoppiare un’astrazione dalla relativa implementazione in maniera tale da
consentire ad entrambe di variare in maniera indipendente.
18/11/2009
UNICAM - p. 80/246
Bridge, applicabilità
Può essere conveniente utilizzare il bridge nei seguenti casi:
▲ Si desidera evitare un binding permanente tra astrazione e interfaccia;
▲ Occorre rendere flessibili astrazione e implementazioni mediante
sub-classing;
▲ Le modifiche all’implementazione non dovrebbero avere impatto sui client;
▲ Rendere l’implementazione completamente invisibile ai client;
▲ Condividere un’implementazione tra oggetti multipli.
18/11/2009
UNICAM - p. 81/246
Bridge, conseguenze
▲
▲
▲
Disaccoppiare interfaccia e implementazione: ciò riduce anche necessità di
ricompliazioni continue dell’Abstraction durante la fase di sviluppo e
incoraggia lo sviluppo di sistemi a layer;
Migliorare l’estensibilità;
Nascondere i dettagli implementativi ai client.
18/11/2009
UNICAM - p. 82/246
Bridge: UML
Client
implementor.operationImp()
Abstraction
+operation()
RedefinedAbstraction
18/11/2009
Implementor
implementor
+operation()
ConcreteImplementorA
ConcreteImplementorB
+operationImp()
+operationImp()
UNICAM - p. 83/246
Bridge. Esempio 1
Vogliamo realizzare funzionalità e realizzazioni per disegnare cerchi. La parte
che riguarda la sezione implementattiva è definita con una interfaccia che
permette successivamente di essere implementata con diverse classi
/* Implementor */
interface Cerchio {
public void disegnaCerchio(double x, double y, double raggio);
}
18/11/2009
UNICAM - p. 84/246
Bridge. Esempio 2
Due realizzazioni di implementazione
/* ConcreteImplementorA*/
class DisegnaCerchio1 implements Cerchio {
public void disegnaCerchio(double x, double y, double r) {
System.out.printf("Cerchio DisegnaCerchio1(%f,%f,&f)",x,y,r);
}
}
e
/* ConcreteImplementorB*/
class DisegnaCerchio2 implements Cerchio {
public void disegnaCerchio(double x, double y, double r) {
System.out.printf("Cerchio DisegnaCerchio2(%f,%f,&f)",x,y,r);
}
}
18/11/2009
UNICAM - p. 85/246
Bridge. Esempio 3
La parte che riguarda la sezione descrittiva è definita con una interfaccia che
permette successivamente di essere estesa con ulteriori funzionalità
/* Abstraction */
interface Disegno {
public void disegna();
public void ridimenzionaInPercentuale(double p);
// funzione aggiunta non definita nell’implementazione
}
18/11/2009
UNICAM - p. 86/246
Bridge. Esempio 4
/* RefinedAbstraction */
class DisegnaCerchio implements Disegno {
private double x, y, r;
private Cerchio d; // associazione implementor
DisegnaCerchio(double x, double y, double r, Cerchio i) {
this.x = x, this.y = y; this.r = r, this.d = i;
}
public void disegna() { d.disegnaCerchio(x,y,r); }
public void ridimenzionaInPercentuale(double p) { r *= p; }
}
}
18/11/2009
UNICAM - p. 87/246
Bridge. Esempio 5
/* Client */
class Bridge {
public static void main(String [] args) {
Disegno [] disegni = new Disegno[2];
disegni[0] = new DisegnaCerchio(1,2,3,new DisegnaCerchio1());
disegni[1] = new DisegnaCerchio(5,7,11,new DisegnaCerchio2());
for (int i = 0; i < disegni.legth; i++) {
disegni[i].ridimensionaInPercentuale(2.5);
disegni[i].disegna();
}
}
}
18/11/2009
UNICAM - p. 88/246
Composite
Il composite pattern è applicato quando c’è bisogno di esprimere un insieme di
oggetti che rappresentano una gerarchia.
18/11/2009
UNICAM - p. 89/246
Composite, scopo
Costruire oggetti che possono essere a loro volta componenti di oggetti dello
stesso tipo.
Comporre oggetti in strutture ad albero per rappresentare gerarchie tutto-parti.
Il Composite consente di trattare oggetti singoli e composizioni di oggetti in
maniera uniforme.
18/11/2009
UNICAM - p. 90/246
Composite, applicabilità
Il Composite dovrebbe essere usato quando:
▲ Si desidera rappresentare gerarchie tutto-parti;
▲ Si cerca di rendere i client capaci di ignorare le differenze tra insiemi di
oggetti ed oggetti individuali;
▲ Si vogliono gestire un insieme di oggetti come se fossero singoli oggetti;
▲ Ogni operazione dell’oggetto composto è la combinazione delle operazioni
dei singoli componenti;
▲ Le funzionalità dell’oggetto composto sono le stesse dei suoi componenti.
18/11/2009
UNICAM - p. 91/246
Composite, conseguenze
▲
▲
▲
▲
▲
▲
▲
▲
Sono definite gerarchie costituite da oggetti primitivi e oggetti composti;
Semplifica il client;
Rende semplice l’aggiunta di nuovi tipi di componenti;
Rende il design generale;
Maggiore modularità: il pattern modulare per definizione;
Maggiore elasticità: i componenti possono essere sviluppati
successivamente;
Maggiore flessibilità: gli oggetti concreti possono realizzare le operazioni in
modo diverso;
E’ però problematico porre dei vincoli sui componenti (es. specificare che un
oggetto può essere composto solo da determinati componenti).
18/11/2009
UNICAM - p. 92/246
Composite, UML
Primitive
+operation()
Leaf
Composite
+operation()
+operation()
Invoca operation()
per ciascun oggetto
primitivo e combina
i risultati
18/11/2009
UNICAM - p. 93/246
Composite. Esempio 1
Una class astratta contiene tutto quello che si può fare con gli oggetti
class Primitive {
protected Vector componenti;
int getVolume();
}
Se si vuole realizzare un modello che realizzi la gestione di pacchi e scatole
che contengono pacchi e scatole.
18/11/2009
UNICAM - p. 94/246
Composite. Esempio 2
Dopo si possono creare degli oggetti semplici, i pacchi con il volume associato:
class Pacco extends Primitive {
private int volume;
public void putVolume(int v) { volume = v; }
public getVolume() { return volume; }
}
18/11/2009
UNICAM - p. 95/246
Composite. Esempio 3
o degli oggetti compositi, le scatole che contengono pacchi o altre scatole:
class Scatola extends Primitive {
public void putVolume(int v) { volume = v; }
public getVolume() {
int volume = 0;
for (Enumeration e = componenti.elements();
hasMoreElement(); )
volume += (Primitive)e.nextElement().getVolume();
return volume;
}
public addComponen(Primitive p) { componenti.add(p); }
}
18/11/2009
UNICAM - p. 96/246
Composite, esempio 4
Esempio di composite in java è:
Primitive: Component
Composite: Container
Leaf: JButton, JTextArea
operation(): getPreferedArea()
18/11/2009
UNICAM - p. 97/246
Decorator
Il design pattern Decorator aggiunge responsabilità in modo dinamico, ossia a
run-time, ad un singolo oggetto senza richiedere l’uso della ereditarietà che
invece aggiunge responsabilità alla classe, e quindi a tutti gli oggetti istanziati,
ed in modo statico ossia già a design-time.
Esso è applicato quando si vuole aggiungere una funzionalità ad una classe
senza modificarla.
Noto come: Wrapper.
18/11/2009
UNICAM - p. 98/246
Decorator, scopo
Aggiungere responsabilità addizionali agli oggetti in maniera dinamica.
18/11/2009
UNICAM - p. 99/246
Decorator, applicabilità
Il Decorator andrebbe usato:
▲ Quando le operazioni aggiunte dal decoratore non annullano le operazioni
effettuate dal decorato;
▲ Quando è possibile aggiungere anche decorazioni nulle a tutte le operazioni
del decorato (il decoratore è un decorato);
▲ Quando si vuole lasciare ad altri la libertà di decorare;
▲ Per aggiungere responsabilità ad oggetti dinamicamente e
trasparentemente;
▲ Per aggiungere responsabilità che possono essere eliminate;
▲ Quando l’estensione tramite sub-classing si rivela poco pratica (es. un
cospicuo numero di estensioni produrrebbe un’esplosione delle sottoclassi
per supportare ogni combinazione).
18/11/2009
UNICAM - p. 100/246
Decorator, conseguenze
▲
▲
▲
▲
▲
▲
Evita che classi relative a feature specifiche siano presenti nella parte alta
della gerarchia;
Un Decorator e i suoi componenti non sono identici, anche se tutto ciò è
trasparente dal punto di vista logico;
Usare i Decorator potrebbe però portare alla creazione di sistemi in cui sono
presenti troppi oggetti molto piccoli, che differiscono tra loro solo per i modi
in cui sono interconnessi;
Maggiore modularità: le decorazioni sono ereditate da tutte le sottoclassi del
decoratore e un decoratore è un componente;
Maggiore elasticità: la decorazione può essere realizzata successivamente
alla definizione del decorato;
Maggiore flessibilità: le funzionalità aggiunte sono ereditabili da tutte le
sottoclassi.
18/11/2009
UNICAM - p. 101/246
Decorator: UML
component.operation()
Component
+operation()
ConcreteComponent
Decorator
addBehaviour
+operation()
ConcreteDecoratorA
-addedState
+operation()
18/11/2009
+operation()
ConcreteDecoratorB
+operation()
+AddedBehaviour()
UNICAM - p. 102/246
Decorator, esempio 1
Si vuole realizzare una finestra e successivamente aggiungere le funzionalità
di scroll orizontale e verticale.
/* Component */
interface Window {
public void draw();
public String getDescrition();
}
/* Decorator */
abstract class WindowDecorator {
protected Window decorateWindow; // associazione con Window
public windowDecorator(Window dw) {
this.decorateWindow = dw;
}
}
18/11/2009
UNICAM - p. 103/246
Decorator, esempio 2
Si realizzano due decorazioni
/* ConcreteDecoratorA */
class VerticalScrollBarDecorator extends WindowDecorator {
public VerticalScrollBarDecorator(Window dw) { super(dw); }
public void draw() {
drawVerticalScrollBar();
decorateWindow.draw();
}
private drawVerticalScrollBar() { /*codice per vert scroll */ }
public String getDescription() {
return decorateWindow.getDescrition() +
", including vertical scrollbar";
}
}
18/11/2009
UNICAM - p. 104/246
Decorator, esempio 3
La seconda decorazione introduce lo scrolling orizontale
/* ConcreteDecoratorB */
class HorizontalScrollBarDecorator extends WindowDecorator {
public HorizontalScrollBarDecorator(Window dw) { super(dw); }
public void draw() {
drawHorizontalScrollBar();
decorateWindow.draw();
}
private drawHorizontalScrollBar() { /*codice per horz scroll */ }
public String getDescription() {
return decorateWindow.getDescrition() +
", including horizontal scrollbar";
}
}
18/11/2009
UNICAM - p. 105/246
Decorator, esempio 4
La finestra senza scrollbar
/* ConcreteComponent */
class SimpleWindow extends Window {
public void draw() { /* codice per disegnare la finestra */ }
public String getDescription() {
return "simple window";
}
}
18/11/2009
UNICAM - p. 106/246
Decorator, esempio 5
Infine per usare Window con scrool
public class DecoratedWindowTest {
public static void main(String[] args) {
// create una finestra con scroll orizontale e verticale
Window decoratedWindow = new HorizontalScrollBarDecorator (
new VerticalScrollBarDecorator(new SimpleWindow()));
// stampa la descrizione della finestra
System.out.println(decoratedWindow.getDescription());
}
}
18/11/2009
UNICAM - p. 107/246
Decorator in JDK
import java.io.*;
class EsempioDiDecorator {
public static void main(String[] args) {
try {
InputStreamReader r = new InputStreamReader(System.in);
BufferedReader b = new BufferedReader(r);
System.out.print("Impostare elenco di caratteri: ");
System.out.println("Il primo carattere è: "+r.read());
System.out.println("Il resto: "+b.readLine());
} catch(IOException e) { System.out.println(e); }
}
}
18/11/2009
UNICAM - p. 108/246
Facade
Il design pattern Facade definisce un’iterfaccia unificata (Facade) di più alto
livello attraverso cui accedere al sotto-sistema e alle sue varie interfaccie
rendendolo più semplice da usare.
Un facade permette di concentrare in una sola classe un sistema composto da
diverse classi.
18/11/2009
UNICAM - p. 109/246
Facade, scopo
Fornire un’interfaccia unificata ad un insieme di interfacce di un sottosisistema,
allo scopo di rendere il sottosistema più semplice da utilizzare.
Creare una nuova interfaccia per un insieme di sottosistemi.
18/11/2009
UNICAM - p. 110/246
Facade, applicabilità
Il Facade andrebbe usato quando:
▲ Si desidera fornire un’interfaccia semplificata ad un sottosistema complesso;
▲ Esistono molte interdipendenze tra i client e le implementazioni delle
astrazioni: il facade effettua un’operazione di disaccoppiamento;
▲ Realizzare sistemi a layer, in cui il facade costituisce punto di accesso ad un
layer (sottosistema);
▲ Si vuole creare una nuova interfaccia per un insieme di sottosistemi;
▲ Si voglioni rendere disponibili nuove operazioni che sono composizioni di
altre operazioni;
18/11/2009
UNICAM - p. 111/246
Facade, conseguenze
▲
▲
▲
▲
▲
▲
“Scherma” il client dai componenti del sottosistema, riducendo il numero di
oggetti che il client manipola;
Favorisce un basso accoppiamento tra sottosistema e client;
In ogni caso, non proibisce ai client l’uso delle classi del sottosistema;
Crea una nuova interfaccia per un insieme di classi o di sottosistemi;
Permette di comporre in una nuova interfaccia un insieme di azioni di un
insieme di sottosistemi;
Rende indipendenti i sottosistemi dall’interfaccia che li compone.
18/11/2009
UNICAM - p. 112/246
Facade: UML
Facade
ClassA
Subsystem One
18/11/2009
ClassB
ClassC
Subsystem Two
UNICAM - p. 113/246
Facade. Esempio 1
Un computer è composta da diversi sottosistemi complessi.
/* Subsystems */
class CPU {
public void freeze() { ... }
public void jump(long position) { ... }
public void execute() { ... }
}
class Memory {
public void load(long position, byte[] data) { }
}
class HardDrive {
public byte[] read(long lba, int size) { }
}
18/11/2009
UNICAM - p. 114/246
Facade. Esempio 2
Il front-end può essere realizzato da un facade.
/* Facade */
class Computer {
public void startComputer() {
cpu.freeze();
memory.load(BOOT_ADDRESS,hardDrive.read(BOOT_SECTOR,SECTOR_SIZE));
cpu.jump(BOOT_ADDRESS);
cpu.execute();
}
}
18/11/2009
UNICAM - p. 115/246
Facade. Esempio 3
Il client usa il front-end facade per attivare tutti i sottosistemi.
/* Client */
class You {
public static void main(String[] args) {
Computer facade = new Computer();
facade.startComputer();
}
}
18/11/2009
UNICAM - p. 116/246
Flyweight
Il flyweight pattern permette di separare la parte variable di una classe dalla
parte che può essere riutilizzata, in modo tale da poter avere quest’ultima
condivisa fra istanze differenti della parte variabile.
Un flyweight è un oggetto condiviso che può essere usato in contesti multipli
simultaneamente, ed è indistinguibile rispetto ad un’istanza non condivisa di un
oggetto.
Il flyweight ha 2 stati: quello intrinseco, condiviso tra i diversi oggetti e
contenuto all’interno del pattern e quello estrinseco, dipendente dal contesto,
quindi non condiviso.
Gli oggetti client sono responsabili del passaggio dello stato estrinseco al
flyweight quando questi lo necessita.
18/11/2009
UNICAM - p. 117/246
Flyweight, scopo
Utilizzare in modo efficiente le operazioni.
Utilizzare meccanismi di condivisione per utilizzare efficientemente oggetti a
grana fine.
18/11/2009
UNICAM - p. 118/246
Flyweight, applicabilità
Il flyweight andrebbe usato soltanto se tutte le seguenti condizioni risultassero
vere:
▲ L’applicazione utilizza un largo numero di oggetti;
▲ I costi di memorizzazione sono elevati a causa della grande quantità di
oggetti;
▲ La maggior parte dello stato dell’oggetto può essere resa estrinseca;
▲ Diversi gruppi di oggetti possono essere sostituiti da pochi oggetti condivisi
una volta rimosso lo stato estrinseco;
▲ L’applicazione non è dipendente dall’identità dell’oggetto;
▲ Quando esistono due distinti stati che vengono variati con frequenza diversa;
▲ Quando è possibile delegare ad un oggetto l’invocazione di operazioni
frequenti.
18/11/2009
UNICAM - p. 119/246
Flyweight, conseguenze
I flyweight introducono costi a runtime associati alla ricerca, al trasferimento, e
al calcolo dello stato estrinseco, specialmente se questo è formalmente
immagazzinato nello stato instrinseco. Tali costi sono ripagati dal risparmio di
spazio, funzione dei seguenti fattori:
▲ Riduzione del numero di istanze in conseguenza della condivisione;
▲ Dimensione dello stato intrinseco di ogni oggetto;
▲ Possibilità di calcolare lo stato estrinseco piuttosto che immagazzinarlo.
▲
▲
Ulteriori caratteristiche:
Modularità: è possibile aggiungere classi per modulare la frequenza di
chiamata;
L’invocazione di operazioni quando varia uno stato agisce solo sugli oggetti
effettivamente variati.
18/11/2009
UNICAM - p. 120/246
Flyweight: UML
FlyweightFactory
-flyweights
+getFlyweight(key)
Flyweight
+Operation(entrinsicState)
Nota
ConcreteFlyweight
UnsharedConcreteFlyweight
intrinsicState
operation(extrinsicState)
allState
operation(extrinsicState)
Client
18/11/2009
UNICAM - p. 121/246
Flyweight. Esempio 1
Per esempio, sia data una classe Punto che è molto usata, in cui i campi x e y
variano molto da un’istanza ad un’altra, mentre c’è poca variazione sul valore
del campo c di tipo Colore.
class Punto {
private int x, y;
private Color c; //parte poco variabile
Punto(int x, int y, Color c) {
this.x = x; this.y = y; this.c = c;
}
public String toString() { return b + "(" + x + ", " + y + ")";
}
}
18/11/2009
UNICAM - p. 122/246
Flyweight. Esempio 2
Si può creare un flyweight che contiene la parte che si può riutilizzare:
class ColorFlyWeight {
private Color c;
ColorFlyWeight(Color c) { this.c = c; }
public String toString(int x, int y) {
return c + "(" + x + ", " + y + ")";
}
}
class Color {
private int c;
Color(int c) { this.c = c; }
public String toString() {
return c == 0 ? "red" : (c == 1 ? "blue" : "green");
}}
18/11/2009
UNICAM - p. 123/246
Flyweight. Esempio 3
Invece la parte variabile resta nella classe Punto sostituendo il riferimento a
Color con ColorFlyWeight.
class Punto {
private int x, y;
private ColorFlyWeight cF;
Punto(int x, int y, ColorFlyWeight c) {
this.x = x; this.y = y; cF = c;
}
public String toString() { return cF.toString(x, y); }
}
18/11/2009
UNICAM - p. 124/246
Flyweight. Esempio 4
class UsoDiPunto {
public static void main(String [] args) {
Punto [] map = new Punto[10000];
Color [] tr = {new Color(0), new Color(1), new Color(2)};
Random r = new Random();
for (int i; i < map.length; i++)
map[i] = new Punto(r.next(i),r.next(i),tr[r.next(2)]);
for (int i; i < map.length; i++)
System.out.println(map[i]);
}
}
18/11/2009
UNICAM - p. 125/246
Proxy
Un pattern Proxy è una classe che gioca il ruolo di un’altra classe RealSubject.
Proxy ha la stessa interfaccia di RealSubject.
Ci sono diverse situazioni in cui questo può essere interessante:
▲ è costoso creare la classe RealSubject. Usando un proxy si può aspettare a
creare un oggetto di RealSubject fino a quando c’è veramente necessità;
▲ non si può creare istantanemente la class RealSubject. In tale caso il Proxy
gioca il ruolo di RealSubject “simulando” il più possibile;
▲ prima di accedere alla classe RealSubject bisogna verificare i diritti di
accesso.
Noto come: Surrogate.
18/11/2009
UNICAM - p. 126/246
Proxy, scopo
Creare un oggetto sosia che si comporti come l’oggetto reale, cioè fornire un
“surrogato” di un altro oggetto in maniera di controllare l’accesso all’oggetto
stesso.
18/11/2009
UNICAM - p. 127/246
Proxy, applicabilità
▲
▲
▲
▲
▲
▲
▲
▲
quando tenere in vita un oggetto costa molto;
quando l’esecuzione delle operazioni di un oggetto non può essere eseguita
immediatamente;
quando la creazione di un oggetto non può essere effettuata
immediatamente;
quando si necessita un riferimento ad un oggetto più versatile di un
puntatore.
Alcuni esempi:
Remote proxy: fornisce una rappresentazione locale di un oggetto
appartenente ad uno spazio di indirizzamento diverso;
Virtual proxy: crea oggetti “pesanti” on-demand;
Protection proxy: controlla l’accesso all’oggetto originale, il quale può avere
differenti diritti di accesso;
Smart reference: sostituzione di un semplice puntatore, che esegue
operazioni aggiuntive quando avviene l’accesso all’oggetto.
18/11/2009
UNICAM - p. 128/246
Proxy, conseguenze
▲
▲
▲
▲
▲
▲
▲
rinvia la creazione di un oggetto sino al primo utilizzo request();
è possibile aggiungere comportamenti non funzionali alle operazioni di
RealSubject per aumentare le prestazioni;
Proxy gestisce l’esecuzione delle operazioni in modo da ottimizzare il
funzionamento di RealSubject;
il Proxy inoltra le richieste al RealSubject quando opportuno, in base al tipo
di proxy realizzato;
Il remote proxy nasconde il fatto che un oggetto risieda in uno spazio di
indirizzamento diverso;
Il virual proxy introduce ottimizzazioni creando oggetti on-demand;
Il protection proxy e la smart reference introducono livelli di protezione
aggiuntivi.
18/11/2009
UNICAM - p. 129/246
Proxy: UML
Subject
Client
+request()
Proxy
RealSubject
+request()
+request()
Nota
18/11/2009
UNICAM - p. 130/246
Proxy. Esempio 1
Per esempio, data una classe RealSubject che può fare un’azione:
class RealSubject extends Subject {
RealSubject() {} void request() { /*...*/ }
}
si può creare un proxy che crea RealSbject solamente quando c’è bisogno di
effettuare tale azione:
class Proxy extends Subject {
private Subject a;
Proxy() { a = null; }
void request() {
if (a == null) a = new RealSubject();
a.request();
}
}
18/11/2009
UNICAM - p. 131/246
Chain of Responsibility
Il design pattern Chain Of Responsibility disaccoppia l’oggetto che effettua una
richiesta dal destinatario dando a più oggetti la possibilità di rispondere. Gli
oggetti candidati a rispondere sono concatenati tra loro e passano la richiesta
lungo la catena sino a quando un oggetto la gestisce.
Il pattern Chain Of Responsibility si applica quando c’è una catena di oggetti
che possono rispondere ad una richiesta. Questa catena è gerarchica. La
richiesta si muove lungo la catena per trovare l’oggetto più adatto per
rispondere. Avere una struttura a catena permette che un oggetto non abbia
bisogno di conoscere tutti gli elementi ma solamente il suo elemento
successivo nella catena.
18/11/2009
UNICAM - p. 132/246
Chain of Responsibility, scopo
Evitare l’accoppiamento tra il mittente di una richiesta e il ricevente, dando a
più oggetti (collegati tra loro a catena) la possibilità di manipolare la richiesta.
18/11/2009
UNICAM - p. 133/246
Chain of Responsibility, applicabilità
Questo pattern andrebbe usato se:
▲ Una richiesta può essere gestita da più di un oggetto, e il gestore non è noto
a priori;
▲ Si desidera inviare una richiesta a diversi oggetti senza specificare
esplicitamente il ricevente;
▲ L’insieme dei gestori della richiesta deve essere specificato dinamicamente.
18/11/2009
UNICAM - p. 134/246
Chain of Responsibility, conseguenze
▲
▲
▲
▲
Isola le classi concrete;
Facilita la portabilità;
Aumenta la consistenza tra i prodotti;
Per contro, inserire nuovi prodotti risulta complicato, in quanto implica
cambiamenti all’Abstract Factory.
18/11/2009
UNICAM - p. 135/246
Chain if responsibility: UML
Handler
successor
Client
+handlerRequest()
18/11/2009
ConcreteHandler1
ConcreteHandler2
+handlerRequest()
+handlerRequest()
UNICAM - p. 136/246
Chain of responsibility. Esempio 1
Tipicamente questo pattern è implementato in Java usando un’interfaccia:
interface Chain {
void setUp(Chain chain);
void process(Object o);
boolean test(Object o);
void action(Object o);
}
Il primo metodo permette di settare l’elemento successivo. Il secondo metodo è
il metodo che tratta una richiesta. I due metodi finali sono dei metodi di
appoggio. Il primo permette di verificare se la richiesta possa essere trattata
dall’oggetto e nel caso positivo il secondo definisce che cosa fare.
18/11/2009
UNICAM - p. 137/246
Chain of responsibility. Esempio 2
Gli elementi da processare sono:
class Element {
final static int NULL = 0, CHAR = 1, DOUBLE = 2;
private int type = NULL;
private Object e;
Element(char c) { type = CHAR; e = new Char(c); }
Element(double d) { type = DOUBLE; e = new Double(d); }
public getType() { return type; }
public Object getElement() { return element.clone(); }
}
18/11/2009
UNICAM - p. 138/246
Chain of responsibility. Esempio 3
Adesso tutti gli oggetti che possono comparire nella catena hanno bisogno di
implementare questa interfaccia. Per esempio, per una classe PrintChar
abbiamo:
public class PrintChar implements Chain {
private Chain chain;
public void setUp(Chain c) { chain = c; }
public void process (Element o) {
if (test(o)) { action(o); }
else if (chain != null) chain.process(o);
}
public boolean test(Element o) {
return o.getType() == Element.CHAR; }
public void action(Element o) {
System.out.print(o.getElement(); }
}
18/11/2009
UNICAM - p. 139/246
Chain of responsibility. Esempio 4
Per un’altra classe PrintDouble:
class PrintDouble implements Chain {
private Chain chain;
public void setUp(Chain c) { chain = c; }
public void process (Element o) {
if (test(o)) action(o);
else if (chain != null) chain.process(o);
public boolean test(Element o) {
return o.getType() == Element.DOUBLE; }
public void action(Element o) {
System.out.print(o.getElement());
}
}
18/11/2009
UNICAM - p. 140/246
Chain of responsibility. Esempio 5
Tutte le classi che implementano l’interfaccia hanno lo stesso codice per
processo. Questa è una situazione in cui il fatto di non avere la
multi-ereditarietà obbliga a duplicare codice.
class Chain {
public static void main(String [] args) {
PrintChar x = new PrintChar();
PrintDouble y = new PrintDouble();
x.setUp(y);
Element a = new Element(’a’), b = new Element(3.14);
x.process(a); x.process(b);
}
}
18/11/2009
UNICAM - p. 141/246
Command
Il design pattern Command permette (alla classe Client) di incapsulate una
richiesta, ossia un comando (Execute) da eseguire ed i suoi parametri (state),
sotto forma di oggetto (ConcreteCommand) da usare per parametrizzare il
comportamento di altri oggetti (Invoker) con diverse richieste (ossia con diversi
oggetti ConcreteCommand), code di richieste oppure log di richieste.
Consente quindi di astrarre la chiamata di un metodo.
Noto come: Action, Transaction.
18/11/2009
UNICAM - p. 142/246
Command, scopo
Incapsulare una richiesta in un oggetto, in modo tale da parametrizzare i client
rispetto a richieste differenti, consentendo operazioni come accodamento,
logging e undo.
18/11/2009
UNICAM - p. 143/246
Command, applicabilità
Il Command è utile per:
▲ Parametrizzare oggetti rispetto ai comandi da eseguire (ciò nei linguaggi
procedurali spesso avviene per mezzo di una callback, ovvero una funzione
registrata in un certo punto per poi essere richiamata successivamente);
▲ Specificare, accodare ed esegure richieste in tempi diversi;
▲ Supportare l’undo: l’operazione di Execute può mantenere lo stato per
annullare il proprio effetto;
▲ Supppotare il logging dei comandi in modo da consentire il re-do in caso di
crash del sistema;
▲ Eseguire transazioni atomiche.
18/11/2009
UNICAM - p. 144/246
Command, conseguenze
▲
▲
▲
▲
Il Command disaccoppia l’oggetto che invoca l’operazione da quello che la
esegue;
Un Command può essere manipolato o esteso come qualsiasi altro oggetto;
E’ possibile assemblare Commands creando dei comandi composti;
Aggiungere nuovi Command risulta semplice e non richiede modifiche alle
classi esistenti.
18/11/2009
UNICAM - p. 145/246
Command: UML
Command
Client
Invoker
+execute()
Receiver
+action()
ConcreteCommand
receiver
-state
+execute()
receiver.action()
18/11/2009
UNICAM - p. 146/246
Command. Esempio 1
Si consideri un interruttore elettrico con due comandi: per accendere e per
spegnere la luce.
Un vantaggio di questa particolare implementazione del pattern di comando è
che lo switch può essere utilizzato con qualsiasi dispositivo che prevede due
comandi per esempio accensione e spegnimento di un motore.
Si noti come lo switch non debba conoscere direttamente o indirettamente i
dettagli sulla lampada.
18/11/2009
UNICAM - p. 147/246
Command. Esempio 2
In Java, un command prende la forma di un’intefaccia:
/* Invoker */
public class Switch {
private Command flipUpCommand;
private Command flipDownCommand;
public Switch(Command flipUpCmd, Command flipDownCmd) {
this.flipUpCommand = flipUpCmd;
this.flipDownCommand = flipDownCmd;
}
public void flipUp() { flipUpCommand.execute(); }
public void flipDown() { flipDownCommand.execute(); }
}
18/11/2009
UNICAM - p. 148/246
Command. Esempio 3
/* Receiver */
public class Light {
public Light() { }
public void turnOn() { System.out.println("The light is on"); }
public void turnOff() { System.out.println("The light is off"); }
}
18/11/2009
UNICAM - p. 149/246
Command. Esempio 4
/* Command */
public interface Command { void execute(); }
18/11/2009
UNICAM - p. 150/246
Command. Esempio 5
/* ConcreteCommand for Up */
public class FlipUpCommand implements Command {
private Light theLight;
public FlipUpCommand(Light light) { this.theLight=light; }
public void execute(){ theLight.turnOn(); }
}
/* ConcreteCommand for Down */
public class FlipDownCommand implement Command {
private Light theLight;
public FlipDownCommand(Light light) { this.theLight=light; }
public void execute() { theLight.turnOff(); }
}
18/11/2009
UNICAM - p. 151/246
Command. Esempio 6
/* Client */
public class PressSwitch {
public static void main(String[] args) {
Light lamp = new Light();
Command switchUp = new FlipUpComman(lamp);
Command switchDown = new FlipDownComman(lamp);
Switch s = new Switch(switchUp,switchDown);
try {
if (args[0].equalsIgnoreCase("ON")) s.flipUp();
else if (args[0].equalsIgnoreCase("OFF")) s.flipDown();
else
System.out.println("ON or OFF is required.");
} catch (Exception e){
System.out.println("Arguments required."); }
}
}
18/11/2009
UNICAM - p. 152/246
Interpreter
Il design pattern Interpreter include la capacità di interpretare elementi di un
linguaggio in un programma perché definisce una rappresentazione per la
grammatica del linguaggio a beneficio di un interprete che usa la
rappresentazione per interpretare frasi prodotte in quel linguaggio.
Esso si applica nella situazione in cui è utile avere un piccolo linguaggio di
comandi o di macro. Nei casi semplici si può scrivere il parser e l’interprete
direttamente usando classe comme StringTokenizer. Nei casi più elaborati si
usa un toolkit dedicato (ad esempio ANTLR).
18/11/2009
UNICAM - p. 153/246
Interpreter, scopo
Dato un linguaggio, definire una rappresentazione per la grammatica di tale
linguaggio e un interprete del linguaggio stesso.
18/11/2009
UNICAM - p. 154/246
Interpreter, applicabilità
L’Interpreter è applicabile quando c’è un linguaggio da interpretare, i cui
statements possono essere rappresentati da nodi dell’albero sintattico.
Il pattern si rivela efficace se:
▲ La grammatica è semplice (altrimenti la gerarchia di classi diventa
complessa e difficile da gestire);
▲ L’efficienza non è essenziale (interpreti basati su espressioni regolari sono
più efficienti se realizzati mediante macchine a stati);
18/11/2009
UNICAM - p. 155/246
Interpreter, conseguenze
▲
▲
▲
▲
E’ semplice modificare ed estendere la grammatica;
Implementare una grammatica risulta, allo stesso modo, abbastanza
agevole;
Tuttavia, è difficile gestire grammatiche complesse (che implicano gerarchie
complesse di classi);
E’ possibile aggiungere nuovi modi di interpretare la medesima espressione,
definendo nuove operazioni nelle classi relative alle espressioni stesse.
18/11/2009
UNICAM - p. 156/246
Interpreter: UML
Context
AbstractExpression
Client
+interpret(Context)
18/11/2009
TerminalExpression
NonterminalExpression
+interpret(Context)
+interpret(Context)
UNICAM - p. 157/246
Interpreter. Esempio 1
Si vuole realizzare una calcolatore per la valutazioni di espressioni in notazione
polacca inversa. La grammatica delle espressioni è la seguente:
expression ::= plus | minus | variable
plus ::= expression expression ’+’
minus ::= expression expression ’-’
variable ::= ’a’ | ’b’ | ’c’ | ... | ’z’
Le espressioni prodotte con la precedente grammatica sono ad esempio:
a b +
a b c + a b + c a - -
18/11/2009
UNICAM - p. 158/246
Interpreter. Esempio 2
/* AbstractExpression */
import java.util.*;
interface Expression {
public int interpret(HashMap variables);
}
18/11/2009
UNICAM - p. 159/246
Interpreter. Esempio 3
/* TerminalExpression */
class Number implements Expression {
private int number;
public Number(int number) { this.number = number; }
public int interpret(HashMap variables) { return number; }
}
class Variable implements Expression {
private String name;
public Variable(String name) { this.name = name; }
public int interpret(HashMap variables) {
return variables.get(name).intValue();
}
}
18/11/2009
UNICAM - p. 160/246
Interpreter. Esempio 3
/* NonterminalExpression */
class Plus implements Expression {
Expression leftOperand;
Expression rightOperand;
public Plus(Expression left, Expression right) {
leftOperand = left; rightOperand = right;
}
public int interpret(HashMap variables) {
return leftOperand.interpret(variables) +
rightOperand.interpret(variables);
}
}
18/11/2009
UNICAM - p. 161/246
Interpreter. Esempio 4
/* NonterminalExpression */
class Minus implements Expression {
Expression leftOperand;
Expression rightOperand;
public Minus(Expression left, Expression right) {
leftOperand = left; rightOperand = right;
}
public int interpret(HashMap variables) {
return leftOperand.interpret(variables) rightOperand.interpret(variables);
}
}
18/11/2009
UNICAM - p. 162/246
Interpreter. Esempio 5
/*Client */
class Evaluator {
private Expression tree;
public Evaluator(String exp) {
Stack stack = new Stack();
for (String token : exp.split(" ")) {
if (token.equals("+"))
stack.push(new Plus(stack.pop(), stack.pop()));
else if (token.equals("-"))
stack.push(new Minus(stack.pop(), stack.pop()));
else stack.push( new Variable(token));
}
tree = stack.pop();
}
public int evaluate(HashMap context) {
return tree.interpret(context);
18/11/2009
}
UNICAM - p. 163/246
Interpreter. Esempio 6
Infine si vuole valutare l’espressione "w x z - +" con w = 5, x = 10, and z = 42.
public class InterpreterExample {
public static void main(String[] args) {
String expression = "w x z - +";
Evaluator sentence = new Evaluator(expression);
HashMap variables = new HashMap(); // Context
variables.put("w", new Integer(5));
variables.put("x", new Integer(10));
variables.put("z", new Integer(42));
int result = sentence.evaluate(variables);
System.out.println(result);
}
}
18/11/2009
UNICAM - p. 164/246
Iterator
Il design pattern Iterator (ConcreteIterator) accede in modo sequenziale gli
elementi di un oggetto aggregato ossia di una collezione (ConcreteAggregate).
Esso permette di scandire tutti gli elementi di una struttura senza conoscere
l’esatta implementazione della struttura.
Noto come: Cursor.
18/11/2009
UNICAM - p. 165/246
Iterator, scopo
Fornire un meccanismo per accedere ad elementi di un oggetto aggregato in
maniera sequenziale, nascondendone la rappresentazione.
18/11/2009
UNICAM - p. 166/246
Iterator, applicabilità
L’Iterator è utilizzato per:
▲ Accedere al contenuto di oggetti aggregati senza esporne la
rappresentazione interna;
▲ Supportare modi di attraversamento multipli;
▲ Fornire un’interfaccia multipla per attraversare diverse strutture (mediante il
polimorfismo degli iterator).
18/11/2009
UNICAM - p. 167/246
Iterator, conseguenze
▲
▲
▲
E’ possibile supportare diverse politiche di attraversamento;
Gli Iterator semplificano l’interfaccia dell’oggetto aggregato;
E’ possibile eseguire contemporaneamente più di un attraversamento sullo
stesso oggetto aggregato.
18/11/2009
UNICAM - p. 168/246
Iterator. Esempio 1
In Java è rappresentato dall’interfaccia Iterator.
public interface Iterator {
boolean hasNext();
Object next();
void remove();
}
18/11/2009
UNICAM - p. 169/246
Iterator. Esempio 2
Per esempio, un metodo che itera su un parametro di tipo Enumeration che
contiene degli elementi di tipo A si scrive come
void process(Iterator enum) {
while (enum.hasNext()) {
A a = (A) enum.next();
//...
}
}
Ogni struttura dati in Java (Vector, Hashtable, ...) che estende Container
implementa Iterator che permette di ottenere la scansione dei suoi elementi.
18/11/2009
UNICAM - p. 170/246
Iterator. Esempio 3
Si può anche creare un proprio Iterator. Per esempio, si può creare una classe
che trasforma un array in una enumearazione:
import java.util.Iterator;
import java.util.NoSuchElementException;
class ArrayEnumeration implements Iterator {
private int index; private Object[] array;
ArrayEnumeration(Object[] a) { array = a; index = 0; }
public Object next() {
if (array.length <= index)
throw new NoSuchElementException();
return array[index++];
}
public boolean hasNext() { return (index < array.length); }
public void remove() {}
}
18/11/2009
UNICAM - p. 171/246
Iterator. Esempio 4
Si può anche modificare gli iteratori. Per esempio, data l’interfaccia che
permette di selezionare gli oggetti validi
interface Filter { boolean valid(Object o); }
18/11/2009
UNICAM - p. 172/246
Iterator. Esempio 5
si può trasformare un’enumerazione di oggetti in un’enumerazione di oggetti
validi:
class FilteredEnumeration implements Enumeration {
private Enumeration enum;
private Filter filter;
private Object element;
private boolean flag;
FilteredEnumeration(Enumeration e, Filter f) {
enum = e; filter = f; element = null; flag = false;
}
18/11/2009
UNICAM - p. 173/246
Iterator. Esempio 6
public Object nextElement() {
if (flag && filter.valid(element)) {
flag = false; return element;
}
while (true) {
Object res = enum.nextElement();
if (filter.valid(res)) return res;
}
}
public boolean hasMoreElements() {
while (true) {
if (!(enum.hasMoreElements())) return false;
element = enum.nextElement();
if (filter.valid(element)) {
flag = true; return true;
}}}
18/11/2009
UNICAM - p. 174/246
Mediator
Il design pattern Mediator è utile quando si desidera implementare una
funzionalità componendo il comportamento di diversi oggetti.
Esso si applica in un gruppo di oggetti che interagiscono fra di loro. In tal caso
si possono concentrare in un oggetto tutte le richieste di interazione. Così
facendo gli oggetti del gruppo devono solamente conoscere il mediatore per
interagire fra di loro.
18/11/2009
UNICAM - p. 175/246
Mediator, scopo
Definire un oggetto che incapsula il modo in cui diversi oggetti interagiscono tra
loro, evitando che tali oggetti possano referenziarsi a vicenda in maniera
esplicita, e consentendo di cambiare il meccanismo di interazione in maniera
agevole.
18/11/2009
UNICAM - p. 176/246
Mediator, applicabilità
Il Mediator è utilizzabile se:
▲ Un insieme di oggetti comunicano in maniera ben definita (ma complessa) e
le interdipendenze risultando difficili da comprendere;
▲ Il riuso di un oggetto è difficoltoso in quanto questo comunica con molti
oggetti;
▲ Un behavior distribuito tra molte classi è customizzabile senza effettuare
molto subclassing.
18/11/2009
UNICAM - p. 177/246
Mediator, conseguenze
▲
▲
▲
▲
▲
Subclassing limitato;
Disaccoppiamento tra oggetti;
Semplificazione del protocollo di comunicazione tra oggetti;
Astrae come gli oggetti cooperano;
Centralizza il controllo.
18/11/2009
UNICAM - p. 178/246
Mediator: UML
Mediator
ConcreteMediator
18/11/2009
Collaugue
ConcreteColleague1
ConcreteColleague2
UNICAM - p. 179/246
Mediator. Esempio 1
Per esempio, consideriamo un gruppo di 3 oggetti, uno di tipo A:
class A {
private B b;
private C c;
void doIt() {
b.reset();
c.doIt();
}
void print(String s) {
//....
}
}
18/11/2009
UNICAM - p. 180/246
Mediator. Esempio 2
uno di tipo B:
class B {
private A a;
void reset() {
//....
}
void print(String s) {
a.print(s);
}
}
18/11/2009
UNICAM - p. 181/246
Mediator. Esempio 3
e uno di tipo C:
class C {
private A a;
void doIt() {
//....
}
void print(String s) {
a.print(s);
}
18/11/2009
UNICAM - p. 182/246
Mediator. Esempio 4
Applicando il pattern di mediazione, si crea un oggetto che conosce i tre
oggetti:
class AMediator {
private A a;
private B b;
private C c;
void doIt() {
b.reset();
c.doIt();
}
void print(String s) { a.print(s); }
}
18/11/2009
UNICAM - p. 183/246
Mediator. Esempio 5
Le class A, B e C si modificano di conseguenza:
class A {
private AMediator mediator;
void doIt() { mediator.doIt(); }
void print(String s) { }
}
class B {
private AMediator mediator;
void reset() { }
void print(String s) { mediator.print(s); }
}
class C {
private AMediator mediator;
void doIt() { }
void print(String s) { mediator.print(s); }
}
18/11/2009
UNICAM - p. 184/246
Memento
Il design pattern Memento senza violazioni della incapsulazione cattura lo stato
interno di un oggetto (Originator) portandolo all’esterno (sottoforma di
Memento) cosicchè quello stato possa essere ripristinato in seguito.
Esso si applica quando c’è bisogno di conservare lo stato di un oggetto per
dare la possibilità di recuperare questo stato più avanti.
Noto come: Token.
18/11/2009
UNICAM - p. 185/246
Memento, scopo
Senza violare l’incapsulamento, cattura ed esternalizza lo stato interno di un
oggetto in modo che possa essere ripristinato in futuro.
18/11/2009
UNICAM - p. 186/246
Memento, applicabilità
Il Memento può essere usato se:
▲ Occorre salvare lo stato di un oggetto (o una parte di esso) in modo da
poterlo ripristinare successivamente;
▲ Un’interfaccia diretta per ottenere tale stato potrebbe esporre dettagli
implementativi (venendo meno così al concetto di incapsulamento).
18/11/2009
UNICAM - p. 187/246
Memento, conseguenze
▲
▲
▲
▲
▲
Preservare l’incapsulamento;
Semplificare l’Originator;
Utilizzare un Memento potrebbe tuttavia essere costoso (se copiare l’intero
stato dovesse risultare dispendioso; è possibile applicare un discorso di
incrementalità, operando in maniera differenziale);
In alcuni linguaggi è difficile limitare al solo Originator l’accesso al Memento;
Il costo del Memento non è noto al caretaker (che quindi non conosce la
dimensione dello stato immagazzinato).
18/11/2009
UNICAM - p. 188/246
Memento: UML
Originator
Memento
-state
+setMemento(in Memento)
+createMemeonto()
-state
+getState()
+setState()
return new Memento(state)
18/11/2009
Caretaker
state=m.getState()
UNICAM - p. 189/246
Memento. Esempio 1
class Originator {
private String state;
public void set(String state) {
System.out.println("Setting state to " + state);
this.state = state;
}
public Object saveToMemento() {
System.out.println("Saving to Memento.");
return new Memento(state);
}
public void restoreFromMemento(Object m) {
if (m instanceof Memento) {
Memento memento = (Memento) m;
state = memento.getSavedState();
System.out.println("Restoring from Memento: "+state);
}}}
18/11/2009
UNICAM - p. 190/246
Memento. Esempio 2
private static class Memento {
private String state;
public Memento(String stateToSave) { state = stateToSave; }
public String getSavedState() { return state; }
}
18/11/2009
UNICAM - p. 191/246
Memento. Esempio 3
import java.util.*;
class Caretaker {
private List savedStates = new ArrayList();
public void addMemento(Object m) { savedStates.add(m); }
public Object getMemento(int index) {
return savedStates.get(index);
}
}
18/11/2009
UNICAM - p. 192/246
Memento. Esempio 4
class MementoExample {
public static void main(String[] args) {
Caretaker caretaker = new Caretaker()
Originator originator = new Originator();
originator.set("State1");
originator.set("State2");
caretaker.addMemento(originator.saveToMemento());
originator.set("State3");
caretaker.addMemento(originator.saveToMemento());
originator.set("State4");
originator.restoreFromMemento(caretaker.getMemento(1));
}
}
18/11/2009
UNICAM - p. 193/246
Memento. Esempio 5
Quando occorre, si può produrre lo stato dell’oggetto memorizzato chiamando
il metodo reset.
18/11/2009
UNICAM - p. 194/246
Observer
Il design pattern Observer viene in aiuto quando un oggetto (ConcreteSubject)
vuole notificare il suo cambio di stato (subjectState) a un gruppo di oggetti (i
ConcreteObserver) a lui dipendenti.
Esso si applica quando ci sono diversi oggetti interessati alle modifiche di un
oggetto. Un’applicazione tipica di questo pattern è nella visualizzazione di un
oggetto.
Usare il pattern di osservazione permette di separare l’oggetto che è
visualizzato dalla visualizzazione.
Noto come: Dependant, Publish-Subscribe.
18/11/2009
UNICAM - p. 195/246
Observer, scopo
Definire una relazione uno-a-molti tra oggetti, in modo che quando un oggetto
cambia stato tutti gli ascoltatori collegati sono notificati ed aggiornati.
18/11/2009
UNICAM - p. 196/246
Observer, applicabilità
L’Observer andrebbe usato quando:
▲ L’astrazione è composta da due aspetti, una dipendente dall’altra e si
desidera incapsulare le due astrazioni in oggetti separati;
▲ Il cambiamento di un oggetto richiede il cambiamento di altri, senza sapere
quali;
▲ Non si conosce a priori il numero degli oggetti dipendenti;
▲ Un oggetto deve notificare ad altri un cambiamento senza conoscere la
struttura degli oggetti dipendenti.
18/11/2009
UNICAM - p. 197/246
Observer, conseguenze
▲
▲
▲
▲
▲
▲
Accoppiamento astratto tra Subject e Observer: un Subject sa che ha una
lista di Observer, conformi ad un’interfaccia astratta (AbstractObserver), ma
non è a conoscenza delle classi concrete degli stessi;
Supporto alla comunicazione di tipo broadcast;
Aggiornamenti inattesi: se le interdipendenze non sono ben formate,
possono verificarsi aggiornamenti a cascata indesiderati;
In sintesi:
Maggiore modularità: Subject e Observer possono cambiare;
Maggiore elasticità: posso definire e aggiungere diversi Observer;
Maggiore flessibilità: posso agganciare diversi Observer ognuno dei quali
può implementare una differente vista.
18/11/2009
UNICAM - p. 198/246
Observer: UML
Subject
+attach(observer:Observer)
+detach(observer:Observer)
+notify()
ConcreteSubject
18/11/2009
Observer
-observers
0..* +update()
ConcreteObserver
UNICAM - p. 199/246
Observer. Esempio 1
Per esempio, si cunsideri il gioco della roulette:
class Roulette {
private int numero;
Random tavolo = new Random();
final private NUMERI = 36;
int getNumero() { return numero; }
void lancio() { numero = nextInt(NUMERI); }
//...
}
18/11/2009
UNICAM - p. 200/246
Observer. Esempio 2
Per permettere di osservare le modifiche, ovvero il risultato dei lanci, si crea
prima un’interfaccia che rappresenta gli osservatori:
interface Observer { void update(); }
18/11/2009
UNICAM - p. 201/246
Observer. Esempio 3
Dopo si può modificare la classe Subject per tenere conto dei suoi osservatori:
class Roulette implement Subject{
private int numero;
private Vector observers;
Random tavolo = new Random();
final private NUMERI = 36;
public int getNumero() { return numero; }
void lancio() {
numero = nextInt(NUMERI);
for (Enumeration e = observers.elements();
e.hasMoreElements(); )
((Observer)e.nextElements()).update();
}
public void attach(Observer o) { observers.add(o); }
}
18/11/2009
UNICAM - p. 202/246
Observer. Esempio 4
Un osservatore ha bisogno di registrarsi con il metodo attach() e dopo viene
avvertito con il metodo update() quando il valore è stato cambiato. Per
l’esempio della roulette vi possono essere diversi osservatori. Ad esempio
l’osservatore del pari:
class Pari implements Observer {
private int pari = 0;
Pari(Subject s) { s.attach(this); }
void update() { if (s.getNumero() % 2 == 0) pari++;
}
18/11/2009
UNICAM - p. 203/246
Observer. Esempio 5
L’osservatore del rosso:
class Rosso implements Observer {
private int rosso = 0;
static private int [] rossi = {2,4,6,8,10,11,13,15,17,20,
22,24,26,28,29,31,33,35};
Rosso(Subject s) { s.attach(this); }
void update() { if (in(s.getNumero()) rosso++; }
private boolean in(int n) {
for (int i = 0; i < rossi.length(); i++)
if (rossi[i]==n) return true;
return false;
}
}
18/11/2009
UNICAM - p. 204/246
State
Il design pattern State cambia il comportamento di un oggetto (il risultato
dell’operazione Request dell’oggetto Context) al cambiare dello stato
(ConcreteStateA, ConcreteStateB, ...).
Esso si applica quando c’è un oggetto che cambia stato. Invece di avere un
metodo che effettua dei test per sapere quale codice usare, si usano gli oggetti.
Noto come: Objects o States.
18/11/2009
UNICAM - p. 205/246
State, scopo
Descrivere il comportamento di un sistema mediante la composizione di azioni
intraprese in un numero finito di stati. In ciascuno stato vengono intraprese
singole azioni. La somma delle azioni determina il comportamento
complessivo del sistema.
Consentire ad un oggetto di modificare il proprio comportamento quando il suo
stato interno cambia.
18/11/2009
UNICAM - p. 206/246
State, applicabilità
Utilizzare lo State nei seguenti casi:
▲ Quando l’astrazione permette di descrivere il sistema come un automa a
stati finiti;
▲ Quando il cambiamento del comportamento del sistema in un determinato
stato non comporta la modifica del comportamento di altri stati;
▲ Quando non si conosce a priori il numero degli stati;
▲ Quando le azioni che vengono intrapese in uno stato non dipendono dalle
azioni intraprese in altri stati;
▲ Il comportamento di un oggetto dipende dal suo stato, e deve modificare il
proprio comportamento a run-time in base alla variazione dello stato;
▲ Le operazioni sono implementate mediante largo uso di statement
condizionali dipendenti dai valori assunti dalle variabili di stato: lo State pone
ciascun branch in una classe separata.
18/11/2009
UNICAM - p. 207/246
State, conseguenze
▲
▲
▲
▲
▲
▲
▲
Partiziona i comportamenti state-specific;
Rende esplicite le transizioni tra stati;
Protegge il Context dalla possibilità di passare in stati inconsistenti;
Possibilità di condividere (es. mediante un Flyweight) degli State objects;
In sistesi:
Maggiore modularità: il comportamento dell’intero sistema è la composizione
dei comportamente dei singoli stati;
Maggiore elasticità: si possono aggiungere nuovi stati senza modificare
pesantemente il codice;
Maggiore flessibilità: è possibile modificare il comportamento di uno stato
senza alterare il sistema.
18/11/2009
UNICAM - p. 208/246
State: UML
Context
+request()
-states
0..*
State
+handle()
states.handle()
18/11/2009
ConcreteStateA
ConcreteStateB
+handle()
+handle()
UNICAM - p. 209/246
State
▲
▲
▲
▲
L’applicazione del pattern State comporta a creazione di un
ConcreteContext e di un insime di ConcreteState
ConcreteContext descrive il comportamento del sistema. Mantiene un
riferimento allo stato concreto corrente
ConcreteContext invoca mediante il riferimento allo stato corrente
l’esecuzione delle operazioni associate allo stato mediante l’interfaccia di
operazione standard "handle()".
L’operazione "handle()" può avere come argomento "handle(Context c)" così
che i singoli stati possono indicare lo stato succssivo al Context.
18/11/2009
UNICAM - p. 210/246
State, esempio 1
Si vuole modellare il comportamento di un orogogio digitale con due pulsanti:
MODE e CHANGE. Il punsante MODE permette di selezionare le modalità:
"visualizzazione normale", "modifica dell’ora", "modifica dei minuti". Il secondo
pulsante CHANGE permette di accendere il display se la modalità di
funzionamento è normale, di incrementare le ore o i minuti se nelle modalità
"modifica dell’ora" o "modifica dei minuti".
18/11/2009
UNICAM - p. 211/246
State, esempio 2
CHANGE/Display()
Normal
Dispaly
MODE
MODE
CHANGE/IncHours()
Updating
Hours
MODE
Updating
Minutes
CHANGE/IncMinutes()
18/11/2009
UNICAM - p. 212/246
State, esempio 3
Il codice per Clock è il seguente:
class Clock extends Context {
private State currentState = new Display();
private String button;
public String getButton() { return button; }
public void setState(State s) { currentState = s; }
public void request() { /*...*/ }
}
dove gli stati Display, Hours e Minutes susseguono, applicando il pattern di
state, si crea prima la classe degli stati
18/11/2009
UNICAM - p. 213/246
State. Esempio 2
class NormalDisplay extends State {
void handle(Context c) {
if (c.getButton() == CHANGE) {
Display(); c.setState(new NormalDisplay());
}
if (c.getButton() == MODE) { c.setState(new UpdatingHours());
}
}
class UpdatingHours extends State {
void handle(Context c) {
if (c.getButton() == CHANGE) {
IncHours(); c.setState(new UpdatingHours());
}
if (c.getButton() == MODE) { c.setState(new UpdatingMinutes());
}
}
18/11/2009
UNICAM - p. 214/246
State. Esempio 3
class UpdatingMinutes extends State {
void handle(Context c) {
if (c.getButton() == CHANGE) {
IncMinutes(); c.setState(new UpdatingMinutes());
}
if (c.getButton() == MODE) { c.setState(new NormalDisplay());
}
}
18/11/2009
UNICAM - p. 215/246
State. Esempio 4
Il metodo handle(State s) di ogni stato esegue le operazioni di sua
competenza quindi termina determinando quale nuovo stato deve assumere il
sistema e lo comunica al contesto mediante il metodo setState(State s).
Quindi quando il controllo ritorna a request(), questi reinvocherà il metodo
handle(State s) dello stato attuale.
public void request() {
while(true) {
button = getButton();
currentState.handle(this);
}
}
In request() viene effettuata la lettura del tasto.
18/11/2009
UNICAM - p. 216/246
Strategy
Il design pattern Strategy incapsula una famiglia di algoritm in una famiglia
(Strategy) di classi (ConcreteStrategyA, ConcreteStrategyB, ...) rendendoli
intercambiabili in base alla situazione (Context). La scelta di quale algoritmo
della famiglia utilizzare è determinata dal contesto in cui viene impiegato
(Context), poi l’utilizzo di un algoritmo piuttosto che l’altro è del tutto
trasparente.
Esso è molto simile a quello di stato. Il pattern di strategia permette di avere
dei comportamenti a scelta. Si applica in situazioni dove esistono diverse
strategie per fare una cosa ma l’utente (o l’oggetto) ne sceglie una solamente.
Noto come: Policy.
18/11/2009
UNICAM - p. 217/246
Strategy, scopo
Definire una famiglia di algoritmi, incapsularli e renderli intercambiabili in
maniera trasparente rispetto all’uso da parte del client.
18/11/2009
UNICAM - p. 218/246
Strategy, applicabilità
Lo Strategy è utile se:
▲ Classi collegate differiscono solo per il loro comportamento (es. algoritmi di
compressione che minimizzano lo spazio/minimizzano il tempo);
▲ Gli algoritmi utilizzano dati non a conoscenza del client;
▲ Una classe definisce differenti comportamenti, espressi come statement
condizionali multipli nelle operazioni (il problema si risolve in maniera simile
al caso del pattern State).
18/11/2009
UNICAM - p. 219/246
Strategy, conseguenze
▲
▲
▲
▲
▲
▲
▲
Creazione di famiglie di algoritmi collegati;
E’ un’alternativa al subclassing;
Eliminazione degli statement condizionali;
Possibilità di scelta a run-time di una implementazione;
Il client deve essere a conoscenza delle differenti strategie prima di
selezionarne una;
Possibilità di overhead di comunicazione tra Strategy e Context;
Il numero di oggetti cresce.
18/11/2009
UNICAM - p. 220/246
Strategy: UML
Context
strategy
+contexInterface()
Strategy
+algorithmInterface()
ConcreteStrategyA
ConcreteStrategyB
ConcreteStrategyC
+algorithmInterface()
+algorithmInterface()
+algorithmInterface()
18/11/2009
UNICAM - p. 221/246
Strategy. Esempio 1
Per esempio, per implementare una classe A che stampa un oggetto in due
modi diversi (postscript o pdf) si può scrivere come segue:
abstract class Strategy {
abstract void print(Object o);
}
class PS extends Strategy {
void print(Object o) { ... }
}
class PDF extends Strategy {
void print(Object o) { ... }
}
18/11/2009
UNICAM - p. 222/246
Strategy. Esempio 2
class Context {
private Strategy strategy;
void setStrategy(Strategy s) { strategy = s; }
void print(Object o, String s) {// imposta il ConcreteStrategy
if ("ps".equals(s)) {
setStrategy(new PS());
} else if ("pdf".equals(s)) {
setStrategy(new PDF());
}
print(o);
}
void print(Object o) { strategy.print(o); }
}
18/11/2009
UNICAM - p. 223/246
Template
Il design pattern Template Method definisce lo scheletro di un algoritmo (nel
metodo TemplateMethod di AbstractClass) e delega ad una classe derivata
(ConcreteClass) la possibilità di ridefinire alcuni passi (PrimitiveOperation1,
PrimitiveOperation2, ...) dell’algoritmo (nel metodo TemplateMethod) senza
bisogno di modificare l’intero algoritmo o la sua struttura.
Esso permette di scrivere metodi generici. Tale pattern è la base della
programmazione orientata agli oggetti. Per istanziare questo metodo si usa
l’ereditarietà dando un’implementazione alle operazioni sulle quali si appoggia
il metodo generico.
18/11/2009
UNICAM - p. 224/246
Template, scopo
Definire lo scheletro di un algoritmo in un’operazione, delegando alcuni
substeps alle sottoclassi, le quali possono modificarli senza impattare la
struttura dell’algoritmo.
18/11/2009
UNICAM - p. 225/246
Template, applicabilità
Il template method andrebbe usato:
▲ Per implementare parti invarianti di un algoritmo e lasciare alle sottoclassi
l’implementazione dei comportamenti variabili;
▲ Quando risulta opportuno “clusterizzare” comportamenti comuni tra
sottoclassi per evitare duplicazione di codice;
▲ Per controllare estensioni di sottoclassi.
18/11/2009
UNICAM - p. 226/246
Template, conseguenze
Il Template Method è fondamentale per effettuare riuso, particolarmente nella
realizzazione di librerie di classi, in quanto costituisce un metodo per
fattorizzare comportamenti comuni.
18/11/2009
UNICAM - p. 227/246
Template: UML
AbstractClass
+templateMethod()
+primitiveOperation1()
+primitiveOperation2()
...
primitiveOperation1()
...
primitiveOperation2()
...
ConcreteClass
+primitiveOperation1()
+primitiveOperation2()
18/11/2009
UNICAM - p. 228/246
Template. Esempio 1
Per esempio, consideriamo un metodo generico per disegnare un quadrato. Se
si dispone di un metodo per andare avanti e di un metodo per girare a destra,
un metodo generico può essere definito andando avanti, girando, avanti,
girando, avanti. Tale metodo generico si scrive in Java nel modo seguente:
abstract class A {
abstract void goForth(); // primitiveOperation1
abstract void goRight(); // primitivaOperation2
final void doSquare() { // templateMethod
goForth(); goRight();
goForth(); goRight();
goForth(); goRight();
goForth();
}
}
18/11/2009
UNICAM - p. 229/246
Template. Esempio 2
Adesso nelle class che derivano da A ogni volta che si darà
un’implementazione di goForth e goRight si erediterà un metodo per fare dei
quadrati. La cosa importante è che se il codice di doSquare è stato scritto
prima dell’implementazione di goForth e goRight si userà comunque tale
implementazione nell’esecuzione.
18/11/2009
UNICAM - p. 230/246
Visitor
Il design pattern Visitor si applica quando si ha un insieme di oggetti e c’è
bisogno di "visitare" questi oggetti per effettuare la computazione. L’idea del
pattern di visita è che questa visita si può fare in modo esterno all’oggetto,
occorre solamente che l’oggetto preveda di essere visitato.
18/11/2009
UNICAM - p. 231/246
Visitor, scopo
Rappresentare un’operazione da eseguire sugli elementi di una struttura. Il
Visitor consente di definire nuove operazioni senza modificare le classi degli
elementi su cui operare.
Esso rende indifferenti gli oggetti da computare rispetto alla computazione.
18/11/2009
UNICAM - p. 232/246
Visitor, applicabilità
Il visitor andrebbe usato quando:
▲ Una struttura di oggetti contiene diverse classi con diverse interfacce, è si
desidera eseguire le operazioni in base alle classi concrete;
▲ Occorre eseguire sulla struttura diverse operazioni distinte e scorrelate, e si
desidera evitare di complicare l’interfaccia delle classi della struttura,
aggiungendo le diverse operazioni;
▲ E’ possibile separare l’algoritmo da applicare agli oggetti dagli oggetti stessi;
▲ Si vogliono adottare diversi schemi di elaborazione sugli stessi oggetti;
▲ Gli oggetti a cui si applica la computazione possono subire alterazioni dello
stato senza che ciò comporti alterazioni dell’agoritmo.
▲ IMPORTANTE: le classi della struttura cambiano raramente, ma occorre
spesso invece definire nuove operazioni. Modifiche alla struttura
richiederebbero la ridefinizione dell’interfaccia di tutti i visitor;
18/11/2009
UNICAM - p. 233/246
Visitor, conseguenze
▲
▲
▲
▲
▲
▲
▲
L’aggiunta di nuove operazioni è agevole;
Le operazioni correlate sono messe assieme, quelle scorrelate sono
separate;
E’ difficile aggiungere nuove classi di ConcreteElement;
I visitor consentono di accumulare stato;
Il visitor assume che il ConcreteElement abbia un’interfaccia in grado di
consentirgli di svolgere il proprio compito. Ciò forza la pubblicazione di
alcune operazioni del ConcreteElement, in contrasto con i meccanismi di
incapsulamento;
Indipendenza degli algoritmi dagli oggetti da computare;
Applicazione di diversi algoritmi a diversi oggetti senza conseguenze nel
client.
18/11/2009
UNICAM - p. 234/246
Visitor: UML
ConcreteVisitor1
ConcreteVisitor2
+VisitConcreteElementA(ConcreteElementA)
+VisitConcreteElementA(ConcreteElementA)
+VisitConcreteElementB(ConcreteElementB)
+VisitConcreteElementB(ConcreteElementB)
Visitor
+VisitConcreteElementA(ConcreteElementA)
+VisitConcreteElementB(ConcreteElementB)
Client
ObjectStructure
visitor.visitConcreteElement(this)
visitor.visitConcreteElement(this)
ConcreteElementA
ConcreteElementB
Element
+accept(Visitor)
+accept(Visitor)
+Accept(Visitor)
+operationA()
18/11/2009
+operationB()
UNICAM - p. 235/246
Visitor. Esempio 1
Tale pattern è molto utile per esempio nel caso di un oggetto composito. Per
esempio, consideriamo gli alberi binari. Abbiamo una classe astratta
abstract class Tree { }
e due sottoclassi
18/11/2009
UNICAM - p. 236/246
Visitor. Esempio 2
class Node extends Tree {
private String name;
private Tree left;
private Tree right;
Node(String n, Tree l, Tree r) { right = r; left = l; name = n; }
public String getName() { return name; }
public Tree getLeft() { return left; }
public Tree getRight() { return right; }
}
18/11/2009
UNICAM - p. 237/246
Visitor. Esempio 3
e
class Leaf extends Tree {
private int value;
Leaf (int v) { value = v; }
public int getValue() { return value; }
}
Adesso un visitatore per questa classe sarà un oggetto che ha due metodi, uno
per ogni sottoclasse:
abstract class Visitor {
abstract void visit(Node node);
abstract void visit(Leaf leaf);
}
18/11/2009
UNICAM - p. 238/246
Visitor. Esempio 4
La prima cosa da fare è di permettere a ogni componente di un albero di
accettare il visitatore. Si fa con il metodo accept:
abstract class Tree {
abstract void accept(Visitor v);
}
18/11/2009
UNICAM - p. 239/246
Visitor. Esempio 5
class Node extends Tree {
private String name;
private Tree left;
private Tree right;
Node(String n, Tree l, Tree r) {
right = r; left = l; name = n;
}
public String getName() { return name; }
public Tree getLeft() { return left; }
public Tree getRight() { return right; }
public void accept(Visitor v) { v.visit(this); }
}
18/11/2009
UNICAM - p. 240/246
Visitor. Esempio 6
class Leaf extends Tree {
private int v;
Leaf (int value) { value = v; }
public int getValue() { return value; }
public void accept(Visitor v) { v.visit(this); }
}
18/11/2009
UNICAM - p. 241/246
Visitor. Esempio 7
Adesso per scrivere un visitatore si può estendere la classe Visitor. Per
esempio, possiamo scrivere un visitatore che stampa gli elementi dell’albero in
modo prefisso:
class PrefixVisitor extends Visitor {
void visit(Node node) {
System.out.println(node.getName());
node.getLeft().accept(this);
node.getRight().accept(this);
}
void visit(Leaf leaf) {
System.out.println(leaf.getValue());
}
}
18/11/2009
UNICAM - p. 242/246
Visitor. Esempio 8
un altro in modo postfisso:
class PostfixVisitor extends Visitor {
void visit(Node node) {
node.getLeft().accept(this);
node.getRight().accept(this);
System.out.println(node.getName());
}
void visit(Leaf leaf) {
System.out.println(leaf.getValue());
}
}
18/11/2009
UNICAM - p. 243/246
Visitor. Esempio 9
Dato l’albero
Tree t = new Node("a", new Node("b", new Leaf(1),
new Leaf(2)), new Leaf(3));
t.accept(new PrefixVisitor());
produce
a
b
1
2
3
18/11/2009
UNICAM - p. 244/246
Visitor. Esempio 10
Mentre
t.accept(new PostfixVisitor());
produce
1
2
b
3
a
18/11/2009
UNICAM - p. 245/246
Visitor. Esempio 11
Usando il pattern di visita si possono aggiungere funzionalità ad una class fuori
della sua definizione. Il fatto che il meccanismo di visita sia così complicato
(accept chiama visit che richiama accept) è una conseguenza del fatto che
l’overloading è risolto staticamente.
18/11/2009
UNICAM - p. 246/246