Curs 10 Curs 12

Proxy






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:
 


Surogatul imaginii creaza imaginea reala numai cand editorul solicita afisarea ei, prin invocarea unei operatii gen Draw(). Surogatul redirecteaza apelul la Draw spre obiectul care reprezinta imaginea propriu-zisa. Pentru aceasta este necesar ca surogatul sa detina o referinta la imagine. Admitand ca imaginea este stocata intr-un fisier separat, referinta ar putea fi chiar numele fisierului in cauza.
Obiectul proxy mai poate contine si alte informatii utile despre imaginea pe care o substituie. De exemplu, dimesiunile imaginii (lungimea si inaltimea) pot sa apara in proxy si sa fie furnizate la nevoie, chiar daca imaginea nu este incarcata. In diagrama de mai jos este ilustrata configuratia claselor care ar putea constitui o solutie a exemplului abordat:

Editorul acceseaza imaginile din document prin intermediul interfetei oferite de clasa Graphic. Clasa ImageProxy este folosita ca substitut pentru imaginile care se creaza la cerere. Ea include ca atribut numele fisierului in care se afla imaginea (fileName), nume care este pasat ca parametru al constructorului ImageProxy. ImageProxy include de asemenea date despre dimensiunile imaginii (extent) precum si o referinta la instanta imaginii propriu-zise (image). Operatia Draw() va verifica in prealabil daca imaginea a fost instantiata si abia apoi va apela operatia Draw din Image. In continuare este data o posibila implementare a diagramei de clase de mai sus:
Presupunand ca in clasa client DocumentEditor se creaza un obiect al unei clase TextDocument in care avem o operatie de forma
void Insert(Graphic*);
aceasta ar putea fi apelata ca in secventa urmatoare:

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:


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.
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.

Implementare

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);
          Image* operator->(){
            return GetImage();
          }
          Image& operator*(){
            return *GetImage();
          }
        private:
          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;
      }

      //exemplu de utilizare
      ImagePtr ip("aFile");
      ip -> Draw(); // (ip.operator->()) -> Draw();

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.

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.


Adaptorul (Adapter)






Definitie
Acest sablon realizeaza conversia interfetei unei clase intr-o alta interfata, asteptata de client.

Context
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:

Spre exemplu operatia BoundingBox din TextShape ar putea fi implementata ca:
return textViewRef -> getExtent();
Clasa adaptor realizeaza de multe ori si o extindere a facilitatilor oferite de clasa adaptata. Astfel, in exemplul nostru clasa TextShape ofera metoda CreateManipulator care returneaza o instanta a clasei TextManipulator (subclasa a clasei Manipulator). Cu ajutorul acestui obiect utilizatorul poate muta de colo colo pe ecran textul editat, facilitate pe care clasa TextView nu o contine.

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:

Adapter la nivel de clasa

Adapter la nivel de obiect
Clientii apeleaza operatii ale unei instante a clasei Adapter. Aceasta, la randul ei, apeleaza operatii ale clasei Adaptee, pentru a satisface cererile.
 

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 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.
In cazul adaptorului la nivel de obiecte:
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;
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.
Implementare
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)
        }
      }

Curs 10 Curs 12