MokaByte Numero 31  - Giugno 1999   

Proxy

di
Lorenzo Bettini

Delegare e controllare l'accesso ad un oggetto: un grado di indirezione in più.

 


Ci sono situazioni in cui l'indirezione offerta da un puntatore (o riferimento) non sono sufficienti: ci serve un grado di indirezione in più, che però sia invisibile agli occhi dell'utilizzatore. 


Introduzione

Quando si vuol 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 ad 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 (in questo caso si preferisce parlare di riferimenti). Il fatto che Java costringa ad utilizzare sempre riferimenti non deve essere considerata una limitazione: il polimorfismo si applica solo a puntatori e riferimenti (e questo vale anche in C++), e 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 ad oggetti).

Il problema (anzi i problemi)

Spesso, in certi programmi, come ad esempio i text editor visuali, si possono includere nei documenti certi 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 tale 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 ad 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 ad oggetti di certe classi, effettuare, cioè, un controllo a run-time, basandosi ad esempio su dei diritti di accesso. Un caso del genere si ha per una risorsa il cui accesso è basato su dei privilegi di accesso, o il cui accesso deve avvenire in modo condiviso, ma sincronizzato.

Parlando poi di programmazione distribuita, si vuole spesso la possibilità di accedere ad un oggetto remoto in modo trasparente dalla 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 dell'RMI e di 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 delle 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, sia elegantemente, che semplicemente, tutti questi problemi garantendo la trasparenza agli utilizzatori.

Proxy vuol dire: procuratore, mandatario, delegato. Infatti l'idea di questo pattern è proprio quella di utilizzare un oggetto al posto di quello effettivo che abbia la stessa interfaccia, e che riceva le richieste destinare all'oggetto reale e gliele comunichi, eventualmente effettuando delle 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 ad 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 ad una certa risorsa, ma avrà sempre l'impressione di avere a che fare un oggetto della classe desiderata. Questo ovviamente deriva dal fatto che il proxy implementa la stessa interfaccia dell'oggetto originale.

Vediamo comunque in dettaglio i partecipanti a questo pattern:

  • Soggetto (Subject): Definisce l'interfaccia comune sia per il proxy, che per l'oggetto che dovrebbe essere realmente il destinatario delle varie richieste.
  • Soggetto Reale (Real Subject): E' l'oggetto rappresentato, e, se si vuole, nascosto dal proxy, implementa l'interfaccia definita da Subject.
  • Proxy: Implementa l'interfaccia definita da Subject, tra l'altro 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 che 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 ci fa un accesso.
  • i remote proxy, utilizzati ad esempio nella libreria degli Aglet della IBM, che mette a disposizione un framework per la programmazione di agenti mobili in Java.

A presto :-)

Lorenzo