Per esempio, ci sono due versioni B e C di una class A:
abstract class A { public abstract String getVal(); } class B extends A { private String val; B(String val) { this.val = val; } public String getVal() { return "B: " + val; } } class C extends A { private String val; C(String val) { this.val = val; } public String getVal() { return "C: " + val; } }La fattoria permette di astrarre come si fa la scelta fra B e C:
class AFactory { public static final int MAX_LENGTH = 3; public AFactory() { } public static boolean test(String s) { return s.length() < MAX_LENGTH; } public static A get(String s) { if (test(s)) { return new B(s); } return new C(s); } }Adesso si creano gli oggetti di tipo A attraverso Factory
A a = AFactory.get("ab"); A b = AFactory.get("abc");È lo stesso codice, ma adesso a.getVal() restituisce "B: ab", invece b.getVal() restituisce "C: abc".
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 class AFactory.
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; } } 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; } }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); }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); } }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 A1 B b1 = f1.getB(1); // crea un oggetto di tipo B1 A a2 = f2.getA("ab"); // crea un oggetto di tipo A2 B b2 = f2.getB(2); // crea un oggetto di tipo B2
public final class System { ... }oppure
public final class Math { ... }Un modo più sofisticato è di mettere il costruttore della class privato e di avere un metodo statico che è chiamato per creare oggetti. Tale metodo restituisce null quando la creazione è impossibile. Per esempio, abbiamo
class ASingleton { private static boolean flag = false; private ASingleton() { } public static ASingleton get() { if (flag) { return null; } flag = true; return new ASingleton(); } public void finalize() { // questo è dal garbagge collector quando // l'oggetto non è piú usato flag = true; } }
Per esempio, per costruire un oggetto della class A bisogna dare un oggetto di tipo B:
class A { private B b; A(B b) { this.b = b; } }Per B ci sono due possibilità:
class B { private C c; B(C c) { this.c = c; } } class B1 extends B { private B b; B1(B b, C c) { super(c); this.b = b; } }dove la class C è definita come:
class C { C() { } }Un builder per la class A elabora l'oggetto A in funzione di un parametro.
public class ABuilder { public static final int NORMAL = 0; public static final int EXTRA = 1; public ABuilder() { } A get(int type) { switch (type) { case NORMAL: return new A(new B(new C())); case EXTRA: return new A(new B1(new B(new C()), new C())); default: return null; } } }
class Object { ... protected Object clone() throws CloneNotSupportedException { if (! (this instanceof Cloneable)) { throw new CloneNotSupportedException(); } .... } }Per essere duplicato un oggetto deve implementare l'interfaccia Cloneable (che è vuota!). Il comportamento di default è di copiare l'oggetto ma non i campi.
Per esempio, per permettere di copiare una class A fuori di A bisogna fare un overriding del metodo clone:
class A implements Cloneable { private int i; int getI() { return i; } void setI(int i) { this.i = i; } public Object clone() throws CloneNotSupportedException { return super.clone(); } }Adesso si può creare un prototipo:
class APrototype { A get(A a, int i) { try { A res = (A) a.clone(); res.setI(i); return res; } catch (CloneNotSupportedException e) { return null; } } }
Questa organizzazione può essere fatta usando l'ereditarietà (si parla allora di pattern di class) o usando oggetti che contengono altri oggetti (si parla allora di pattern di oggetti).
Il consiglio è di preferire sempre la seconda soluzione.
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() { ... } }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 { .... }Adesso esistono due possibilità:
class AClassAdapter extends B implements ACapable { public int getX() { return (getXPlusY() + getXMinusY()) / 2; } public int getY() { return (getXPlusY() - getXMinusY()) / 2; } }
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.
Per esempio, data l'implementazione
abstract class AImpl { abstract void run(String command); } abstract class AImpl1 extends AImpl { void run(String command) { } } abstract class AImpl2 extends AImpl { void run(String command) { } } class ABridge { static AImpl impl; static void setImpl(AImpl impl) { ABridge.impl = impl; } static AImpl getImpl() { return impl; } }si può costruire un semplice singleton che dà accesso a queste implementazioni:
class ABridge { static AImpl impl; static void setImpl(AImpl impl) { ABridge.impl = impl; } static AImpl getImpl() { return impl; } }Adesso il servizio che è descritto nella class A si serve del singleton per ottenere la sua implementazione e fare delle variazioni.
abstract class A { AImpl impl; A() { impl = ABridge.getImpl(); } abstract void doIt(); } class A1 extends A { void doIt() { impl.run("do"); } } class A2 extends A1 { void doIt() { System.out.println("Trying"); super.doIt(); System.out.println("Done"); } }
Una class astratta contiene tutto quello che si può fare con gli oggetti
abstract class AComposite { abstract String getName(); abstract AComposite[] getSons(); }Dopo si possono creare degli oggetti semplici:
class ALeaf extends AComposite { String getName() { return "Leaf"; } AComposite[] getSons() { return new AComposite[0]; } }o degli oggetti compositi:
class ANode extends AComposite { private AComposite[] as; String getName() { return "Node"; } AComposite[] getSons() { return as; } }
Per esempio, data una class che può fare un'azione:
abstract class A { abstract void doIt(); }si può creare una class che decora un oggetto di tipo A permettendo di avere una traccia:
class ADecorator extends A { private A a; void doIt() { System.out.println("Trying"); a.doIt(); System.out.println("Do it"); } }
Per esempio, abbiamo un sistema che contiene una class che permette di fare un'azione:
class B { void readIt() {} }ed un'altra class che permette di fare un'altra azione:
class C { void playIt() {} }Una facade permette di concentrare in una sola class l'insieme delle due azioni:
class AFacade { B b; C c; void readIt() { b.readIt(); } void playIt() { c.playIt(); } }
Per esempio, sia data una class A 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 b:
class A { private int x; private int y; private B b; public String toString() { return b + "(" + x + ", " + y + ")"; } }Si può creare un flyweight che contiene la parte che si può riutilizzare:
class AFlyWeight { private B b; public String toString(int x, int y) { return b + "(" + x + ", " + y + ")"; } }Invece la parte variabile resta nella class A
class A { private int x; private int y; private AFlyWeight aF; public String toString() { return aF.toString(x, y); } }
class A { A() {} void doIt() { } }si può creare un proxy che crea A solamente quando c'è bisogno di effettuare tale azione:
class AProxy { private A a; AProxy() { a = null; } void doIt() { if (a == null) { a = new A(); } a.doIt(); } }
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.
Adesso tutti gli oggetti che possono comparire nella catena hanno bisogno di implementare questa interfaccia. Per esempio, per una class A abbiamo:
public class A implements Chain { private Chain chain; public void setUp(Chain chain) { this.chain = chain; } public void process (Object o) { if (test(o)) { action(o); } else { if (chain != null) { chain.process(o); } } } public boolean test(Object o) { ... } public void action(Object o) { ... } ... }Per un'altra class B:
class B implements Chain { private Chain chain; public void setUp(Chain chain) { this.chain = chain; } public void process (Object o) { if (test(o)) { action(o); } else { if (chain != null) { chain.process(o); } } } public boolean test(Object o) { .... } public void action(Object o) { ... } ... }Tutte le class che implementano l'interfaccia hanno lo stesso codice per process. Questa è una situazione in cui il fatto di non avere la multi-ereditarietà obbliga a duplicare codice.
In Java, un command prende la forma di un'intefaccia:
interface Command { void execute(); }Adesso data la class A
class A { ... public void doIt() { ... } ... }per trasformare la chiamata del metodo doIt in un command, si fa:
class ACommand implements Command { A a; ACommand(A a) { this.a = a; } public void execute() { a.doIt(); } }Adesso nella class B si può permettere di eseguire il metodo doIt senza conoscere A:
class B { Command c; void setCommand(Command c) { this.c = c; } void run() { c.execute(); } }
In Java è rappresentato dall'interfaccia Enumeration.
public interface Enumeration { boolean hasMoreElements(); Object nextElement(); }Per esempio, un metodo che itera su un parametro di tipo Enumeration che contiene degli elementi di tipo A si scrive come
void process(Enumeration enum) { while (enum.hasMoreElements()) { A a = (A) enum.nextElement(); ... } }Ogni struttura dati in Java (Vector, Hashtable, ...) ha un metodo elements() che permette di ottenere un'enumerazione. Si può anche creare una propria enumerazione. Per esempio, si può creare una class che trasforma un array in una enumerazione:
import java.util.Enumeration; import java.util.NoSuchElementException; class ArrayEnumeration implements Enumeration { private int index; private Object[] array; ArrayEnumeration(Object[] array) { this.array = array; index = 0; } public Object nextElement() { if (array.length <= index) { throw new NoSuchElementException(); } return array[index++]; } public boolean hasMoreElements() { return (index < array.length); } }Si può anche modificare gli iteratori. Per esempio, data l'interfaccia che permette di selezionare gli oggetti validi
interface Filter { boolean valid(Object o); }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 enum, Filter filter) { this.enum = enum; this.filter = filter; element = null; flag = false; } public Object nextElement() { if (flag && filter.valid(element)) { flag = false; return element; } Object res; while (true) { 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; } } } }
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) { .... } }uno di tipo B:
class B { private A a; void reset() { .... } void print(String s) { a.print(s); } }e uno di tipo C:
class C { private A a; void doIt() { .... } void print(String s) { a.print(s); }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); } }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); } }
Per esempio, dato un oggetto di tipo A dove il suo stato è rappresentato da un intero e da una stringa:
class A { private int value; private String name; int getValue() { return value; } void setValue(int value) { this.value = value; } String getName() { return name; } void setName(String name) { this.name = name; } .... }si può conservare il suo stato usando un oggetto di tipo AMemento
class AMemento { private int value; private String name; private A a; AMemento(A a) { this.a = a; value = a.getValue(); name = a.getName(); } void reset() { a.setValue(value); a.setName(name); } }Quando occorre, si può restituire lo stato dell'oggetto a memorizzato chiamando il metodo reset.
Per esempio, considerando un oggetto A che ha una valore x:
class A { private int x; int getX() { return x; } void setX(int x) { this.x = x; } ... }Per permettere di osservare le modifiche sul valore x, si crea prima un'xinterfaccia che rappresenta gli osservatori:
interface AObserver { void update(); }Dopo si può modificare la class A per tenere conto dei suoi osservatori:
class A { private int x; private AObserver[] observers; int getX() { return x; } void setX(int x) { this.x = x; for (int i = 0; i < observers.length; i++) { observers[i].update(); } } void addAObserver(AObserver o) { AObserver[] newObservers = new AObserver[observers.length + 1]; System.arraycopy(observers, 0, newObservers, 0, observers.length); newObservers[observers.length] = o; observers = newObservers; } ... }Un osservatore ha bisogno di registrarsi con il metodo addAObserver e dopo viene avvertito con il metodo update quando il valore è stato cambiato.
Per esempio, dato il codice seguente:
class A { private int state; public static final int A1 = 1; public static final int A2 = 2; void execute() { switch (state) { case A1: { ... state = A2; return; } case A2: { ... state = A1; return; } } } }dove gli stati A1 e A2 si alternano, applicando il pattern di state, si crea prima la class degli stati
abstract class AState { abstract void execute(A a); }L'argomento a nel metodo execute permette di cambiare lo stato. Adesso il codice di A diventa:
class A { private AState state; void setState(AState state) { this.state = state; } void execute() { state.execute(this); } }La class A delega l'esecuzione al suo stato. Per completare l'implementazione e ottenere il precedente alternarsi tra A1 e A2, abbiamo:
class A1 extends AState { void execute(A a) { ... a.setState(new A2()); } }
class A2 extends AState { void execute(A a) { ... a.setState(new A1()); } }
Per esempio, per implementare una class A che stampa un oggetto in due modi diversi (postscript o svg) si può scrivere come segue:
abstract class AStrategy { abstract void print(Object o); }
class APs extends AStrategy { void print(Object o) { ... } }
class ASvg extends AStrategy { void print(Object o) { ... } }
class A { private AStrategy strategy; void setStrategy(AStrategy strategy) { this.strategy = strategy; } void print(Object o, String s) { if ("ps".equals(s)) { setStrategy(new APs()); } else if ("svg".equals(s)) { setStrategy(new ASvg()); } print(o); } void print(Object o) { strategy.print(o); } }
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(); abstract void goRight(); final void doSquare() { goForth(); goRight(); goForth(); goRight(); goForth(); goRight(); goForth(); } }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.
Tale pattern è molto utile per esempio nel caso di un oggetto composito. Per esempio, consideriamo gli alberi binari. Abbiamo una class astratta
abstract class Tree { }e due sottoclass
class Node extends Tree { private String name; private Tree left; private Tree right; Node(String name, Tree left, Tree right) { this.right = right; this.left = left; this.name = name; } public String getName() { return name; } public Tree getLeft() { return left; } public Tree getRight() { return right; } }
class Leaf extends Tree { private int value; Leaf (int value) { this.value = value; } public int getValue() { return value; } }Adesso un visitatore per questa class sarà un oggetto che ha due metodi, uno per ogni sottoclass:
abstract class Visitor { abstract void visit(Node node); abstract void visit(Leaf leaf); }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); }
class Node extends Tree { private String name; private Tree left; private Tree right; Node(String name, Tree left, Tree right) { this.right = right; this.left = left; this.name = name; } public String getName() { return name; } public Tree getLeft() { return left; } public Tree getRight() { return right; } public void accept(Visitor v) { v.visit(this); } }
class Leaf extends Tree { private int value; Leaf (int value) { this.value = value; } public int getValue() { return value; } public void accept(Visitor v) { v.visit(this); } }Adesso per scrivere un visitatore si può estendere la class 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()); } }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()); } }Dato l'albero
Tree t = new Node("a", new Node("b", new Leaf(1), new Leaf(2)), new Leaf(3));
t.accept(new PrefixVisitor());dà
a b 1 2 3Invece
t.accept(new PostfixVisitor());dà
1 2 b 3 aUsando 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.