Definitie
Este sablonul care permite crearea unui inlocuitor pentru un obiect, inlocuitor care sa controleze accesul la obiectul respectiv. Acest sablon se mai numeste surogat.
Context
Atunci cand crearea si initializarea unui obiect reprezinta procese costisitoare, este necesar ca ele sa se execute doar cand obiectul este efectiv necesar. Acesta este un exemplu de situatie in care se doreste controlul accesului la un obiect.
Sa presupunem ca avem un editor de documente care pot contine, printre altele, si imagini grafice. De regula, aceste imagini sunt stocate in fisiere separate si necesita un efort mare pentru a fi create si atunci este preferabil ca ele sa fie incarcate numai cand se afla in portiunea vizibila a documentului, altfel nefiind necesare. Cu alte cuvinte, se impune ca un obiect costisitor sa fie creat doar cand este absolut necesar, adica la cerere.
Pe de alta parte, insa, pentru a nu ingreuna implementarea editorului, se pune problema de a utiliza "ceva" pe post de inlocuitor al imaginii in punctul din document in care ea ar trebui sa se afle. Un asemenea inlocuitor este un obiect, numit proxy sau surogat, al carui rol este de a tine locul imaginii propriu-zise pe durata cat ea nu apare in fereastra de document. In momentul in care imaginea trebuie sa apara pe ecran, obiectul proxy este cel care se va ocupa de incarcarea ei, adica va initia crearea obiectului ce va modela imaginea. In figura de mai jos este redata configuratia de obiecte corespunzatoare situatiei descrise:
class Image : public Graphic{
class ImageProxy : public Graphic {
ImageProxy::ImageProxy(const char* imageFile){
Motivatii
Sablonul Proxy se aplica in general in situatiile in care este necesara o referinta la un obiect mai complexa si mai flexibila decat un simplu pointer. Cateva dintre cazurile in care sablonul Proxy poate fi utilizat sunt:
necesitatea unui reprezentant local al unui obiect aflat in alt spatiu de adrese (pe alta masina) decat clientul sau. In acest caz obiectul proxy care joaca rolul reprezentantului local se mai numeste remote proxy sau ambasador;
crearea la comanda a obiectelor costisitoare (cum este cazul din exemplul prezentat in paragraful anterior). In acest caz obiectul proxy se mai numeste proxy virtual, el dand iluzia existentei unui obiect server inainte ca acesta sa fi fost creat;
daca se pune problema ca diversi clienti sa aibe drepturi de acces diferite la un anumit obiect server, se poate folosi cate un obiect proxy care sa reprezinte serverul din perspectiva fiecarei categorii de clienti. In acest caz, obiectul proxy se mai numeste proxy de protectie;
un obiect proxy poate juca rolul unui pointer inteligent, care poate efectua operatii suplimentare la accesarea obiectului referit. Asemenea operatii suplimentare tipice sunt:
- contorizarea referintelor la un anumit obiect, existente la un moment dat. Acest lucru permite stergerea obiectului respectiv cand se constata ca nu mai exista nici o referinta la el. De exemplu, un garbage collector ar putea utiliza acest principiu de lucru;
- incarcarea in memorie a unui obiect persistent atunci cand el este referit prima oara;
- asigurarea blocarii obiectului referit, pe durata accesarii lui, astfel incat sa nu poata fi modificat.
Solutie
In figura de mai jos este data structura de clase care constituie sablonul Proxy:
Consecinte
Sablonul Proxy introduce un nivel de indirectare la accesarea unui obiect.
Implementare
//exemplu de utilizare
Copierea la scriere. Mai jos sunt prezentate secventele de cod care ilustreaza conceptul de copy-on-write, considerand ca structura de date implicata este o tabela de perechi de forma <cheie, valoare> (tabela hashing). Se precizeaza ca nu se includ aspectele legate de sincronizarea accesului pentru cazul exploatarii concurente a tabelei.
class RefTable : public Table{ //rol de RealSubject
class LargeTableProxy : public Table { //rol de Proxy
//secventa dintr-o clasa client
class LargeTableProxy extends Hashtable { //rol de Proxy
In cazul in care un proxy nu este obligat sa cunoasca tipul concret al obiectului referit, atunci el poate lucra folosind interfata Subject si, prin urmare, nu trebuie definita cate o clasa Proxy separata pentru fiecare RealSubject in parte. Daca insa obiectul proxy este responsabil pentru instantierea obiectului RealSubject, cum e cazul proxy-ului virtual, atunci el trebuie sa cunoasca clasa concreta a obiectului referit.
Definitie
Context
Una din optimizarile pe care acest sablon le poate efectua fara ca obiectele client sa "simta" este asa-numita copiere la scriere (copy-on-write) care este legata in special de proxy-ul virtual. In principiu, copierea la scriere consta in urmatoarele: sa presupunem ca mai multe fire de executie trebuie sa lucreze cu cate o structura de date complexa (un arbore sau o tabela hashing de ex) pe care o actualizeaza in mod independent. La prima vedere problema se rezolva prin distribuirea catre fiecare fir a cate unei copii a structurii. Cum crearea unei copii a unei structuri complexe este costisitoare, ea nu se justifica daca operatiile asupra ei sunt doar consultari. Rezolvarea acestei probleme se poate face printr-un compromis: structura de date sa existe initial intr-un singur exemplar si ea sa fie accesata via obiecte proxy; atata timp cat accesele presupun doar consultare nu se modifica nimic; in momentul in care unul din fire doreste sa opereze o scriere in structura, pentru el se va crea o noua copie a structurii, in timp ce exemplarul initial va fi folosit in continuare de celelalte fire. In paragraful Implementare va fi dat codul corespunzator solutiei descrise, in variantele C++ si Java.
In cazul proxy-urilor de tip pointer inteligent se pot exploata anumite mecanisme oferite de diferitele limbaje de programare. De exemplu, in C++ putem aplica supraincarcarea operatorului -> de acces la membri. Vom ilustra acest lucru folosind exemplul cu imaginea grafica, unde in locul clasei ImageProxy vom defini o clasa ImagePtr ale carei obiecte vor avea rol de pointeri inteligenti pentru obiectele clasei Image.
class ImagePtr{
public:
};
ImagePtr(const char* imageFile);
private:
Image* operator->(){
return GetImage();
}
Image& operator*(){
return *GetImage();
}
Image* GetImage();
Image* image;
char* fileName;
ImagePtr::ImagePtr(const char* imageFile){
fileName=imageFile;
}
image=0;
Image* ImagePtr::GetImage(){
if(image==0) image = new Image(fileName);
}
return image;
ImagePtr ip("aFile");
ip -> Draw(); // (ip.operator->()) -> Draw();
class Table { //cu rol de Subject
private:
};
virtual void* copie();
public:
Table();
virtual void* clone();
virtual Value get(Key k);
virtual void put(Key k, Value v);
virtual int size();
// alte operatii
void* Table::copie(){
//operatia copie creaza o copie shallow a obiectului
}
//curent fara a apela constructorul;
//aceasta operatie va trebui redefinita in subclasele lui Table
char *p = new char[sizeof(Table)];
memcpy(p,this,sizeof(Table)];
return p;
void* Table::clone(){
//operatia clone este cea prin care clientii
}
//pot crea copii ale obiectelor
return copie();
private:
};
int proxyCount;
RefTable() : Table() { proxyCount=1;}
virtual void* copie();
void* clone();
int getProxyCount(){
return proxyCount;
}
void addProxy(){
proxyCount++;
}
void removeProxy(){
proxyCount--;
}
friend class LargeTableProxy;
//se observa ca obiectele clasei RefTable nu sunt accesibile
clientilor; aceasta clasa va fi utilizata doar de LargeTableProxy
void* RefTable::copie(){
char* p = new char[sizeof(RefTable)];
}
memcpy(p,this,sizeof(RefTable)];
return p;
void* RefTable::clone(){
RefTable* copy = (RefTable*)Table::clone();
}
copy -> proxyCount=1;
return copy;
private:
} //end class LargeTableProxy
TableRef* theTable;
public:
virtual void* copie();
void copyOnWrite(){
/* operatia creaza o copie a tabelei daca aceasta este referita de minimum 2 proxy; ea este apelata inainte de a se executa o scriere in tabela */
}
if(theTable -> getProxyCount()>1){
theTable -> removeProxy();
}
theTable = (TableRef*)(theTable -> clone());
LargeTableProxy(){
theTable = new TableRef;
}
virtual int size(){
return theTable -> size();
}
virtual Value get(Key k){
return theTable -> get(k);
}
virtual void put(Key k,Value v){
copyOnWrite();
}
theTable -> put(k,v);
virtual void* clone(){
/* operatia returneaza o copie a proxy-ului curent; ambele exemplare de proxy vor indica aceeasi tabela */
}
void* copy = Table::clone();
theTable -> addProxy();
return copy;
Table* t1 = new LargeTableProxy;
t1 -> put(k1,v1);
t1 -> put(k2,v2);
//. . .
Table* t2 = (LargeTableProxy*)t1 -> clone();
t1 -> get(k1);
t2 -> get(k2);
//pana aici s-a lucrat cu un singur exemplar al tabelei
t1 -> put(k3,v3); //in acest moment s-a creat o copie cu care
//t1 va lucra de acum incolo;
//t2 continua sa refere exemplarul initial
In acest caz putem beneficia de metoda predefinita clone() care ne scuteste de a prevedea o operatie copie() ca in varianta anterioara. Biblioteca limbajului ofera, de asemenea, clase predefinite pentru modelarea tabelelor hashing, clase pe care le-am putea folosi in locul clasei Table. In secventa de mai jos vom folosi clasa java.util.Hashtable pe post de Subject.
private TableRef theTable;
} //end class LargeTableProxy
public LargeTableProxy(){
the table = new TableRef();
}
public int size(){
return theTable.size();
}
public Object get(Object k){
return theTable.get(k);
}
public Object put(Object k,Object v){
copyOnWrite();
}
return theTable.put(k,v);
private void copyOnWrite(){
if(theTable.getProxyCount()>1){
}
theTable.removeProxy();
}
theTable=(TableRef)theTable.clone();
public Object clone(){
Object copy=super.clone();
}
theTable.addProxy();
return copy;
private class RefTable extends Hashtable{ //rol de RealSubject
private int proxyCount=1;
} //end class RefTable
public Object clone(){
RefTable copy = (RefTable)super.clone();
}
copy.proxyCount=1;
return copy;
public int getProxyCount(){
return proxyCount;
}
public void addProxy(){
proxyCount++;
}
public void removeProxy(){
proxyCount--;
}
Acest sablon realizeaza conversia interfetei unei clase intr-o alta interfata, asteptata de client.
Uneori sunt situatii in care anumite clase de biblioteca, destinate reutilizarii nu pot fi utilizate direct deoarece interfata lor nu se potriveste interfetei specifice domeniului unei aplicatii.
Sa presupunem ca avem un editor grafic care permite utilizatorilor sa asambleze elemente grafice de baza (linii, poligoane, text etc) pentru a obtine diverse diagrame. Abstractiunea esentiala folosita de editor este obiectul grafic caruia i se poate edita conturul si care se poate desena pe sine. Presupunem ca interfata pentru obiectele grafice se numeste Shape. Pentru fiecare tip de obiect grafic se definesc subtipuri ale lui Shape: LineShape, PolygonShape etc.
Clasele corespunzatoare formelor geometrice elementare (linii, poligoane) sunt relativ simplu de implementat, deoarece implica facilitati de editare si desenare limitate. In schimb, o clasa TextShape care sa permita afisarea si editarea de text in regim grafic este mult mai dificil de realizat, intrucat si operatiile cele mai simple de editare de text presupun actiuni complicate de actualizare a ecranului si de gestionare a buffer-elor. Pe de alta parte este posibil sa se dispuna de clase de biblioteca destinate editarii de text in regim grafic, care ar fi potrivite pentru aplicatie. Este foarte probabil insa ca asemenea clase sa fi fost proiectate fara a se tine cont de posibilele interfete Shape. Ca urmare, obiectele acestor clase nu vor putea fi utilizate acolo unde se asteapta obiecte de tip Shape.
Presupunem ca o clasa de genul amintit se numeste TextView. Ca sa o putem adapta la interfata Shape, fara a-i modifica interiorul (modificare posibila de altfel doar daca dispunem de codul sursa), vom utiliza o clasa TextShape care sa se interpuna intre Shape si TextView, realizand adaptarea acesteia din urma. Spunem in acest caz ca TextShape este un adaptor.
Adaptarea se poate realiza in 2 moduri:
Motivatii
Sablonul Adapter se aplica in situatiile
in care:
se doreste utilizarea unei clase deja existente, a carei interfata nu se potriveste cu necesitatile aplicatiei;
se doreste crearea unei clase reutilizabile care coopereaza cu clase ale caror interfete nu sunt compatibile intre ele;
se doreste utilizarea unor clase existente, dar nu este adecvata definirea unui descendent comun al lor. Acest caz este valabil pentru adaptorul la nivel de obiecte.
Solutie
In figura de mai jos este data structura de clase care constituie sablonul Adapter:
Consecinte
Sablonul Adapter presupune cateva compromisuri:
In cazul adaptorului la nivel de clasa:
adaptarea se realizeaza prin intermediul unei clase Adapter concrete; ea nu va putea realiza adaptarea in situatia in care se doreste acest lucru nu doar pentru clasa Adaptee, ci si pentru toate subclasele ei;In cazul adaptorului la nivel de obiecte:
in Adapter se pot redefini operatii din Adaptee, deoarece intre cele 2 exista relatia de subclasa-superclasa;
se creaza un singur obiect, instanta a clasei Adapter care va ingloba si obiectul Adaptee, nemaifiind nevoie de o referinta pentru a-l accesa pe acesta din urma.
este posibil ca un singur obiect Adapter sa lucreze cu mai multe obiecte care sunt instante ale clasei Adaptee insasi sau ale oricarei subclase ale ei;Implementare
redefinirea metodelor din Adaptee nu este prea simpla: ar presupune crearea unei subclase a lui Adaptee in care sa se realizeze redefinirea, iar obiectul Adapter sa refere aceasta subclasa si nu pe Adaptee.
In C++ adaptorul la nivel de clasa se recomanda a fi implementat definind clasa Adapter ca mostenind Target in mod public si Adaptee in mod private. In felul acesta este ca si cum Adapter este un subtip al lui Target, dar nu si al lui Adaptee.
Pentru ca o clasa sa fie reutilizabila este necesar sa se minimizeze constrangerile pe care clientii trebuie sa le suporte ca sa poata utiliza clasa respectiva. Daca o clasa este prevazuta in interior cu un mecanism de adaptare a interfetei, practic se elimina constrangerea ca celelalte clase sa "vada" o aceeasi interfata. O asemenea clasa este denumita cu termenul de "pluggable adapter" si ea poate fi incorporata in mai multe aplicatii care asteapta interfete diferite.
In cele ce urmeaza vom considera un exemplu in acest sens si vom arata 2 metode de implementare a adaptorilor de tip "pluggable":
Sa consideram ca avem o clasa TreeDisplay care permite afisarea grafica a structurilor de arbore. Daca aceasta clasa ar fi de unica folosinta, in sensul ca ar fi proiectata special pentru o anumita aplicatie, este foarte probabil ca s-ar impune ca obiectele pe care ea le-ar afisa sa se conformeze unei interfete anume, de exemplu Tree.
Daca insa, dorim ca TreeDisplay sa fie de uz mai general, atunci ea ar trebui sa functioneze pentru diverse obiecte care modeleaza arbori, definite in aplicatii diferite. De exemplu putem avea un obiect care modeleaza o structura de fisiere, caz in care metoda de acces la descendentii unui nod s-ar putea numi GetSubdirectories. Pe de alta parte, putem avea obiecte care modeleaza o ierarhie de clase si atunci metoda respectiva s-ar numi GetSubclasses. Clasa TreeDisplay ar trebui sa poata lucra cu ambele categorii de obiecte, chiar daca ele au interfete diferite. Altfel spus, clasa TreeDisplay ar trebuie sa fie dotata cu un mecanism de adaptare a interfetei.
Un prim pas, comun pentru cele 2 metode de implementare, ar fi acela de a gasi o interfata restransa pentru clasele de adaptat (Adaptee), care sa cuprinda setul minim de operatii necesar adaptarii. O asemenea interfata restransa este mai usor de adaptat. Pentru TreeDisplay clasele de adaptat reprezinta structuri ierarhice arborescente. O interfata minimala in aces caz ar putea include 2 operatii: una care sa permita reprezentarea grafica a unui nod din structura (CreateGraphicNode) si una care sa asigure accesul la fiii unui nod (GetChildren).
- Varianta 1 de implementare: prin utilizarea operatiilor abstracte
In clasa TreeDisplay se prevad cele 2 operatii din interfata restransa ca operatii abstracte. In aplicatia ce va utiliza clasa TreeDisplay va trebui creata o subclasa a acesteia, in care sa se implementeze cele 2 operatii si sa se adapteze clasa ce reprezinta structura ierarhica. De exemplu, in subclasa DirectoryTreeDisplay cele 2 operatii se vor implementa accesand un obiect reprezentand sistemul de fisiere:unde operatia BuildTree este:
BuildTree(node n){
GetChildren(n);
}
for each child {AddGraphicNode(CreateGraphicNode(child))
}
BuildTree(child)
- Varianta 2 de implementare: prin utilizarea delegarii
In aceasta varianta clasa TreeDisplay redirecteaza cererile de acces la structura ierarhica spre un obiect delegat. Se pot alege diferite strategii de adaptare schimband delegatul.
Pentru aceasta se defineste o clasa abstracta TreeAccessor care contine interfata restransa de care aminteam mai sus. Delegatul concret (DirectoryBrowser) trebuie sa mosteneasca TreeAccessor:unde operatia BuildTree este:
BuildTree(node n){
delegate -> GetChildren(this,n);
}
for each child {AddGraphicNode(delegate -> CreateGraphicNode(this,child))
}
BuildTree(child)