Courier e Binder Patterns
Il concetto di accoppiamento tra oggetti è una delle linee guida nella realizzazione di software: un basso accoppiamento è uno degli obiettivi a cui tendere nella ricerca del riuso dei diversi componenti realizzati. Dal mondo delle GUI (Graphic User Interface) emergono molte idee a riguardo ma, oltre al famoso paradigma/pattern MVC possiamo evidenziare elementi minori la cui importanza risulta evidente se si considerano UI particolarmente articolate: stiamo parlando del Courier e Binder Patterns.
Entrambi i design hanno come obiettivo quello di disaccoppiare gli oggetti collaboranti nel dato sistema, ma svolgono questo compito in modo leggermente differente: con questo presupposto possiamo intuire come questi pattern siano da considerarsi come variazioni sul tema dei mediatori(Mediator G.o.F. Pattern).
Il Courier Pattern viene presentato come oggetto che incapsula l'interazione tra due oggetti, promuovendo l'indipendenza dei differenti elementi coinvolti e la possibilità di alterare la logica di interazione stessa. Vengono introdotte due interfacce, PRODUCER e CONSUMER che verranno implementate dagli oggetti in gioco in base al ruolo rivestito; materiale di scambio tra i due elementi è l'oggetto MESSAGE.
public interface Producer
{
public Object produce();
}
public interface Consumer
{
public void consume(Object message);
}
L'oggetto Courier mantiene i riferimenti quanto al produttore che al consumatore e si occupa di passare un messaggio dal primo al secondo, dopo eventuali pre-processamenti, secondo la seguente forma:
public class Courier
{
...
public void execute()
{
Message m = producer.produce();
preProcess(m);
consumer.consume(m);
}
...
}
L'attività del Courier è generalmente stimolata da un terzo oggetto detto INVOKER che provocherà, in conseguenza di un'azione dell'utente della GUI, l'invocazione della logica di coordinamento del mediatore:
public interface Invoker
{
public void action();
}
Nel mondo Swing questo design trova un immediata collocazione grazie alla presenza di meccanismi di listening che offrono un momento di ingresso alla realizzazione del pattern. Esempio immediato lo si può evidenziare nell'uso di oggetti di tipo Button. Un oggetto Button può essere costruito per rispondere alla sua pressione in base ad una data Action: una realizzazione dell'astrazione Action sarà l'oggetto Courier che vedrà come Invoker il Button associato all'azione stessa.
Supponiamo ora di avere una GUI in cui la scelta di un file, dopo un azione di browsing, permette la compilazione di un campo testuale che riporta il nome del file selezionato; l'azione di ricerca del file su un dato
file chooser viene scatenata da un bottone dedicato.
La realizzazione di questa GUI è possibile utilizzando il Courier Pattern secondo quanto segue:
public class Field extends JTextField implements Consumer
{
public void consume(Object message)
{
File msg = (File)message;
setText(msg.getAbsolutePath());
}
}
public class Chooser extends JFileChooser implements Producer
{
public Object produce()
{
JFrame owner = new JFrame();
owner.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setDialogTitle("Choose a File");
setMultiSelectionEnabled(false);
showOpenDialog(owner);
owner = null;
return getSelectedFile();
}
}
public class Courier extends AbstractAction
{
public Courier(Producer p ,Consumer c)
{
...
}
public void actionPerformed(ActionEvent actionEvent)
{
execute();
}
protected void execute()
{
Producer prod = ...
Consumer cons = ...
Object message = null;
if (prod != null) {
message = prod.produce();
}
if (cons != null) {
cons.consume(message);
}
}
}
Il Binder Pattern è da considerarsi una visione idiomatica del Courier secondo il linguaggio Java. Il Binder è intermediario tra due oggetti, ma concepisce gli elementi in gioco come dei JavaBean: sfruttando questa specifica attua un automatico passaggio dei valori delle proprietà di interesse tra le due componenti. L'interfaccia del Binder è nella forma:
public class Binder
{
...
public void setFirst(Object bean)
{
...
}
public void setSecond(Object bean)
{
...
}
public void setDirection(int direction)
{
...
}
public void bind(String[] firstProps, String[] secondProps,
Converter[] converters)
{
...
}
public void execute()
{
...
}
...
}
Il Binder oltre a mantenere i riferimento agli oggetti da intermediare, è parametrico rispetto alle proprietà dei due elementi gestiti ed introduce una nuova interfaccia: il CONVERTER. Questo elemento si occuperà di rendere compatibile il messaggio nel momento dell'invocazione tra gli oggetti; si presenta della forma:
public interface Converter
{
public Object fromFirstToSecond(Object data);
public Object fromSecondToFirst(Object data);
}
L'uso del Binder, riprendendo il precedente esempio, sarà secondo la seguente sintassi:
binder.setFirst(chooser);
binder.setSecond(field);
binder.setDirection(Binder.FIRST_TO_SECOND);
binder.bind(
new String[] {"selectedFile"},
new String[] {"text"},
new Converter[]{converter});
Immediatamente dopo la scelta del file si avrà l'invocazione:
Il Binder introduce un visione più dichiarativa e l'automatismo sulle invocazioni, che si presenta
direzionabile. Svantaggi sono la perdita della semplicità e dell'immediatezza d'uso. Un altro vincolo è il riferirsi esplicitamente a delle proprietà e la mancanza di una logica di coordinamento. Questo problema è superabile concependo nel Binder un metodo che prenda in ingresso un BindTask secondo la forma:
Binder
{
...
bind(BindTask task)
{
...
}
}
public interface BindTask
{
public void bindFromFirstToSecond();
public void bindFromSecondToFirst ();
}
Un ulteriore variazione sullo stesso argomento è data dall'idea di SwitchBoard elemento condiviso tra Widget e nel quale sono registrati sotto forma di Command le callback relative agli eventi generati dai differenti componenti grafici. Al verificarsi di un data evento, il Widget stimola la SwitchBoard su uno specifico metodo; a sua volta, la board, esegue lo specifico Command registrato.
ISP e Object Adaptation
L'
Interface Segregation Principle ci invita a creare delle interface che non forzino il Client di una data astrazione a dipendere da metodi che non utilizza: in ciò l'idea di avere interfacce coese e quindi evitare elementi FAT, carichi di responsabilità differenti. Il basso accoppiamento che viene stimolato trova la concezione più generica nelle
Weak Interface(Interfaccia Debole): questi elementi sono caratterizzati da un protocollo semplicissimo, generalmente espresso da uno, massimo tre metodi; a volte la genericità viene rafforzata dall'uso di altre interfacce o classi astratte come argomenti e tipi di ritorno dei metodi d'interfaccia. Esponente illustre di questo design è il Command Pattern, che nella forma più semplice prevede un unico metodo: execute().
Partendo da queste idee e nell'osservazione che molte delle problematiche di collaborazione tra oggetti si esplicano in attività di conversione ed adattamento, nel mondo del linguaggio Python è stata introdotta una specifica di
Object Adaptation basata su un semplice protocollo espresso nella funzione adapt(...). L'idea di fondo è relativamente semplice: un oggetto può supportare lo stesso tipo di protocollo richiesto o un protocollo compatibile, necessitare di adattamento o non supportare il protocollo. In Java, questa stessa idea vede come importante esempio la piattaforma Eclipse, grazie ad un elemento del suo runtime, l'interfaccia IAdaptable, espressa nella forma:
public interface IAdaptable
{
public Object getAdapter(Class target);
}
In entrambi i linguaggi il momento di adattamento passa da una «semplice» operazione di
casting alla creazione di un vero e proprio
Adapter, istanza di un prototipo depositato in un registro che opera da
Factory per tutti gli adattatori.
Vediamo un semplice esempio. Supponiamo di avere l'interfaccia Adaptable e gli elementi Table, Shape nonché i protocolli espressi dalle interfacce Writer e Reader. Vogliamo adattare quanto l'astrazione Shape che l'astrazione Table ai protocolli del Reader e del Writer.
public interface Adaptable
{
public Object adapt(Class targetType);
}
public interface Reader {
public Object read();
}
public interface Shape
{
public float area();
public void draw();
}
public interface Writer
{
public void write(Object o);
}
public class Table implements Adaptable
{
....
public Object adapt(Class targetType)
{
return AdapterRegistry.getInstance().getAdapter(this, targetType);
}
...
}
Il protocollo di adattamento viene a essere incapsulato nell'AdapterRegistry che fornirà l'opportuna Adapter in base alla seguente logica:
- l'oggetto da adattare è NULL, viene restituito NULL
- l'oggetto da adattare è compatibile con il protocollo del tipo target, viene restituito l'oggetto stesso
- l'oggetto appartiene ad una classe per cui è registrato un adapter, viene restituito l'adapter
- si cercano adapter registrati usando la super classe e le interfacce dell'oggetto
- viene restituito NULL
Un esempio d'uso può essere il seguente.
Allo startup del sistema vengono caricati nell'AdapterRegistry le diverse associazioni tra adapter e adattabili
AdapterRegistry.getInstance().registerAdapter(
Table.class, Reader.class, TableReaderAdapter.class);
AdapterRegistry.getInstance().registerAdapter(
Table.class, Writer.class, TableWriterAdapter.class);
AdapterRegistry.getInstance().registerAdapter(
Shape.class, Reader.class, ShapeReaderAdapter.class);
AdapterRegistry.getInstance().registerAdapter(
Shape.class, Writer.class, ShapeWriterAdapter.class);
A runtime vengono creati gli altri oggetti del sistema:
Table t = new Table("Employee");
Circle c = new Circle();
I collaboratori degli oggetti chiedono richiedono l'adattamento ad uno specifico protocollo:
Writer w = (Writer) t.adapt(Writer.class);
Writer w2 = (Writer) c.adapt(Writer.class);
Conclusioni
Le tematiche affrontate in questo articolo hanno fatto uso di materiale derivante dal mondo delle interfacce grafiche per evidenziare come a volte «guardando un po' più in là» è possibile trovare soluzioni di design interessanti, oltre le standardizzazione o le catalogazioni. Rimane sempre attivo l'obiettivo di stimolare la vostra curiosità e partecipazione e, perché no, la speranza di essere stato utile a qualcuno.