Introduzione
Quando si vuole un grado di indirezione, si è soliti utilizzare il meccanismo dei puntatori; in Java si sente spesso dire che non esistono i puntatori, ma la versione corretta di questa affermazione è che non esiste l'aritmetica sui puntatori. In realtà, dichiarando una variabile di un tipo non primitivo, si dichiara un riferimento a un oggetto della classe; infatti subito dopo si deve creare esplicitamente l'oggetto, pena la classica NullPointerException.
Quindi in Java si ha continuamente a che fare con puntatori, anche se in questo caso si preferisce parlare di riferimenti. Il fatto che Java costringa a utilizzare sempre riferimenti non deve essere considerato una limitazione: il polimorfismo si applica solo a puntatori e riferimenti, e questo vale anche in C++, quindi si tratta di una decisione molto Object Oriented.
Ci sono però situazioni in cui questo grado di indirezione non basta; inoltre non si vuole semplicemente aggiungere un'indirezione in più: si vuole anche che questa sia invisibile all'utilizzatore di un tale oggetto. Vediamo alcuni casi in cui questo può rendersi necessario, o comunque in cui una soluzione elegante risparmia molti problemi e permette di non appesantire l'implementazione o di accoppiare certe classi, dipendenze che non sono mai ben viste nella programmazione a oggetti.
Il problema, anzi i problemi
Spesso, in certi programmi, come ad esempio i text editor visuali, si possono includere nei documenti oggetti di dimensioni notevoli; tali oggetti sono inutili fino a quando non saranno davvero necessari, ad esempio quando dovranno essere visualizzati. Siamo nella stessa situazione anche nei sistemi operativi, quando si devono allocare grosse aree di memoria: non necessariamente al processo che le ha allocate servirà tutto lo spazio di tali aree.
In entrambi i casi è vantaggioso lasciare che l'occupazione di risorse avvenga solo quando è strettamente necessario, avere cioè un meccanismo di creazione on demand. Tuttavia, inserire tali controlli nel programma principale rischia di complicare il codice e di non renderlo riutilizzabile: si perde il disaccoppiamento di certe classi. Si vorrebbe invece utilizzare un surrogato dell'oggetto che si occupi di allocare l'oggetto solo quando è necessario.
Inoltre, spesso ci si trova davanti al problema dell'accesso diretto a un oggetto tramite i metodi pubblici della sua classe. Qualcuno potrebbe obiettare che, essendo pubblici, è nell'intenzione del programmatore rendere accessibili tali metodi al mondo esterno. Ci sono casi in cui, però, si vorrebbe limitare tale accesso solo a oggetti di certe classi, effettuare cioè un controllo a runtime basandosi, ad esempio, su diritti di accesso. Un caso del genere si ha per una risorsa il cui accesso è basato su privilegi o deve avvenire in modo condiviso, ma sincronizzato.
Parlando poi di programmazione distribuita, si vuole spesso la possibilità di accedere a un oggetto remoto in modo trasparente rispetto alla sua locazione fisica, come se fosse effettivamente sul computer locale: avere cioè l'indipendenza dalla località. In questo caso si sente spesso parlare, come nel caso di RMI e CORBA, di stub, letteralmente surrogato. Lo stesso termine si ritrova anche parlando di librerie a caricamento dinamico, come le DLL, che rappresentano un caso di caricamento di codice on demand.
Molto più semplicemente, in altri casi si può avere la necessità di compiere operazioni aggiuntive prima e dopo l'accesso a certi oggetti, ovviamente senza che l'utente se ne accorga; un caso classico è quello degli smart pointer, che, quando viene creato un oggetto basandosi su un altro oggetto, in realtà fanno puntare il nuovo puntatore all'oggetto originale, incrementandone il contatore dei riferimenti.
Il pattern Proxy
Il pattern Proxy risolve, elegantemente e semplicemente, tutti questi problemi garantendo la trasparenza agli utilizzatori.
Proxy vuol dire procuratore, mandatario, delegato. L'idea di questo pattern è proprio quella di utilizzare un oggetto al posto di quello effettivo che abbia la stessa interfaccia, riceva le richieste destinate all'oggetto reale e gliele comunichi, eventualmente effettuando operazioni prima e/o dopo l'accesso.
Ad esempio, se siamo nel caso del caricamento o allocazione on demand, il proxy non allocherà l'oggetto fino a quando non sarà strettamente necessario. Nel caso invece in cui si voglia un accesso controllato a un oggetto, ogni metodo controllerà se chi ha effettuato la richiesta abbia effettivamente tutti i diritti necessari per completare l'accesso.
Di tutto questo l'utilizzatore non sarà a conoscenza: tutt'al più si vedrà negato l'accesso a una certa risorsa, ma avrà sempre l'impressione di avere a che fare con un oggetto della classe desiderata. Questo deriva dal fatto che il proxy implementa la stessa interfaccia dell'oggetto originale.
Vediamo in dettaglio i partecipanti a questo pattern:
- Soggetto (Subject): definisce l'interfaccia comune sia per il proxy, sia per l'oggetto che dovrebbe essere realmente il destinatario delle varie richieste.
- Soggetto reale (Real Subject): è l'oggetto rappresentato e, se si vuole, nascosto dal proxy; implementa l'interfaccia definita da Subject.
- Proxy: implementa l'interfaccia definita da Subject, implementata anche dal Soggetto reale, e mette a disposizione un accesso controllato al Real Subject.
In linguaggi come il C++, tipicamente il Subject rappresenta una classe astratta dalla quale derivano sia il Real Subject sia il Proxy. In linguaggi come Java, invece, il Subject è un'interfaccia che gli altri due partecipanti implementano.
Esempi di utilizzo di questo pattern sono:
- il già citato meccanismo degli smart pointer, molto utile in linguaggi come il C++ se si vuole ottenere un consumo ridotto della memoria;
- il meccanismo del copy on write implementato nei sistemi operativi Unix, ad esempio Linux, tramite il quale una certa area di memoria viene effettivamente allocata solo quando il processo richiedente vi accede;
- i remote proxy, utilizzati ad esempio nella libreria degli Aglet di IBM, che mette a disposizione un framework per la programmazione di agenti mobili in Java.
