Design Pattern
Short Description
Descrizione in italiano di tutti i Design Pattern ed esempi di implementazione in Java...
Description
Design Pattern DESIGN PATTERN CREAZIONALI Factory Method (Class) Problema Una classe deve creare un oggetto che implementa un’interfaccia, ma non deve dipendere dalla specifica implementazione concreta tra quelle disponibili. Ossia si delega ad una sottoclasse la creazione dell’oggetto.
Soluzione Gli oggetti vengono creati con un metodo:
Il metodo può stare nella classe che usa gli oggetti oppure in un’altra classe come metodo static; Il metodo può essere abstract e quindi deve essere implementato in una sottoclasse.
Struttura Creator: dichiara il Factory Method; ConcreteCreator: implementa il Factory Method al fine di ritornare l’oggetto; Product: interfaccia dell’oggetto da creare; ConcreteProduct: implementa l’oggetto ed i metodi della sua interfaccia.
Conseguenze
Si può cambiare la sottoclasse concreta da utilizzare senza avere alcun impatto sul client, ossia si può estendere il comportamento della classe base cambiando il tipo di prodotto creato. Consente di collegare gerarchie di classi in modo parallelo (ConcreteCreator e ConcreteProduct).
Esempi
Creazione di una connessione ad un database Connection conn = DriverManager.getConnection(…);
Creazione di un iteratore Iterator iter = MyCollection.iterator();
Abstract Factory (Object) Problema Una classe deve creare una serie di oggetti che implementano interfacce correlate, ma si vuole evitare che dipenda da una specifica implementazione concreta; ossia abbiamo delle famiglie di oggetti interconnessi, che vengono utilizzate nel loro insieme. Può essere utile anche per creare una libreria di oggetti rivelando solo le interfacce e non le classi concrete.
Soluzione Si definisce l’interfaccia AbstractFactory che ha i metodi per creare i diversi prodotti e una o più classi concrete che implementano l’interfaccia creando una famiglia di prodotti. La creazione della ConcreteFactory avviene a run-time e può essere realizzata anche con un Factory Method.
Struttura
AbstractFactory: interfaccia che contiene le firme dei metodi per creare i prodotti; ConcreteFactory: implementazione dei metodi per creare i prodotti; AbscractProduct: interfaccia che contiene le firme dei metodi relativi ai prodotti; Product: implementazione dei metodi relativi ai prodotti.
Conseguenze
Nasconde le classi concrete, le quali sono istanziate solo dalle Factory e non direttamente dal client; Si può sostituire facilmente una famiglia di prodotti con un’altra senza dover modificare il client; I prodotti devono essere della stessa famiglia dato che implementano lo stesso AbstractProduct; Svantaggio: è oneroso aggiungere nuovi prodotti ad una famiglia in quanto questo implica la modifica di tutte le ConcreteFactory.
Esempio La libreria AWT deve poter creare per ogni componente un oggetto la cui implementazione dipende dalla piattaforma utilizzata. Si risolve rendendo la classe java.awt.Toolkit una AbstractFactory, per la quale viene creata una sottoclasse concreta con un Factory Method
(Toolkit.getDefaultToolkit). La sottoclasse concreta viene scelta in base alla configurazione della JVM.
Singleton (Object) Problema Vogliamo che una classe abbia un’unica istanza, accessibile attraverso un unico punto di accesso; può essere necessario quando la classe contiene informazioni di stato che devono essere condivise da più parti del programma.
Soluzione Si rende il costruttore della classe privato (non accessibile dall’esterno); quindi si fornisce un metodo static che restituisce l’unica istanza della classe conservata in un campo static privato.
Struttura
Conseguenze
Non occorrono variabili globali per accedere all’unica istanza; È semplice estendere la classe singleton senza modificare il codice che la usa; L’accesso all’unica istanza è controllata; Se necessario si può passare da una a più istanze.
Esempio Nella libreria AWT la classe SystemTray è un Singleton, dato che sul desktop ci può essere un solo system tray.
Implementazione import java.util.*; public class Cache { static private Cache instance=null; public static Cache getCache() { if (instance==null) instance=new Cache(); return instance; } private Map map; private Cache() { map=new HashMap(); } public String get(String key) { return map.get(key); } public void put(String key, String value) {
}
map.put(key, value);
}
DESIGN PATTERN STRUTTURALI Adapter (Class - Object) Problema Vogliamo riutilizzare una classe già disponibile insieme ad una libreria di classi sviluppata indipendentemente, la quale richiede una particolare interfaccia.
Soluzione Class Si crea una classe Adapter che implementa l’interfaccia Target ed eredita l’implementazione della classe Adaptee; i metodi di Adapter richiamano quelli di Adaptee.
Object Si crea una classe Adapter che implementa l’interfaccia Target e contiene un riferimento ad un oggetto della classe Adaptee; i metodi di Adapter richiamano quelli di Adaptee. Alternativa: la classe Adapter può essere realizzata come classe interna dell’Adaptee; così l’Adapter ha accesso anche alle componenti private di Adaptee e non si aggiungono nuove classi visibili all’esterno.
Struttura Class
Object
Adapter: definisce l’interfaccia che adatta i metodi dell’Adaptee all’interfaccia Target; Target: interfaccia specifica della libreria utilizzata; Adaptee: è la classe da interfacciare.
Conseguenze
Class Se Target non è un’interfaccia pura, è necessaria l’ereditarietà multipla; Un Adapter adatta un solo Adaptee, pertanto se ci sono più Adaptee occorrono anche più Adapter; Viene creato solamente l’oggetto Adapter che include al suo interno sia Target sia Adaptee, pertanto c’è un basso overhead a tempo di esecuzione.
Object L’Adapter può essere associato a più Adaptee; L’Adapter può essere utilizzato per oggetti della classe Adaptee e delle sue classi derivate; Si può cambiare l’Adaptee associato a run-time; Adapter e Adaptee rimangono 2 oggetti distinti (overhead di memoria).
Esempio (Object) Gestione degli eventi in Java (ActionListener, MouseAdapter): le azioni vengono associate agli eventi attraverso un Object Adapter che implementa i metodi dell’interfaccia Listener richiamando i metodi dell’oggetto che effettivamente esegue l’azione.
Composite (Object) Problema Vogliamo gestire oggetti (Component) che possono essere sia semplici sia complessi (composti da più oggetti semplici, eventualmente in maniera gerarchica); si vuole rendere il client indipendente dal fatto che stia usando componenti semplici o complessi.
Soluzione Si definisce la classe Composite che implementa l’interfaccia di Component ed ha al suo interno un insieme di component; il Composite invoca i metodi di Component su tutti i Component da cui è costituito.
Struttura
Component: interfaccia degli oggetti; Leaf: rappresenta l’oggetto singolo e definisce il comportamento base degli oggetti; Composite: rappresenta l’oggetto composto e definisce il comportamento dell’oggetto contenitore ed ha il riferimento ai componenti figli.
Conseguenze
Semplifica il client nascondendo la differenza tra oggetti semplici e composti; È semplice aggiungere nuovi componenti; Problema: è difficile porre dei vincoli sulla composizione (es. un oggetto può avere solo un tipo di componenti).
Esempio
Gerarchia dei componenti grafici in AWT: il componente grafico deriva da java.awt.Component, mentre esiste la classe java.awt.Container che rappresenta un Component che contiene altri Component.
Decorator (Object) Problema Vogliamo aggiungere a run-time delle responsabilità a un oggetto Component senza cambiarne l’interfaccia; non è possibile usare l’ereditarietà.
Soluzione Si definisce una classe Decorator che implementa l’interfaccia di Component ed ha al suo interno un riferimento al Component che viene decorato; nei metodi del decorator vengono aggiunte le pre o post-elaborazioni necessarie e quindi si richiama il metodo del Component.
Struttura
Component: l’interfaccia degli oggetti da decorare; ConcreteComponent: oggetto a cui bisogna aggiungere funzionalità; Decorator: interfaccia conforme a quella del Component che mantiene l’associazione con l’oggetto Component; ConcreteDecorator: implementa l’interfaccia Decorator ed aggiunge le nuove funzionalità all’oggetto.
Conseguenze
È trasparente per gli utenti del Component (basta sostituire al posto dei riferimenti al Component quelli al Decorator); Si possono applicare più Decorator in cascata; I Decorator possono essere decisi a run-time ed essere aggiunti o rimossi all’occorrenza.
Esempio La libreria I/O di Java la quale ha una classe FilterInputStream che è un Decorator (vi sono 4 ConcreteDecorator, in verde nella figura) e può essere applicato ai componenti concreti dell’interfaccia base InputStream (in giallo).
Facade (Object) Problema Vogliamo nascondere al client la complessità interna di un sistema costituito da numerosi interfacce e classi; si vuole rendere le funzioni del sistema accessibili semplicemente interagendo con una sola classe.
Soluzione Si realizza una classe Facade (di “facciata”), la quale ha al suo interno i riferimenti agli oggetti che compongono il sistema; il client vede solo Facade e i metodi di quest’oggetto attivano le opportune operazioni del sistema.
Struttura Facade: classe di interfaccia tra client e sistema.
Conseguenze
L’interazione con il sistema incapsulato diventa più semplice; Si riducono le associazioni tra client e sistema; È semplice modificare il sistema senza dover modificare l’interfaccia della Facade ed il client; Problema: alcune funzionalità possono non essere accessibili attraverso la Facade, ma può risultare necessario in alcune situazioni accedere comunque al sistema.
Esempio In Java la classe org.junit.runner.JUnitCore è una Facade per accedere al sottosistema di JUnit che si occupa di lanciare i test.
Proxy (Object) Problema Vogliamo utilizzare un oggetto Proxy al posto di un altro oggetto Subject, in quanto l’accesso al Subject è complesso e vogliamo nascondere tale complessità.
Soluzione Si realizza un oggetto Proxy che implementa l’interfaccia del Subject; invocando i metodi del Proxy, dopo le opportune operazioni per l’accesso e la comunicazione, vengono invocati i corrispondenti metodi del Subject.
Struttura SubjectInterface: interfaccia che presenta le operazioni eseguibili dall’oggetto e viene utilizzato dal client; RealSubject: oggetto reale che deve essere interfacciato dal Proxy; Proxy: oggetto che implementa l’interfaccia del Subject e mantiene al suo interno un riferimento al RealSubject.
Conseguenze
Il Proxy fornisce un livello di indirezione nell’accesso al Subject reale, che viene utilizzato per nascondere al client la complessità dei meccanismi di accesso. Proxy vs Decorator: i pattern differiscono solo per il loro campo di utilizzo; il Decorator aggiunge responsabilità ad un oggetto, il Proxy nasconde il meccanismo con cui si accede ad un oggetto.
Esempi
Remote Proxy: il Subject reale si trova in un processo diverso da quello del client o anche su un altro computer (potrebbe anche girare su una diversa piattaforma o essere scritto in un diverso linguaggio di programmazione); il Proxy si occupa di utilizzare i servizi di Inter-Process-Communication e di rete per interfacciarlo. Virtual Proxy (Lazy Instantiation): il Subject reale è oneroso da istanziare, pertanto si usa un Proxy al suo posto il quale crea il Subject reale solo quando viene richiamato per la prima volta un metodo non gestibile direttamente dal Proxy. Future Proxy: il Subject reale richiede molto tempo per essere creato; per evitare di bloccare il programma, esso viene creato in un thread secondario mentre al suo posto viene utilizzato il Proxy nel programma; se viene richiamato un metodo che il Proxy non è in grado di gestire ed il Subject reale non è ancora pronto, viene introdotta un’attesa o restituito un valore nullo (le immagini in AWT vengono gestite in questo modo). Smart Proxy: il Subject usa meccanismi speciali per l’allocazione e la deallocazione e si vuole nasconderli al client, pertanto si usa un Proxy con meccanismi normali che si occupa di gestire quelli speciali richiesti dal Subject (in C++ la classe String usa uno Smart Proxy per nascondere l’allocazione dinamica, il quale si occupa anche di
decidere quando deallocare la stringa attraverso il reference counting e utilizza il Copy on Write per evitare di dover creare una copia della stringa ogni volta che viene passata ad un sottoprogramma). Protection Proxy: viene utilizzato per aggiungere controlli di sicurezza nell’accesso alle operazioni del Subject, che non sono già previsti dal Subject reale (es. utilizzo in un contesto untrusted, come un’applicazione scaricata da internet, di una classe pensata per un contesto trusted).
DESIGN PATTERN COMPORTAMENTALI Template Method (Class) Problema Vogliamo definire la struttura di un algoritmo delegando alcune operazioni a sottoclassi; questo può essere utile quando abbiamo più problemi simili che si risolvono con algoritmi che hanno la stessa struttura. In tal modo si può riutilizzare la struttura comune per diversi algoritmi dello stesso tipo, rispettando il principio DRY (Don’t Repeat Yourself).
Soluzione Si definisce una classe abstract che contiene lo schema dell’algoritmo all’interno di un metodo (Template Method) che richiama altri metodi (Hook Method) per le parti che differiscono tra diversi problemi. Gli Hook Method possono essere abstract o avere un’implementazione di default. Per ogni problema si crea una sottoclasse che ridefinisce i metodi hook.
Struttura AbstractClass: definisce il metodo concreto ed i metodi hook astratti; ConcreteClass: implementa i metodi hook.
Conseguenze
Si separa la struttura generale comune ad un insieme di algoritmi dai dettagli differenti, in modo da poterla riutilizzare; Le soluzioni a problemi analoghi saranno coerenti; È la classe base a richiamare i metodi specifici per il particolare problema (Hollywood Principle: “Don’t call us, we’ll call you”); Bisogna evitare di definire troppi metodi hook, altrimenti il Template Method risulta difficile da utilizzare (seguire la raccomandazione YAGNI: You Ain’t Gonna Need It).
Esempio
Framework per definire applicazioni in cui la struttura generale dell’applicazione è fissata e bisogna specificare soltanto gli elementi peculiari: Applet, Servlet.
Implementazione public abstract class Accumulator { // Template method public int compute(int a[], int n) { int value=initialValue(); int i; for(i=0; i0) return currentValue+1; else return currentValue; }
Chain of Responsibility (Object) Problema Vogliamo disaccoppiare il client che genera una richiesta dallo specifico destinatario che deve gestirla; il mittente non deve sapere chi gestirà la richiesta, basta solamente che sappia chi è il primo handler della catena a cui inviare la richiesta.
Soluzione Si definisce una classe astratta Handler, i cui oggetti concreti hanno un metodo per gestire la richiesta ed un riferimento all’Handler successivo; la richiesta viene inviata alla catena di Handler ed ognuno deciderà se gestirla o passarla al successivo.
Struttura Handler: definisce l’interfaccia per gestire le richieste ed implementa il collegamento al successivo; ConcreteHandler: gestisce le richieste di cui è responsabile e propaga le altre al suo successore.
Conseguenze
C’è un basso accoppiamento tra il client che esegue la richiesta e la classe che effettivamente la gestisce; Ciascun Handler seleziona da solo le richieste che è in grado di gestire; La catena di responsabilità permette di definire una priorità tra gli Handler; Si può modificare la catena senza che il client ne sia affetto; È opportuno avere alla fine della catena un Handler che catturi tutte le richieste non evase dagli Handler precedenti; Problema: se il numero di Handler è elevato, aumenta l’overhead necessario per gestire ogni richiesta.
Esempi
La propagazione delle eccezioni in Java è una catena di responsabilità, infatti se il gestore dell’errore non riesce a gestirlo lo propaga nella catena; Nella versione 1.0 di AWT gli eventi generati dai componenti venivano gestiti dal componente stesso o venivano propagati al componente padre.
Implementazione public abstract class Parameters { protected Parameters next; protected Parameters(Parameters next) { this.next = next; }
}
public abstract String getParameter(String name);
public class CommandLineParameters extends Parameters { private String args[]; public CommandLineParameters(String args[], Parameters next) { super(next); this.args=args; } public String getParameter(String name) { String value=search(name); if (value==null && next!=null) value=next.getParameter(name); return value; } private String search(String name) { // Search the parameter in the command line } } public class ConfigurationFileParameters extends Parameters { private String fileName; public ConfigurationFileParameters(String fileName, Parameters next) { super(next); this.fileName=fileName; }
}
public String getParameter(String name) { String value=search(name); if (value==null && next!=null) value=next.getParameter(name); return value; } private String search(String name) { // Search the parameter in the file }
public class DefaultParameters extends Parameters { private Map map;
}
public DefaultParameters(Parameters next) { super(next); map=new HashMap(); map.put("param1", "pluto"); map.put("param2", "paperino"); } public String getParameter(String name) { String value=map.get(name); if (value==null && next!=null) value=next.getParameter(name); return value; }
public static void main(String args[]) { Parameters defaultParam=new DefaultParameters(null); Parameters cfgFileParam=new ConfigurationFileParameters("pippo.cfg", defaultParam); Parameters cmdLineParam=new CommandLineParameters(args, cfgFileParam);
}
MyClass x=new MyClass(cmdLineParam);
Command (Object) Problema Un sistema può dover eseguire azioni che hanno per oggetto altre azioni del sistema (comando undo, salvataggio di una macro, comandi di personalizzazione della barra dei menu).
Soluzione Si definisce una interfaccia/classe astratta Command che contiene i metodi per eseguire un comando; ogni azione viene realizzata con una classe concreta che implementa Command.
Struttura
Invoker: effettua l’invocazione del comando; Command: interfaccia che contiene i metodi per eseguire un comando; ConcreteCommand: implementazione del comando; Receiver: riceve il comando e sa come eseguirlo.
Conseguenze
C’è disaccoppiamento tra la definizione dei comandi e la loro esecuzione; È possibile creare comandi composti, comandi che manipolano altri comandi; È possibile estendere facilmente l’insieme dei comandi; Svantaggio: è più complesso definire il singolo comando rispetto alla semplice definizione di un metodo, pertanto il pattern è conveniente solo se dobbiamo gestire comandi che lavorano su altri comandi.
Implementazione public class Account { private double balance; // Saldo del conto
}
public Account(double initialBalance) { balance=initialBalance; } // Restituisce il saldo public double getBalance() { return balance; } // Esegue un versamento public void deposit(double amount) { balance += amount; } // Esegue un prelievo public void withdraw(double amount) { balance -= amount; }
public abstract class Command { protected Account account; protected Command(Account account) { this.account = account; } public abstract void perform(); public abstract void undo(); } public class DepositCommand extends Command { private double amount; public DepositCommand(Account account, double amount) { super(account); this.amount=amount; } public void perform() { account.deposit(amount); } public void undo() { account.withdraw(amount); } } public class WithdrawCommand extends Command { private double amount; public WithdrawCommand(Account account, double amount) { super(account); this.amount=amount; } public void perform() { account.withdraw(amount); } public void undo() { account.deposit(amount); } }
import java.util.Stack; public class AccountManager { private Account account; private Stack commandHistory; public AccountManager(Account account) { this.account=account; commandHistory=new Stack(); }
}
public double getBalance() { return account.getBalance(); } public void deposit(double amount) { Command cmd=new DepositCommand(account, amount); commandHistory.push(cmd); cmd.perform(); } public void withdraw(double amount) { Command cmd=new WithdrawCommand(account, amount); commandHistory.push(cmd); cmd.perform(); } public void undo() { Command last=commandHistory.pop(); last.undo(); }
Iterator (Object) Problema Si vuole consentire al client di accedere agli elementi di un aggregato senza dipendere dalla struttura dati effettivamente utilizzata.
Soluzione Si definisce un’interfaccia Iterator che rappresenta una posizione all’interno dell’aggregato; l’Iterator fornisce metodi per verificare se ci sono altri elementi, per accedere all’elemento corrente e per spostarsi nell’aggregato. L’implementazione concreta dell’Iterator è associata all’implementazione dell’aggregato: è possibile ottenere l’Iterator attraverso un Factory Method.
Struttura
Iterator: interfaccia con i metodi per accedere alla struttura dati; ConcreteIterator: implementazione dell’iteratore; Aggregate: interfaccia dell’aggregato che contiene un metodo per creare l’iteratore; ConcreteAggregate: implementa l’aggregato ed il metodo per creare l’iteratore.
Conseguenze
Si disaccoppia il client dalla conoscenza dell’implementazione dei dati; Con alcuni vincoli sulla collezione si possono aumentare le operazioni effettuabili con l’iteratore; La collezione può essere gestita anche da diversi iteratori che implementano algoritmi di navigazione differenti.
Esempio Gli iteratori per le collezioni nella libreria standard di Java.
Observer (Object) Problema Vogliamo fare in modo che i cambiamenti nello stato di un oggetto Subject vengano riflessi su altri oggetti dipendenti da esso; inoltre si vuole disaccoppiare il Subject dagli oggetti dipendenti.
Soluzione Si definisce un’interfaccia Observer con un metodo che viene richiamato ad ogni modifica del Subject; gli oggetti, che implementano Observer, devono registrarsi per gli eventi a cui sono interessati presso il Subject; a sua volta il Subject ogni qualvolta cambia il proprio stato richiama il metodo di notifica per tutti gli Observer registrati.
Struttura
Subject: interfaccia che contiene i metodi per iscriversi e cancellarsi e la lista degli osservatori iscritti; ConcreteSubject: oggetto che viene osservato, il quale notifica gli osservatori in caso di un cambio di stato; Observer: interfaccia che contiene il metodo per aggiornare gli osservatori in caso di modifica del Subject; ConcreteObserver: implementa l’interfaccia dell’Observer definendo il comportamento in caso di modifica del Subject.
Conseguenze
Il Subject è disaccoppiato dagli oggetti che dipendono da esso, migliorando la riusabilità e la testabilità; Il Subject notifica tutti gli Observer in modalità broadcast, senza necessità di sapere quali e quanti sono; Observer vs Chain of Responsibility: nel primo tutti gli oggetti che osservano sono allo stesso livello e possono anche rispondere insieme, nel secondo gli handler hanno una diversa priorità e si assume che solo uno di essi gestisca la richiesta.
Esempi
La gestione degli eventi nella libreria AWT (i Listener hanno il ruolo di Observer, mentre i componenti dell’interfaccia rappresentano il Subject). La libreria Java provvede una classe (java.util.Observable) e un’interfaccia (java.util.Observer) per implementare il pattern.
Implementazione import java.util.Observable; public class AlertCondition extends Observable { public static final int GREEN=0, YELLOW=1, RED=2; private int condition; public AlertCondition() { condition=GREEN; } public int getCondition() { return condition; } public void setCondition(int newCondition) { if (newCondition!=RED && newCondition!=YELLOW && newCondition!=GREEN) throw new RuntimeException("Unvalid alert condition!"); if (newCondition != condition) { condition=newCondition; setChanged(); } notifyObservers(); } } import java.util.*; import java.io.*; import java.text.*; public class LogAlertObserver implements Observer { private PrintWriter out; public LogAlertObserver(String fileName) throws IOException { FileOutputStream fos=new FileOutputStream(fileName, true); OutputStreamWriter osw=new OutputStreamWriter(fos, "UTF-8"); out=new PrintWriter(osw); } public void update(Observable subject, Object arg) { AlertCondition alert=(AlertCondition)subject; DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG); String date=dfmt.format(new Date()); String state; switch (alert.getCondition()) { case AlertCondition.GREEN: state="GREEN"; break; case AlertCondition.YELLOW: state="YELLOW"; break; case AlertCondition.RED: state="RED"; break; default: state="UNKNOWN"; }
} }
out.println("["+date+"] the alert is: "+state); out.flush();
import java.awt.*; import java.awt.event.*; import java.util.*; public class GraphicAlertObserver extends Frame implements Observer { private Canvas canvas; public GraphicAlertObserver() { super("Alert status"); setSize(400, 300); canvas=new Canvas(); canvas.setBackground(Color.GREEN); add("Center", canvas); setVisible(true); } public void update(Observable subject, Object arg) { AlertCondition alert=(AlertCondition)subject; switch (alert.getCondition()) { case AlertCondition.GREEN: canvas.setBackground(Color.GREEN); break; case AlertCondition.YELLOW: canvas.setBackground(Color.YELLOW); break; case AlertCondition.RED: canvas.setBackground(Color.RED); break; default: canvas.setBackground(Color.GRAY); break; } canvas.repaint(); } } import java.util.*; import java.applet.*; import java.net.*; public class SoundAlertObserver implements Observer { private AudioClip clip; public SoundAlertObserver(String audioClipName) { URL url=getClass().getResource(audioClipName); clip=Applet.newAudioClip(url); } public void update(Observable subject, Object arg) { AlertCondition alert=(AlertCondition)subject; if (alert.getCondition()==AlertCondition.RED) clip.loop(); else clip.stop(); } }
import java.io.*; public class Main implements Runnable { public static void main(String args[]) throws IOException { new Main(); } private AlertCondition alert; public Main() throws IOException { alert=new AlertCondition(); alert.addObserver(new LogAlertObserver("alert.log")); alert.addObserver(new GraphicAlertObserver()); alert.addObserver(new SoundAlertObserver("alarm.wav")); Thread t=new Thread(this); t.start(); }
}
public void run() { final int DELAY=3000; while (true) { try { alert.setCondition(AlertCondition.GREEN); Thread.sleep(DELAY); alert.setCondition(AlertCondition.YELLOW); Thread.sleep(DELAY); alert.setCondition(AlertCondition.RED); Thread.sleep(DELAY); } catch (InterruptedException exc) { } } }
State (Object) Problema Vogliamo che il comportamento di alcune operazioni dipenda dallo stato in cui l’oggetto Context si trova; inoltre si vuole evitare che la dipendenza dallo stato sia cablata nei metodi dell’oggetto perché ciò renderebbe complicato estendere l’insieme degli stati (es. blocchi ifelse per verificare lo stato in cui ci si trova).
Soluzione Lo stato del Context viene trasformato in un oggetto che implementa un’interfaccia State con le operazioni che devono essere eseguite in modo diverso per ogni stato; ogni stato diventa una sottoclasse concreta di State ed il Context mantiene un riferimento all’oggetto che rappresenta lo stato corrente, modificando tale riferimento quando cambia lo stato.
Struttura Context: è l’oggetto utilizzato dal client e mantiene un riferimento al ConcreteState corrente; State: interfaccia che descrive il comportamento associato ai diversi stati; ConcreteState: sottoclasse che implementa il comportamento associato ad uno stato.
Conseguenze
La dipendenza dallo stato è definita nelle implementazioni concrete di State, senza essere distribuita nei metodi del Context; È semplice modificare il comportamento di uno stato oppure aggiungere nuovi stati.
Esempio Un software di disegno varia il risultato di un click del mouse in base allo strumento correntemente selezionato.
Implementazione import java.awt.*; public abstract class Tool { public void mouseDown(Graphics g, int x, int y) { } public void dragTo(Graphics g, int x, int y) { } public void mouseUp(Graphics g, int x, int y) { } }
import java.awt.Graphics; public class LineTool extends Tool { private int prevX, prevY; public void mouseDown(Graphics g, int x, int y) { prevX = x; prevY = y; } public void mouseUp(Graphics g, int x, int y) { g.drawLine(prevX, prevY, x, y); } } import java.awt.Graphics; public class FreehandTool extends Tool { private int prevX, prevY; public void mouseDown(Graphics g, int x, int y) { prevX = x; prevY = y; } public void dragTo(Graphics g, int x, int y) { g.drawLine(prevX, prevY, x, y); prevX = x; prevY = y; }
}
public void mouseUp(Graphics g, int x, int y) { g.drawLine(prevX, prevY, x, y); }
// . . . public class Doodle extends Canvas { private Tool selectedTool; public Doodle() { // . . . addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent evt) { selectedTool.mouseDown(getGraphics(), evt.getX(), evt.getY()); } // . . . }); // . . . } // . . . public void setTool(Tool t) { selectedTool=t; } // . . . }
Strategy (Object) Problema Vogliamo rendere il contesto in cui viene utilizzato un algoritmo indipendente da una particolare implementazione dell’algoritmo (es. in un algoritmo di ordinamento abbiamo bisogno di confrontare 2 oggetti e vogliamo che l’algoritmo sia indipendente dal modo in cui viene fatta la comparazione).
Soluzione Si definisce un’interfaccia Strategy che contiene le operazioni di cui si vuole nascondere l’implementazione e quindi per ogni implementazione si definisce una sottoclasse concreta di Strategy; l’oggetto Context riceve un riferimento alla sottoclasse concreta di Strategy utilizzata.
Struttura Context: rappresenta il contesto ed ha il compito di invocare l’algoritmo; Strategy: interfaccia dell’algoritmo che viene invocata dal Context; ConcreteStrategy: contiene l’implementazione concreta dell’algoritmo.
Conseguenze
È possibile realizzare famiglie di algoritmi simili che differiscono solo per l’implementazione di alcune operazioni; Si può cambiare l’implementazione scelta anche a run-time, cambiando l’oggetto ConcreteStrategy; Consente di eliminare i blocchi condizionali per la scelta del comportamento desiderato; Template Method vs Strategy: il primo non richiede la creazione di più oggetti, il secondo permette di evitare un aumento indiscriminato di sottoclassi se il contesto ha più operazioni da incapsulare indipendentemente, inoltre l’implementazione della Strategy può essere effettuata a run-time ed infine una Strategy può essere riutilizzata in contesti diversi; State vs Strategy: sono identici dal punto di vista implementativo, ma il primo serve per semplificare variazioni di comportamento legate allo stato, mentre il secondo serve a parametrizzare un algoritmo; inoltre lo stato può cambiare durante l’esecuzione mentre la Strategy solitamente non cambia una volta scelta; infine il context conosce tutti i possibili State, mentre invece non conosce l’insieme delle possibili Strategy che potrà ricevere.
Esempio Per gli algoritmi di ordinamento si può utilizzare un Comparator che rappresenta la Strategy per la comparazione.
Implementazione package java.util; public interface Comparator { // Confronta due oggetti // restituisce un valore 0 // a seconda che sia xy int compare(Object x, Object y);
}
// verifica se un altro oggetto è uguale // a questo Comparator boolean equals(Object other);
// UTILIZZO: public class Collections { // Ordina una lista public static List sort(List list, Comparator order) { // . . . public class Accumulator { private AccumulationStrategy strategy; public Accumulator(AccumulationStrategy strategy) { this.strategy=strategy; } public int compute(int a[], int n) { int value=strategy.initialValue(); int i; for(i=0; i0) return currentValue+1; else return currentValue; }
// UTILIZZO: AccumulationStrategy strategy=new SumStrategy(); Accumulator acc=new Accumulator(strategy); int sum=acc.compute(a,n);
Visitor (Object) Problema Abbiamo una gerarchia di classi stabile, ma l’insieme delle operazioni che vogliamo effettuare è variabile (in pratica non è necessario aggiungere nuove sottoclassi, ma capita spesso di aggiungere alla classe base nuove operazioni che devono essere implementate in modo diverso per ogni sottoclasse). La programmazione orientata agli oggetti è efficace nel caso opposto, ossia operazioni stabili e insieme di classi variabili.
Soluzione Si definisce un’interfaccia/classe astratta Visitor con un metodo per ogni classe concreta della gerarchia; per ogni operazione si definisce una sottoclasse concreta di Visitor i cui metodi implementano l’operazione da effettuare su ciascun tipo di elemento. La classe base della gerarchia avrà un metodo accept che riceve in ingresso un Visitor; le classi concrete della gerarchia implementano tale metodo richiamando il metodo del Visitor che corrisponde al tipo di elemento considerato, passandogli come riferimento lo stesso oggetto corrente this.
Struttura
Element: definisce il metodo accept che prende in input un Visitor; ConcreteElement: rappresenta un oggetto Element della gerarchia e implementa il metodo accept prendendo un Visitor come argomento; ObjectStructure: rappresenta una collezione di Element; Visitor: interfaccia che definisce un metodo visit per ogni Element, distinguibile per la firma ed il parametro passato;
ConcreteVisitor: implementa il metodo visit e definisce le operazioni da applicare all’Element passato come parametro.
Conseguenze
È semplice aggiungere nuove operazioni che agiscono sugli Element semplicemente definendo un nuovo Visitor; Si ottiene una separazione tra stato, rappresentato negli Element, ed algoritmi, contenuti nei Visitor; Il codice risulta più leggibile e non si viola il principio DRY (infatti il problema risolto dal pattern Visitor potrebbe essere risolto anche con una serie di if in cascata per controllare il tipo di oggetto); Il pattern Visitor aggira una limitazione del polimorfismo nella maggior parte dei linguaggi OO, ossia permette di scegliere il metodo da applicare in dipendenza da 2 oggetti (multiple dispatch), l’elemento e l’operazione. Il polimorfismo invece permette la scelta del metodo solo in base all’oggetto ricevente (single dispatch); Problema: è difficoltoso aggiungere nuovi tipi di elemento, in quanto occorrerebbe cambiare tutti i Visitor.
Implementazione public abstract class User { public String getIpAddress() { return "10.0.77.1"; } public abstract void accept(UserVisitor visitor); } public interface UserVisitor { void visitAnonymous(AnonymousUser user); void visitRegular(RegularUser user); void visitDeluxe(DeluxeUser user); } public class AnonymousUser extends User { public void accept(UserVisitor visitor) { visitor.visitAnonymous(this); } } public abstract class NamedUser extends User { private String name; public NamedUser(String name) { this.name = name; } public String getName() { return name; } }
public class RegularUser extends NamedUser { public static final int DAILY_CREDITS=100; private int credits; public RegularUser(String name) { super(name); credits=DAILY_CREDITS; } public void accept(UserVisitor visitor) { visitor.visitRegular(this); } public int getCredits() { return credits; } public void consumeCredits(int amount) { credits -= amount; } public void restoreCredits() { credits=DAILY_CREDITS; } } public class DeluxeUser extends NamedUser { private String creditCard; public DeluxeUser(String name, String creditCard) { super(name); this.creditCard=creditCard; } public void accept(UserVisitor visitor) { visitor.visitDeluxe(this); } public void pay(double amount) { System.out.println("User "+getName()+" has paid "+amount+ " euros with card n. "+creditCard); } } public class SendMessageVisitor implements UserVisitor { public static final int MESSAGE_CREDITS=3; private String receiver, body; public SendMessageVisitor(String receiver, String body) { this.receiver=receiver; this.body=body; } public void visitAnonymous(AnonymousUser user) { System.out.print(user.getIpAddress()); System.out.println(", non sei autorizzato a mandare messaggi!"); } public void visitRegular(RegularUser user) { if (user.getCredits()>=MESSAGE_CREDITS) { System.out.print("Messaggio inviato da "+user.getName()); System.out.println(" a "+receiver+": "+body); user.consumeCredits(MESSAGE_CREDITS); } else { System.out.print(user.getName()); System.out.println(", hai esaurito i crediti a tua disposizione."); } } public void visitDeluxe(DeluxeUser user) { System.out.println("Esimio commendator "+user.getName()+","); System.out.println(" il suo messaggio: "+body); System.out.println(+receiver); System.out.println("Le porgiamo umilmente i nostri saluti."); } }
// ...
// ...
UserVisitor send=new SendMessageVisitor("Pippo", "ciao!"); User a=new AnonymousUser(); User b=new RegularUser("Paperino"); User c=new DeluxeUser("Gastone", "PAP131313"); a.accept(send); b.accept(send); c.accept(send);
View more...
Comments