Abstract Factory - Kit

Pattern descritto in Design Pattern.

L'idea dell'Abstract Factory é che spesso si devono costruire famiglie diverse di oggetti che dipendono tutti da una scelta iniziale.

Ad esempio, se vogliamo che la nostra applicazione supporti diversi look-and-feel, potremmo volere che i vari oggetti della GUI vengano tutti creati da una factory specifica e non vogliamo che il nostro client si debba stare a preoccupare tanto di tutto ciò.

Così, per creare una finestra, si chiamerà il metodo createWindow() della nostra Abstract Factory. Noi avremo in precendenza istanziato la factory concreta che fa al caso nostro, e quindi la chiamata si risolverà implementando la finestra per la corretta modalità.

Partecipanti
  • AbstractFactory: dichiara l'interfaccia per la creazione degli oggetti.
  • ConcreteFactory: esiste una classe di questo tipo per ogni famiglia di oggetti che vogliamo creare.
  • AbstractProduct: ogni tipo di oggetti che vogliamo creare dovrà rispettare questa interfaccia.
  • ConcreteProduct: la classe che definisce le specifiche dell'oggetto da creare.
  • Client: per creare gli oggetti usa solo le interfacce AbstractFactory e Abstract Product.
Implementazione

Dato che tipicamente esiste sono una ConcreteFactory per esecuzione, risulta normalmente naturale implementarla usando il pattern Singleton.

La creazione dei prodotti avviene nella ConcreteFactory, tipicamente usando il pattern Factory Method.

Se ci sono molte famiglie si può fare ricorso al pattern Prototype per ridurre la complessità del sistema.

Per ridurre l'impatto del cambiamento delle factories, é possibile usare un metodo generico "make" a cui viene passato un parametro che specifica il tipo dell'oggetto che si vuole effettivamente creare. In questo caso si paga la maggior flessibilità con una minore sicurezza. Chiaramente é più semplice implementare questa soluzione in un linguaggio dinamicamente tipizzato, più il linguaggio tende alla staticità dei tipi di dati, più la cosa diventa difficile. In ogni caso spesso può essere necessario fare un downcast, che é inerentemente una operazione poco sicura.

Esempio in Java

Nel nostro caso abbiamo tre possibili implementazioni per un sistema di oggetti grafici, che consiste al momento di due soli elementi: Button e Window.
La nostra Abstract Factory sarà perciò qualcosa del genere:

public abstract class A1Factory {
public enum Type { ONE, TWO, THREE };
public static A1Factory getFactory(Type type) {
switch(type) {
case ONE:
return new A1FactoryOne();
case TWO:
return new A1FactoryTwo();
case THREE:
default:
return new A1FactoryThree();
}
}

public abstract Button createButton();
public abstract Window createWindow();
}

L'effettiva costruzione di un oggetto é delegata alla Factory concreta, vediamone qui una possibile implementazione:

public class A1FactoryOne extends A1Factory {

@Override
public Button createButton() {
return new ButtonOne();
}

@Override
public Window createWindow() {
return new WindowOne();
}
}

Ogni oggetto da creare avrà una sua gerarchia, che comprende una interfaccia:

public interface Button {
public void click();
}

E una implementazione specifica per il tipo di sistema utilizzato:

public class ButtonOne implements Button {

public void click() {
System.out.println("Click One");
}
}

Per testare il sistema possiamo scrivere un client come questo:

public class Main {
public static void main(String[] args) {
for(A1Factory.Type t: A1Factory.Type.values()) {
A1Factory f = A1Factory.getFactory(t);
System.out.println("Factory " + t);
f.createButton().click();
f.createWindow().drag();
}
}
}

Un secondo esempio in Java

Forziamo adesso un po' il pattern per usare la reflection di Java.

Nel nostro caso le Factory concrete ci servono solo per tenere l'informazione di quale tipo di gerarchia di classi dobbiamo instanziare. In realtà questa informazione la possiamo tenere nella Factory madre, che possiamo quindi far diventar concreta, rinunciando a tutte le Factory figlie.
Usiamo inoltre il metodo generico make(), indicato dalla GoF come possibile alternativa per ridurre l'impatto al cambiamento, utilizzando come parametro selettore l'interfaccia alla classe che vogliamo creare.
Come terza variazione, introduciamo una interfaccia, Widget, che sta alla base della gerarchia di oggetti che possono essere creati dalla nostra Factory. In questo modo possiamo rendere meno insicuro l'uso del metodo generico make(), in quanto possiamo restringere il suo campo d'uso a Widget e classi derivate. Ecco quindi la nostra nuova factory:

public class A2Factory {
public enum Type { One, Two, Three };
private Type type;

public A2Factory (Type type) {
this.type = type;
}

public Widget make(Class clazz) throws ClassNotFoundException {
try {
String name = clazz.getCanonicalName() + type;
System.out.println(name);
Widget w = (Widget) (Class.forName(name)).newInstance();
return w;
}
catch(Exception ex) {
throw new ClassNotFoundException(ex.getMessage());
}
}
}

Certamente un punto debole di questa implementazione é che viene usata una naming convention sulle classi generate. Gli oggetti della famiglia "One" devono avere tutti quella desinenza quindi, ad esempio, il suo bottone si chiamerà obbligatoriamente "ButtonOne".
Ma penso che sia una limitazione che generalmente si possa accettare.

L'interfaccia Widget potrebbe essere definita in questo modo:

public interface Widget {
public void paint();
}

E quindi l'interfaccia alla base della gerarchia di oggetti diventa qualcosa di simile a questa:

public interface Window extends Widget {
public void drag();
}

Mentre la classe che definisce un widget sarà circa così:

public class WindowOne implements Window {

public void paint() {
System.out.println("Window One");
}

public void drag() {
System.out.println("Drag One");
}
}

A seguire, un piccolo client per provare il sistema:

public class Main {
public static void main(String[] args) {
for(A2Factory.Type t: A2Factory.Type.values()) {
A2Factory f = new A2Factory(t);

System.out.println("Factory " + t);

Widget button = null;
Widget window = null;
try {
button = f.make(Button.class);
window = f.make(Window.class);
}
catch (ClassNotFoundException ex) {
System.out.println("ClassNotFoundException: " + ex.getMessage());
return;
}

// just basic widget behaviour required
window.paint();
button.paint();

// specialized behaviour required
if(window instanceof Window)
((Window)window).drag();
if(button instanceof Button)
((Button)button).click();

// trying making something wrong
try {
Object o = f.make(Object.class);
}
catch(ClassNotFoundException ex) {
System.out.println("As expected, a ClassNotFoundException: " + ex.getMessage());
}
}
}
}

Come si vede, il codice risultante é più complesso di quello del primo caso, dato che dobbiamo tener conto dei possibili utilizzi scorretti del sistema, e quindi cautelarci con una opportuna gestione delle eccezioni.

In cambio abbiamo una miglior adattabilità del sistema al cambiamento.

Nessun commento:

Posta un commento