Sunteți pe pagina 1din 59

8 SCURTĂ INTRODUCERE ÎN

STUDIUL ŞABLOANELOR DE
PROIECTARE UTILIZÂND
FORMALISMUL UML1

Tezaurizarea înţelepciunii este o îndeletnicire cu


implicaţii asupra vitezei cu care împingem cât mai
departe de înţelegerea noastră barierele
necunoscutului. Să nu uităm, însă, că şi
înţelepciunea, asemenea tuturor darurilor omului
are nevoie de schimbări pentru a ţine pasul cu
structura izvoarelor ei, mijlocite şi nemijlocite.

8.1 De ce sunt necesare şabloanele de


proiectare?

8.1.1 Introducere
Realizarea unui sistem software presupune parcurgerea mai multor faze,
fiecare cu dificultăţile şi riscurile proprii. A realiza un sistem software fiabil, cu
rezistenţă crescută la schimbările care, în mod inevitabil, apar şi cu un grad
înalt de performanţă şi scalabilitate2 nu este o sarcină uşoară.
Concepte precum încapsulare, flexibilitate, modularitate,
performanţă, adaptabilitate, granularitate, etc., trebuie luate în calcul iar
soluţia aleasă pentru sistemul software trebuie să ofere un grad cât mai mare de
eficienţă pentru fiecare dintre aceste concepte. Însă, ironia sorţii sau nu, în 99%
dintre cazuri, cresterea eficienţei pentru unele dintre aceste trăsături ale
sistemelor soft conduce la scăderea eficienţei pentru altele trăsături. Se
impune, astfel, un compromis pe care designer-ul soluţiei este nevoit să îl facă,
pentru a aduce, în final, soluţia la un grad cât mai mare de optimalitate.

1
Capitol scris, cu mult talent şi convingătoare maturitate, de foştii mei studenţi Anca
Holostencu şi Bogdan Mocanu, supus de autor unui proces de înveşmântare sintactică
şi semantică, specific acestui gen de lucrare
2
Capacitatea unui sistem de a se adapta, cu cheltuieli minime, la cerinţe noi (mai mulţi
utilizatori, mai multe baze de date, mai multe procesoare pe o statţie de lucru, etc.)

111111111111111
În plus faţă de conceptele de mai sus, pe care, după cum menţionam mai
sus, soluţia finală trebuie să le implementeze cu o eficienţă cât mai mare, mai
trebuie luat în considerare încă un concept, extrem de important, mai ales în
ultimii ani. Este vorba de conceptul de reutilizabilitate, care a căpătat în
ultimii ani o importanţă din ce în ce mai mare, şi tot mai multe sisteme încearcă
să ofere o arhitectură compusă din module ce sunt uşor reutilizabile, conducând
astfel la o eficienţă mult mai mare în scrierea codului, şi, implicit, la o scădere a
costurilor aferente dezvoltării sistemelor software.

Punctul extrem către care se tinde este ca dezvoltarea noilor sisteme să


se realizeze, exclusiv, din asamblarea unor componente existente, a unor
blocuri constructive care simplifică obţinerea soluţiei finale.

În acest mod, nu mai este necesar efortul de a crea noi componente care
să comunice optim cu cele existente. Ar exista un set complex de componente,
care ar rezolva o gamă întreagă de probleme, iar soluţia unui nou sistem
software nu ar face decât să specifice componentele şi modul de asamblare a
acestora, pentru a rezolva problema iniţială.
Evident, scenariul prezentat mai sus este, încă, utopic. În prezent sunt
rare situaţiile în care o componentă existentă efectuează exact operaţiile
necesare pentru un nou sistem software şi, mai mult, pentru fiecare dintre
operaţiile pe care noul sistem software doreşte să le realizeze există câte o
componentă. De aceea, designerii de sisteme software se văd nevoiţi, de fiecare
dată, să creeze noi componente, pentru a satisface cerinţele utilizatorilor faţă de
noile sisteme. În sprijinul afirmaţiei de mai sus vine şi modul în care se face
reutilizarea de componente, în mediile vizuale de programare, de exemplu.

8.1.2 Două categorii de designeri software


După cum oricine poate realiza, designerii software pot fi uşor împărţiţi
în două categorii : designeri experimentaţi şi designeri începători. Deşi, după
cum spuneam, este extrem de dificil să se realizeze o soluţie software care să
îmbine într-un mod eficient toate cerinţele utilizatorilor, şi să fie, în acelaşi
timp, compusă din elemente reutilizabile, totuşi astfel de elemente există sub
forma unor arhitecturi sau componente şablon, designerii sau arhitecţii
experimentaţi reuşind să ofere o soluţie care să îndeplinească toate cererile
legate de flexibilitate, reutilizabilitate, etc.
Şi atunci, în mod natural, se pune problema: care este acel element pe
care designerii experimentaţi îl deţin sau îl ştiu, iar începătorii nu ? Răspunsul
este simplu: experienţa.

222222222222222
Un lucru clar pe care designerii experimentaţi ştiu că NU trebuie să îl
facă, este să înceapă fiecare design de la zero, “from scratch”. În schimb, ei
folosesc soluţii care s-au dovedit eficiente în trecut.
În momentul în care, în procesul de design al soluţiei pentru un sistem
software, o soluţie nouă se dovedeşte eficientă şi cu un grad mare de
reutilizabilitate, designerii software tind să o reţină şi să o aplice, iar şi iar, de
fiecare dată când aceasta se potriveşte în designul unui nou sistem software.
Deoarece aceste soluţii sunt, mai degrabă, moduri în care se poate
realiza soluţia unei anumite probleme, acestea pot fi asemănate cu şabloanele
(patterns) folosite în proiectare.
Analogiile care pot fi realizate sunt multiple. Dacă analizăm modul în
care un matematician rezolvă o problemă, vom observa apariţia pattern-urilor.
Soluţii care la alte probleme au mers, sunt aplicate şi la rezolvarea altor
probleme, astfel încât nimic nu se începe de la zero. De asemenea, un scriitor
de piese de teatru sau de nuvele îşi bazează scrierile pe şabloane testate în
trecut şi care şi-au dovedit eficienţa şi atracţia la public. Opere precum Hamlet
sau Macbeth au la bază şablonul eroului ce moare într-un mod tragic, şi
exemplele pot continua la nesfârşit.
Se observă astfel că peste tot în jurul nostru sunt folosite diferite
şabloane. Christopher Alexander3 (unul dintre fondatorii pattern-urilor) spunea :
“fiecare pattern descrie o problemă ce apare iar şi iar în jurul nostru, după care
descrie nucleul ei, principalele ei idei, astfel încât soluţia la problemă să poată
fi folositaă de o mie de ori, fără a o folosi de două ori în acelaşi fel”. Se observă
astfel că un pattern descrie o soluţie la o problemă generală, văzută într-un
context particular.

8.1.3 Abstractizarea şi problemele ei


Realizarea design-ului pentru o soluţie software nu este posibilă fără a
apela la abstractizare în procesul de stabilire a arhitecturii. Însă, nivelul de
abstractizare la care se poate ajunge depinde exclusiv de cât de experimentat
este designer-ul sistemului software.
Principala problemă ce apare în cadrul dezvoltării soluţiei pentru un
sistem software este identificarea elementelor (clase şi obiecte) ce compun
soluţia finală a sistemului. Opţiunile pe care un designer le are la dispoziţie
includ, printre altele :

3
Profesor de arhitectură la Universitatea Berkley din California, considerat părintele
mişcării Design Pattern din informatică.. A scris cartea A pattern language, în care a
lansat ideile fundamantale ale mişcării Design Pattern.

333333333333333
-stabilirea clară a cerinţelor utilizatorilor faţă de sistemul software
şi identificarea substantivelor şi verbelor care modelează aceste
cerinţe. Aceste elemente se transformă, mai departe, în clase, obiecte şi
operaţii.

-stabilirea modului în care diferite componente ale sistemului a


cărui soluţie urmează sa o implementăm colaborează între ele, şi,
de asemenea, stabilirea responsabilităţilor acestora.

-modelarea sistemului real, aşa cum este el perceput în viaţa de zi cu


zi.

Indiferent de soluţia aleasă, o serie de obiecte nu vor fi atât de evidente,


nefiind incluse în specificarea soluţiei. De asemenea, indiferent de soluţia
aleasă pentru specificarea părţilor componente ale arhitecturii sistemului
software, clasele incluse în soluţie vor adresa, cel mai probabil, cerinţele
actuale, însă vor face puţine referiri la cerinţele de mâine.
Prin folosirea pattern4-urilor în general, şi a design pattern5-urilor în
particular, aceste scăpări sunt eliminate din procesul de specificare a soluţiei
software. Şabloanele de proiectare specifică setul de elemente care trebuie creat
şi utilizat şi, astfel, toate obiectele necesare sunt evidenţiate, indiferent cât de
ascunse sunt, sau cât de mare este gradul de abstractizare a soluţiei.
Deoarece soluţiile propuse de design patterns au o fundaţie solidă, fiind
verificate şi testate exhaustiv, folosirea lor în design-ul unei arhitecturi software
nu face decât să aducă un plus de robusteţe şi reutilizabilitate soluţiei. Deoarece
aceste componente au fost create pentru a face faţă schimbărilor şi pentru a fi
uşor reutilizabile, dezvoltarea soluţiei software utilizând design patterns rezolvă
rapid o serie de probleme care, altfel, ar necesita mult timp şi pentru care doar
în proporţie de aproximativ 30% s-ar identifica soluţiile optime.

8.1.4 Un exemplu practic comentat


Avantajele pe care design pattern-urile le aduc dezvoltării soluţiei unui
sistem software sunt cel mai bine vizibile în momentul în care se utilizează pe
un exemplu practic. Pentru a evidenţia modul în care design pattern-urile oferă
rapid soluţii eficiente şi accelerează procesul de design al arhitecturii sistemului
software, vom considera un exemplu de aplicaţie pentru managementul
fişierelor şi directoarelor de pe disc.
4
Cuvânt englezesc având înţelesul de şablon în prezenta lucrare.
5
Versiunea în limba engleză a ceea ce în limba română vom numi şabloane de
proiectare.

444444444444444
Presupunem că dorim realizarea unei aplicaţii asemănătoare cu
Windows Explorer sau Total Commander. Evident, primul pas care trebuie
realizat este specificarea cerinţelor. Pentru exemplul de faţă ne vom limita doar
la a cere sistemului reprezentarea cât mai fidelă în memorie a elementelor cu
care va lucra sistemul: fişiere şi directoare.
Considerăm, de asemenea, cazul unui designer începător, care doreşte să
specifice soluţia pentru sistemul nostru software, fără însă a utiliza design
pattern-uri.
Tehnica pe care o folosim pentru a începe specificarea componentelor
soluţiei este bazată pe identificarea substantivelor şi verbelor. Avem, astfel,
fişier şi director. Observând faptul că orice fişier are un director în care este
stocat pe disc, ajungem la prima formă a soluţiei pentru sistemul nostru,
reprezentată de clasa din Figura 134. Se observă că avem doar clasa File, ce
conţine atributele pe care orice fisier le are: nume, extensie, dimensiune şi
folder.

Figura 134. Specificarea UML a clasei File

Deoarece cerinţele sistemului par să fie îndeplinite, trecem mai departe


şi continuăm specificarea celorlaltor părţi ale sistemului software (pe care,
deoarece nu fac obiectul exemplului de faţă, nu le menţionăm).
Datorită faptului că întregul sistem software manevrează fişiere, clasa de
alături este folosită în numeroase locuri în cadrul aplicaţiei. Dezvoltarea
proiectului este una iterativă şi incrementală, şi, ca atare, în momentul în care
reluăm specificarea cerinţelor şi ajungem la partea de copiere de fişiere şi de
ştergere de directoare, realizăm că sistemul ales nu mai corespunde. Fiecare

555555555555555
fişier conţine directorul propriu, ceea ce face foarte dificilă ştergerea unui
director şi modificarea structurii din memorie, fiind necesare numeroase căutări
şi prelucrări pe stringuri.

Figura 135. Varianta îmbunătăţită a diagramei UML a claselor pentru


exemplul practic comentat

În consecinţă, reluăm analiza sistemului de fisiere şi directoare, şi


realizăm că avem nevoie de încă un obiect, şi anume folder, care să conţină un
set de fişiere şi un set de directoare (deoarece acestea sunt obiectele pe care un
director le conţine).
Prin urmare, construim clasa Folder şi ajungem la diagrama din Figura
135. Se observă că avem clasa Folder, ce conţine zero sau mai multe fişiere şi
zero sau mai multe directoare (referinţe către obiecte din clasa Folder).
Această nouă formă a diagramei claselor, referitoare la păstrarea în
memorie a directoarelor şi fisierelor este mult îmbunătăţită faţă de varianta
iniţială. Putem acum să implementăm eficient operaţii de adăugare de fişiere la
un folder sau de ştergere a folderelor. Practic, sunt permise toate operaţiile care
sunt întâlnite în activităţile de zi cu zi în prelucrarea şi manevrarea structurilor
de fişiere şi directoare.
Întregul sistem este dezvoltat utilizând structura de clase de mai sus, şi
în iteraţiile următoare nu mai sunt întâlnite probleme în această parte a
aplicaţiei. Putem spune, în acest moment, că soluţia aleasă pentru modelarea
structurilor de fişiere şi directoare este la un nivel calitativ bun, satisfăcând
toate cererile posibililor clienţi faţă de sistemul rezultat.
Şi totuşi, după cum orice programator şi designer de sistem software se
aşteaptă, apar anumite schimbări în cerinţele clienţilor şi în specificaţiile
proiectului. Astfel, la o anumită fază de dezvoltare a soluţiei, clientul vine cu

666666666666666
cererea ca acest produs software să fie portabil, şi să poată rula şi pe sistemele
Unix / Linux. Presupunem că toate părţile, în afară de cea menţionată mai sus,
sunt în regulă, asigurând flexibilitatea necesară utilizării aplicaţiei pe noul
sistem de operare.
Şi atunci, se pune problema dacă această parte a sistemului, cea care se
ocupă cu reprezentarea în memorie a structurilor de fişiere şi directoare poate
face faţă noilor cerinţe. La o analiză amănunţită se observă că răspunsul este
negativ. Doarece sistemul Unix conţine şi alte obiecte care pot exista pe disc, în
afară de fisiere şi directoare (de exemplu, link-uri către diferite fişiere şi
directoare, pipe-uri, etc), soluţia de mai sus nu face faţă cerinţelor. A încerca să
simulăm noile obiecte prin intermediul celor existente este impropriu.
Ajungem, astfel, în faţa necesităţii de a modifica clasele Folder şi File şi, din
păcate, şi alte părţi ale sistemului, care foloseau aceste clase.
Deşi există mai multe soluţii la problemele menţionate mai sus (ca de
exemplu adăugarea în clasa Folder a unui vector de referinţe la instanţe ale
claselor Link, Pipe etc) nici una nu este pe deplin fiabilă. Trecem, astfel, la o
reanalizare a acestei părţi a sistemului, şi observăm că, de fapt, toate clasele
Folder, File, Link, Pipe, etc au un anumit lucru în comun: sunt obiecte ce pot
exista pe disc, cu anumite relaţii de compoziţie şi agregare între ele.

Figura 136. Rezultatul final al efortului de modelare a soluţiei pentru


exemplul practic comentat

Specificând aceste relaţii şi modul de compoziţie al obiectelor, ajungem


la diagrama claselor din Figura 136. După cum se observă, clasa Folder conţine
un vector de DiskItems (deci, practic, poate conţine orice element ce derivează
din DiskItem, ceea ce este normal şi natural) şi fiecare alt element diferit de
Folder are propria lui clasă.

777777777777777
Pe langă faptul că soluţia de mai sus este portabilă, scalabilă şi fiabilă,
este şi rezistentă la schimbări. Orice altă cerinţă (noi elemente ce trebuie
suportate, sau noi modalităţi de agregare) nu face decât să necesite adăugarea
de clase noi în sistemul curent. Deoarece, din acest moment, sistemul va lucra
cu referinţe de tip DiskItem, viitoarele modificări nu vor afecta clasele
existente, ci numai vor adăuga functionalităţi noi.
Încheiem aici exemplul practic de design al unui sistem software.
Analizând atent soluţia finală (care oferă gradul maxim de eficienţă în
specificarea soluţiei sistemului software) se observă că s-a ajuns la
implementarea design pattern-ului Composite. Deoarece spuneam că
designerul dat ca exemplu aici nu cunoştea design pattern-uri (lucru care, de
altfel, putem spune că s-a şi observat în numărul mare de iteraţii şi soluţii
nereuşite), timpul necesar până când sistemul (sau cel puţin partea din sistem
aferentă reprezentării structurii de fisiere şi directoare) a ajuns la o formă de
eficienţă maximă, este foarte mare, şi o mulţime de modificări au fost necesare,
ataât în structura de clase aferentă fişierelor şi directoarelor, cât şi în structura
celorlaltor clase din sistem care utilizau aceste obiecte. Rezultă, astfel că prin
folosirea design pattern-urilor timpul si numărul de iteraţii necesare dezvoltării
unui sistem software care oferă fiabilitate, reutilizabilitate, scalabilitate şi
portabilitate ar fi mult micşorate.

8.1.5 Avantajele utilizării Design Pattern-urilor


Din ideile şi exemplele prezentate până în acest punct se pot trage o
serie de concluzii cu privire la design pattern-uri şi la avantajele evidente pe
care acestea le oferă designerilor de sisteme şi arhitecturi software.
Primul dintre ele ar fi că design pattern-urile ajută la observarea şi
evidenţierea abstracţiilor şi obiectelor ascunse care sunt necesare pentru
specificarea soluţiei. Doarece omul are tendinţa de a trece cu vederea anumite
aspecte, sau de a nu gândi la un nivel de abstractizare suficient de mare, o serie
de obiecte, unele dintre ele vitale pentru arhitectura sistemului, nu sunt
observate în timp util, necesitând, mai tarziu, schimbări în structura claselor, în
particular şi a sistemului în general. Doarece design pattern-urile sunt soluţii la
probleme generale ce au fost testate, “întoarse pe toate feţele” şi implementate
în numeroase sisteme, toate aspectele ascunse sau la care designer-ul trebuie să
acorde o mai mare atenţie sunt scoase la lumină şi prezentate în forma exactă în
care ar trebui folosite în specificarea soluţiei.

888888888888888
Al doilea avantaj pe care design pattern-urile îl aduc este specificarea
nivelului optim de granularitate6 al obiectelor. Se ştie că specificarea unei
soluţii software ce conţine 2-3 clase (în condiţiile în care sistemul software ce
este implementat este de anvergură) ce devin nişte mici “monstri”, pe măsură
ce implementarea sistemului se apropie de final este ineficientă şi aproape
imposibil de întreţinut. Pe de altă parte, a cădea în extrema cealaltă este, de
asemenea, un lucru negativ. Un număr prea mare de clase, cu obiecte prea fine
induce o utilizare de resurse peste nivelul acceptabil, şi reprezintă un coşmar
pentru programatori.
Design pattern-urile specifică nivelul de granularitate optim pentru
sistem şi pentru solutia software. Prin evidenţierea clară a obiectelor şi a
relaţiilor dintre ele, designer-ul poate evalua uşor gradul de fineţe al obiectelor
ce vor compune soluţia finală.
Specificarea interfeţelor obiectelor este iarăşi un punct cheie în
specificarea soluţiei unui sistem software, şi, în 99% dintre cazuri, reprezintă o
sarcină dificilă, ce poate avea consecinţe negative atât asupra sistemului în
cauză cât şi asupra clienţilor lui. Design pattern-urile ajută la specificarea
interfeţelor prin evidenţierea elementelor cheie pentru fiecare clasă în parte, şi
de asemenea, pot specifica ce anume să NU se pună în interfaţă. Nu în
ultimul rând, design pattern-urile pot specifica relaţiile între interfeţele
diferitelor clase. În particular, poate fi necesar ca unele clase din cadrul soluţiei
sistemului software să necesite un set de interfeţe similare, pentru a permite
comunicarea optimă dintre ele.

8.1.6 Principiile impuse de paradigma Design Patterns


În momentul în care design-ul unui sistem este realizat utilizând design
pattern-uri, o serie de concepte şi principii sunt utilizate, uneori fără ca
designer-ul să observe acest lucru. Mai jos sunt evidenţiate o parte dintre
acestea:

 Programarea la nivel de interfaţă, şi nu la nivel de


implementare; prin utilizarea constantă a interfeţelor şi
construirea relaţiei dintre clienţi şi soluţia software prin
intermediul interfeţelor, clienţii rămân în afara
6
În acest context granularitatea se referă la modul în care clasele definitoare ale
obiectelor mapează domeniul problemei pe domeniul soluţiei. Excesul de granularitate
generează redundanţă, care diminuează semnificativ performanţele de moment ale
sistemului. Deficitul de granularitate generează dificultăţi de acomodare a sistemului
la cerinte noi, oricare ar fi complexitatea acestora.

999999999999999
implementării claselor, nu au cunoştinţă de modul de
implementare al acestora şi ca atare nu sunt legaţi de o
anumită implementare sau algoritm. De asemenea, aceştia
nu ştiu ce clase implementează interfaţa pe care ei o
folosesc, permiţând dezvoltatorilor software să opereze
schimbări la nivel de implementare (pentru a îmbunătăţi sau
eficientiza modul de implementare al operaţiilor din spatele
interfeţei) fără ca utilizatorii (clienţii) interfeţelor să aibaă de
suferit. Programatorii OO recunosc în descrierea de mai sus
exigenţele principiului încapsulării.

 Utilizarea compoziţiei în defavoarea moştenirii;
aceasta deoarece moştenirea (şi relaţiile dintre obiecte ce
rezultă din aceasta) este definită la compilare, legătura
stabilitaă între obiecte este statică, neputând fi modificată în
timpul rulării. Pe de altă parte, clasa derivată devine extrem
de legată de clasa de bază, orice modificare în clasa de
bazaă având ecouri (uneori de anvergură) în toate clasele
derivate din aceasta. Ca atare, prin utilizarea compoziţiei în
defavoarea moştenirii se elimină toate aceste dezavantaje, şi
se permite schimbarea dinamica a relaţiilor dintre obiecte, în
timpul rulării, fără a necesita recompilarea programului. Ca
o observaţie în plus în favoarea compoziţiei, atunci când se
derivează o clasă dintr-o altă clasă, şi se suprascriu unele
metode, trebuie luate în considerare diversele dependenţe
dintre metodele clasei de bază, dependenţe ce trebuie
păstrate şi în clasa derivată. Este astfel nevoie de un nivel
avansat de înţelegere a comportamentului şi a modului de
implementare al clasei de bază, pentru a asigura un
comportament fiabil pentru clasa derivată.

 Utilizarea delegării pentru a obţine aceleaşi avantaje
ca şi în cazul utilizării moştenirii; prin utilizarea delegării
(construirea unor metode care nu fac decât să trimită cererea
mai departe la metodele unui obiect conţinut în interiorul

101010101010101010101010101010
clasei), se obţine acelaşi comportament ca în cazul
moştenirii, unde clasele pot apela metode din clasa de bază,
prin intermediul constructiei super.metodaX. Avantajul
major pe care delegarea îl aduce în faţa moştenirii este
posibilitatea ca acest apel să ajungă la un alt obiect decât cel
considerat iniţial (prin intermediul polimorfismului, de
exemplu). Se asigură astfel un grad maxim de dinamicitate
şi se permite schimbarea comportamentului în timpul rulării,
totul cu un efort minim din partea celorlaltor clase ce
compun soluţia sistemului software.

 Crearea design-ului unei soluţii software pentru a
rezista schimbărilor: design pattern-urile oferă soluţia
pentru o anumită problemă astfel încât anumite părţi ale
sistemului să poată evolua independent de altele. Ca un
exemplu, gradul maxim de flexibilitate poate fi observat în
cadrul design pattern-ului Bridge, care permite atât
interfeţelor cât şi implementărilor să varieze independent
una de alta.

 Avantajele design pattern-urilor evidenţiate pe


exemple
Principiile şi avantajele enumerate mai sus pot fi sistematizate în câteva
avantaje concrete. Mai jos este prezentată o listă cu aceste avantaje, pentru
fiecare evindenţiindu-se design pattern-urile în care avantajul respectiv se
observă cel mai bine (aceste design pattern-uri sunt cele prezentate în [5]):
 Crearea indirectă a obiectelor, fără a utiliza clasa concretă
ce implementează comportamentul obiectului: Abstract
Factory, Factory Method, Prototype;

 Independenţa faţă de o anumitaă operaţie. Apelarea unei
operaţii, fără a şti cine va realiza, efectiv, operaţia: Chain of
responsibility, Command;

11 11 11 11 11 11 11 11 11 11 11 11 11 11 11
 Independenţa faţă de platforma software şi hardware.
Crearea sistemelor având cât mai puţine dependenţe de
platforma hardware sau de API-ul platformei software:
Abstract Factory, Bridge;

 Independenţa faţă de reprezentarea şi implementarea unui
obiect. Clienţii care ştiu cum este implementat un obiect sau
care este locaţia acestuia s-ar putea să fie nevoiţi să facă
modificări, odată cu schimbarea obiectului respectiv:
Builder, Iterator, Strategy, Template Method, Visitor;

 Cuplarea slabă între obiecte. Cuplarea slabă între clase
creşte portabilitatea sistemului şi permite claselor să fie de
sine stătătoare, fără a avea nevoie de alte clase pentru a
putea efectua operaţiile din interfaţa acestora: Abstract
Factory, Bridge, Chain of Responsibility, Command,
Facade, Mediator, Observer;

 Extinderea funcţionalităţii prin compozitie şi prin
delegare, în defavoarea moştenirii oferă flexibilitate claselor
şi relaţiilor dintre acestea. Posibilitatea ca relaţiile dintre
obiecte să fie modificate în timpul rulării face ca sistemul să
capete robusteţe, fiabilitate şi, mai ales, flexibilitate şi
rezistenţă în faţa schimbărilor: Bridge, Chain of
Responsibility, Composite, Decorator, Observer,
Strategy;

 Posibilitatea de a altera clasele şi interfeţele acestora în
condiţiile în care codul sursă nu este disponibil sau aceste
schimbări ar duce la efecte colaterale nedorite: Adapter,
Decorator, Visitor.
În consecinţă, design pattern-urile descriu modul în care interacţionează
clase şi obiecte construite astfel încât să rezolve o problemă generală de design
într-un context particular. (idee preluată din [5]).

121212121212121212121212121212
8.1.7 Componentele unui design pattern
În general un pattern este format din patru părţi esenţiale:

 Numele: este o modalitate de referire la pattern, un


handle ce permite cunoscătorilor să se refere la nucleul
pattern-ului şi la designul pe care acesta îl promovează. Se
permite, astfel, descrierea unui sistem şi, implicit, dialogul
dintre designeri la un nivel mult mai mare de abstractizare.
Posesia unui vocabular despre lumea pattern-urilor ne
permite să vorbim despre ele cu persoane cunoscătoare de
design pattern-uri, cu membrii echipei, cu diverşi colegi, să
le includem în documentaţie, etc. Pe scurt, facilitează
dialogul despre design pattern-uri precum şi despre
avantajele şi dezavantajele lor;

 Problema: descrie sistemul de condiţii ce trebuie
îndeplinite înainte de a aplica pattern-ul în condiţii
favorabile. Explică problema ce este rezolvată şi contextul
în care aceasta apare. Poate descrie, de asemenea, clase şi
modalităţi de îmbinare a obiectelor care sunt deficitare
pentru un sistem, sau a căror prezenţă indică utilizarea
respectivului design pattern;

 Soluţia: descrie elementele care compun soluţia
problemei, modul lor de îmbinare, de colaborare şi
responsabilităţile lor. Soluţia nu descrie direct un mod
particular de implementare, sau într-un anume limbaj de
programare, deoarece un pattern este ca un şablon care poate
fi aplicat în multe probleme. În schimb, design pattern-ul
descrie modul general de aranjare a unor elemente (clase şi
obiecte, în general) pentru a rezolva problema respectivă;

 Consecinţele: reprezintă avantajele şi dezavantajele care
rezultă din aplicarea pattern-ului. Deşi în timpul dezvoltării

13 13 13 13 13 13 13 13 13 13 13 13 13 13 13
aceste consecinţe nu sunt exprimate, totuşi ele sunt extrem
de importante la evaluarea diferitelor soluţii. Consecinţele,
de cele mai multe ori, se referă la viteza şi memoria ocupată.
De asemenea, unele dintre consecinţe se pot referi şi la
necesităţi la nivel de limbaj (îngreunând astfel
implementarea unora dintre ele, în unele limbaje de
programare). Luarea în considerare a acestor consecinţe
ajută la înţelegerea şi evaluarea design pattern-urilor.

8.2 Exemple comentate de şabloane de


proiectare fundamentale

8.2.1 Managementul interacţiunii dintre obiecte cu


Mediator
Introducere
Programarea orientată pe obiecte încurajează distribuţia
comportamentelor pe un set cât mai mare de obiecte. Construcţia unui singur
modul care ştie să “facă de toate” este o tehnică ineficientă, cu repercursiuni
majore asupra stabilităţii şi uşurinţei în întreţinerea codului şi a aplicaţiei.
Totuşi, repartizând comportamentul pe mai multe module (sau obiecte),
se ajunge la necesitatea naturală ca aceste obiecte să comunice între ele, şi mai
mult, putem ajunge la situaţia în care fiecare obiect transmite mesaje tuturor
celorlaltor obiecte, creând astfel conexiuni între oricare două obiecte.
Evident, această situaţie, deşi diferită, de exemplul unui modul gigant,
are aproximativ aceleaşi dezavantaje. Deoarece fiecare obiect ştie (şi are
nevoie) de oricare alt obiect din sistem, întreţinerea aplicaţiei, schimbările
efectuate asupra anumitor obiecte sau adăugarea de noi obiecte în sistem sunt
extrem de dificile. Pentru a exemplifica aceste probleme, putem utiliza cazul
unei casete de dialog, întâlnită în toate aplicaţiile cu interfaţă grafică, casetă
prezentată în Figura 137.
Între obiectele grafice care apar în această casetă există o serie de
dependenţe, prezentate sumar în cele ce urmează:

 În momentul în care este afişată caseta, doar câmpurile Number of


people şi butonul Cancel sunt active. Restul sunt dezactivate, până
când numărul de persoane introdus este între 25 şi 100. În acel
moment câmpurile Date, Start time şi End time devin active, dar

141414141414141414141414141414
permit setarea datelor doar în intervalele în care o cameră de
dimensiuni corespunzătoare numărului de persoane introdus este
liberă. Butoanele radio sunt, de asemenea, activate, şi starea lor se
schimbă în funcţie de numărul de persoane introdus.

 Start time trebuie să conţinaă o dată mai mică decât End time;

 Atunci când utilizatorul specifică un Start time şi un End time, şi


selectează un radio buton, lista de mâncăruri devine activă. Anumite
mâncăruri sunt oferite numai pentru un anumit interval orar, altele sunt
de sezon, neapărând în listă dacă sezonul nu este potrivit;

Figura 137.
Exemplu uzual de
casetă de dialog.

 Atunci când
cel puţin un
tip de
mâncare
este
selectat,
butonul OK
devine activ.

Dacă fiecare
obiect grafic îşi asumă responsabilitatea pentru acţiunile pe care trebuie să le
efectueze atunci când starea lui se modifică, ajungem la diagrama de colaborare
din Figura 138.
Se observă, astfel, că aproape fiecare obiect are legături cu toate
celelalte, ceea ce face acest dialog extrem de greu de întreţinut şi de modificat.
De asemenea, nici un obiect din schemă nu poate fi luat separat pentru a fi
reutilizat, fără a prelua o parte (daca nu toate) din restul obiectelor grafice cu
care acesta are legături.

Rezolvarea problemei folosind design pattern-ul Mediator


Soluţia problemei ilustrate mai sus constă în “adunarea”
comportamentelor împrăştiate pe toate obiectele grafice, şi construirea unei
clase care să conţină aceste comportamente. Astfel se centralizează acţiunile
ce sunt efectuate în momentul în care utilizatorul schimbă starea unui anumit

15 15 15 15 15 15 15 15 15 15 15 15 15 15 15
obiect grafic şi înlătură necesitatea ca un obiect grafic să aibă cunoştinţă de alte
obiecte grafice.

Figura 138. Diagrama de colaborare a componentelor casetei de dialog

Ca efecte benefice, se minimizează numărul de conexiuni dintre obiecte


(obţinându-se un cuplaj slab între obiecte, ceea ce este unul dintre conceptele
fundamentale ale design pattern-urilor) şi se scurtează timpul necesar unui
programator să întreţină sau să modifice interacţiunile dintre obiecte. În loc să
modifice şi să caute diferite acţiuni printr-o serie de obiecte, caută într-un
singur loc, şi modificările sunt localizate doar la nivelul punctului central al
acţiunilor dintre obiecte.
Pentru centralizarea interacţiunii dintre obiecte, construim o clasă
Mediator, care va conţine referinţe la fiecare obiect grafic, şi care primeşte
notificări în momentul în care starea unui obiect s-a schimbat.

161616161616161616161616161616
Figura 139. Diagrama de colaborare în cazul utilizării unui Mediator
În funcţie de obiectul care raportează schimbarea, Mediator-ul comandă
altor obiecte să-şi schimbe starea, corespunzător .
Diagrama din Figura 139 ilustrează colaborarea dintre obiecte. Se
observă că numărul de relaţii s-a redus fix la numărul de obiecte (excepţie
făcând Mediator-ul). Mai mult, fiecare obiect are o singură relaţie, şi anume cea
cu Mediator-ul (iarăşi cu menţiunea că Mediator-ul trebuie să menţină un
număr mai mare de relaţii, deoarece oricare dintre obiecte este posibil să fie
anunţat, în funcţie de tipul acţiunii efectuate de utilizator asupra unui obiect
grafic).

Figura 140. Implementarea Java a design-pattern-ului Mediator

Pentru implementarea în Java a acestei soluţii (şi deci a design pattern-


ului Mediator) se utilizează diagrama de clase din Figura 140, unde elementele
principale sunt :

 EventListener1 … EventListenerN sunt interfeţe ce conţin


semnăturile evenimentelor ce pot apare, legate de anumite tipuri de
obiecte grafice (de exemplu keyPressed, clicked, selected, etc);

17 17 17 17 17 17 17 17 17 17 17 17 17 17 17
 Colleague1 … ColleagueN reprezintă obiectele grafice, ce folosesc
referinţe la obiecte ce implementează interfeţele EventListener1 …
EventListenerN, şi care, în momentul în care detectează o schimbare
în starea lor, anunţă toate obiectele care s-au înregistrat pentru a primi
notificări. Din diagramă se observă că doar Mediator-ul va fi cel care
se va înregistra la fiecare obiect, pentru a fi anunţat de eventualele
schimbări în starea obiectelor.
 Mediator reprezinta clasa care face legătura între obiecte.
Implementând interfeţele EventListener1 …EventListenerN are
posibilitatea de a se înregistra pentru a primi notificări. În momentul în
care unul dintre obiectele grafice anunţă Mediator-ul că starea sa
internă a fost schimbată, în funcţie de obiect, Mediator-ul trimite
mesaje obiectelor a căror stare ar trebui să se modifice corespunzător,
pentru a efectua acţiunile aferente schimbării produse în primul obiect.

Generalizarea diagramelor
Ideea enunţată mai sus exprimă esenţa design pattern-ului Mediator.
Generalizând conceptul, ajungem la diagrama de clase din Figura 141.

Figura 141. Structura de principiu a design-pattern-ului Mediator

Figura 142.

181818181818181818181818181818
Toate clasele Colleague folosesc obiecte ce implementează interfaţa
Mediator, cărora le transmit notificări, anunţând schimbări în starea lor internă.
Obiectele ce implementează efectiv interfaţa Mediator (sau clasa abstractă, în
funcţie de necesităţile aplicaţiei) conţin întreaga logică a programului şi sunt
capabile să răspundă la notificările primite, modificând (sau trimiţând mesaje
de modificare) obiectelor a căror stare ar trebui să reflecte recenta acţiune
efectuată de utilizator.
Dialogul şi modul de relaţionare dintre obiecte este ilustrat în diagrama
din Figura 142.
Se observă, încă odată, faptul că relaţiile dintre obiecte sunt mult
diminuate şi fiecare obiect are o singură dependenţă (care, mai mult, aşa cum s-
a văzut în exemplul practic de mai sus, implementat in Java, mediatorul se
înregistra la obiectele grafice, şi deci dependenţa dintre obiecte şi mediator era
inexistentă).

Consecinţe ale utilizării Mediator-ului


Utilizarea Mediator-ului în design-ul soluţiei unui sistem software are
următoarele avantaje şi dezavantaje:

 Limitează derivările de clase. Deoarece Mediatorul centralizează


comportamentul obiectelor, modificarea acestui comportament nu
necesită decât moştenirea Mediatorului într-o nouă clasă. Restul
claselor Colleagues rămân aşa cum sunt.
 Decuplează colegii între ei. Mediatorul promovează cuplajul slab
dintre obiecte, permiţând colegilor să varieze independent unul de
altul.
 Simplifică protocoalele dintre obiecte. Mediator-ul înlocuieşte
relaţiile de tip many-to-many cu relaţii one-to-many, care sunt mai
simplu de implementat, înţeles şi modificat.
 Abstractizează modul în care obiectele cooperează, făcând
mediatorul un concept independent şi încapsulându-l într-un obiect
separat. Acest lucru favorizează vizualizarea lui independent de restul
obiectelor şi permite programatorului să se concentreze asupra
acţiunilor efectuate şi a colaborării dintre obiecte, neţinând cont de
obiectele fizice efective.
 Centralizează controlul: Mediatorul înglobează relaţiile dintre
obiecte, ceea ce duce la mărirea complexităţii clasei şi la posibila
transformare a clasei într-un gigant greu de controlat.

Exemplu de cod

19 19 19 19 19 19 19 19 19 19 19 19 19 19 19
Următorul cod exemplifică unele părţi ale implementării dialogului
prezentat în subcapitolul de mai sus.

public interface EventListener1


{
void handleEvent1(Component eventSource);
}

Interfaţa EventListener1 conţine doar o metodă, ce urmează să fie


implementată de orice clasă care doreşte să captureze acest tip de evenimente.

public class OkButton extends Jbutton


{
private Vector<EventListener1> listeners;

...
public void onClick()
{
for( int index = 0;
index < listeners.size();
index++ )
{
listeners.elementAt( index ).
handleEvent( this );
}
}

public void addEvent1Listener(


EventListener l )
{
listeners.add( l );
}
}

Clasa OkButton (ce implementează comportamentul butonului OK)


conţine un set privat de referinţe la obiecte ce implementează EventListener1.
În momentul în care starea sa internă se modifică (apare evenimentul Click)
obiectul anunţă toţi listen-erii de faptul că starea sa s-a modificat.

public class Mediator implements EventListener1


{
private OkButton okButton;

202020202020202020202020202020
private OtherField field;

public void handleEvent1(Component eventSource)


{
if ( eventSource == okButton )
{
// comportament aferent butonului OK
field.update();
}

if ( eventSource == otherField )
{
// comportament aferent
// campului otherField
}
}
}

În final, clasa Mediator (care implementează interfaţa EventListener1)


oferă comportamentul necesar realizării interacţiunii dintre obiecte. În
momentul în care starea butonului OK se modifică (=este apăsat), acesta
apelează toţi listen-erii pentru a-i înştiinţa de schimbare. Unul dintre listen-eri
este o instanţă a clasei Mediator, care prelucrează evenimentul.
Identificând faptul că generatorul evenimentului este butonul OK,
efectuează sarcinile aferente acestei schimbări (=anunţă un alt câmp,
otherField, că este cazul să-şi actualizeze setările).
Se observă cu uşurinţă independenţa totală dintre obiectele okButton şi
otherField. Comunicarea dintre ele se realizează exclusiv prin intermediul
Mediator-ului.

8.2.2 Schimbarea on-fly a algoritmilor utilizând


Strategy
Scop
Încapsularea unei familii de algoritmi şi alegerea în mod dinamic a
algoritmului folosit, astfel încât algoritmii să varieze independent de clienţii
care îi folosesc.

Un exemplu de aplicare a pattern-ului


Să presupunem că trebuie să elaborăm un sistem pentru comerţ
electronic, în SUA. În mare, ne imaginăm că sistemul va conţine o parte care se

21 21 21 21 21 21 21 21 21 21 21 21 21 21 21
va ocupa de recepţionarea cererilor de produse şi va trimite aceste cereri către
partea de prelucrare a comenzilor. Fie TaskController obiectul care se va
ocupa de primirea cererilor. Acesta va trimite fiecare comandă către un obiect
de tip SalesOrder. Acesta va fi responsabil cu:

 Oferirea unei interfeţe din care utilizatorul să lanseze comanda;


 Calcularea taxelor de transport pentru produsele comandate;
 Procesarea comenzii şi tipărirea unei chitanţe.

Dintre aceste sarcini, ne vom ocupa, în această lucrare, doar de


calcularea taxelor de transport. Avem, aşadar, propunerea din Figura 143.

TaskController
SalesOrder
+ calcTax ()

Figura 143

Să considerăm, acum, că
sistemul trebuie să îşi extindă aria de vânzare şi în Canada. Pentru produsele
livrate către acest stat, taxele de transport se vor calcula diferit faţă de cele
aplicate produselor vândute în SUA.
Va trebui, deci, să oferim suport şi pentru calcularea taxelor
corespunzătoare produselor livrate către Canada. O soluţie ar fi să extindem
clasa SalesOrder şi să reimplementăm metoda calcTax(), deoarece aceasta
este singura care îşi schimbă comportamentul(ca în Figura 144).

calculeaza
calculeazataxele
taxelepentru
CanadianSalesOrder
TaskController
SalesOrder pentruCanada
SUA

+ calcTax()
+ calcTax()
TaxCalculation

Figura 144.

Dezavantajele soluţiei propuse

222222222222222222222222222222
Această soluţie nu este, însă, cea mai potrivită. Dacă în viitor, sistemul
va trebui să ofere suport şi pentru comercializarea produselor în alte ţări, va
trebui, pentru fiecare stat în parte, să derivăm o nouă clasă din SalesOrder,
deci să creăm, de fiecare dată, un nou obiect responsabil cu prelucrarea
comenzii. Dar singurul lucru care se schimbă în cazul adăugării unei tări, este
modul de calculare a taxelor de transport. Restul prelucrărilor asupra comenzii
rămân neschimbate. Se observă, aşadar, că în această arhitectură, singura
modificare de comportament a claselor derivate din SalesOrder faţă de
clasa părinte este doar în cadrul metodei calcTax(). Modul de folosire al
moştenirii, în acest caz, este impropriu: clasele derivate nu realizează o
specializare veritabilă a clasei părinte, ci îi modifică parţial
comportamentul.
În plus, amestecând modul de calculare a taxelor cu restul
responsabilităţilor obiectului SalesOrder, acesta devine mai greu de întreţinut,
înţeles şi modificat. De asemenea, nu putem varia în mod dinamic tipul de
calculare a taxelor.
Folosind design-ul de mai sus, înainte de extinderea vânzărilor în
Canada, în clasa TaskController am fi avut un apel de genul:

salesOrder = new SalesOrder();


// saleable e produsul comandat
salesOrder.processOrder( saleable );

La extinderea ariei de vanzare şi către Canada, ar fi trebuit să modificăm


apelul de mai sus în:

if(customer.nationality== Nationality.AMERICAN
)
salesOrder = new SalesOrder();
else salesOrder=new CanadianSalesOrder();
salesOrder.processOrder( saleable );

Dacă, în viitor, sistemul va trebui să suporte comercializarea produselor


şi în alte state, ar trebui să modificăm if-ul de mai sus într-un switch şi
pentru fiecare stat în parte să adăugăm un case. Se observă, deci, că pentru
simpla adăugare a unei ţări avem de făcut cel puţin următoarele modificări:

 Derivarea unei noi clase din SalesOrder, care să moştenească


întreg comportamentul lui SalesOrder, dar să reimplementeze
calcTax();

23 23 23 23 23 23 23 23 23 23 23 23 23 23 23
 Adăugarea unui case la switch-ul din TaskController, deci
modificarea unei clase care nu are legătură cu calcularea
taxelor.
Pe langă modificările de mai sus, într-un proiect amplu cum sunt cele de
e-comerce, cu siguranţă vor exista şi alte locuri care nu au legătură cu
calcularea taxelor, dar care vor trebui modificate la fiecare adăugare a unei noi
ţări. Această muncă va deveni cu atât mai meticuloasaă şi mai dificilă cu cât
arhitectura proiectului devine mai amplă.
Arhitectura prezentată mai sus nu încalcă doar principiile programării
obiect orientate, ci şi pe cele ale design pattern-urilor. „Gang of Four”
demonstrează în [5], că este bine să folosim (atunci când este cazul) compoziţia
în locul moştenirii (în exemplul nostru, este exact invers).

produsul+ vandut
calcTax()
e de tip Saleable
TaskController
SalesOrder
CanadianTaxCalculation
USTaxCalculation
TaxCalculation

- taxCalculation : TaxCalculation

+ taxValue( in itemSold : Saleable, in quantity : int ) : int


++ taxValue(…):int
taxValue(…):int

+ calcTax()

Figura 145

Găsirea soluţiei potrivite

242424242424242424242424242424
Să încercăm acum să aducem soluţia la varianta corectă. Ne vom folosi
de un alt principiu al design pattern-urilor şi vom încerca să „încapsulăm ceea
ce este variabil”. Am observat că singurul lucru care variază la adăugarea unei
ţări, este modul de calculare a taxelor. Ar trebui, aşadar, să încapsulăm
calcularea taxelor. Pentru aceasta, vom crea o clasă abstractă – TaxCalculation
şi vom deriva din ea USTaxCalculation şi CanadianTaxCalculation. Vom
folosi acum şi principiul compoziţiei şi vom îngloba un obiect de tip
TaxCalculation în SalesOrder. Obţinem situaţia din Figura 145.
În acest fel, orice obiect de tip SalesOrder va conţine un obiect de tip
TaxCalculation. Atunci când SalesOrder primeşte un obiect Saleable pentru
care s-a făcut o comandă, apelează metoda taxValue(…) a obiectului de tip
TaxCalculation conţinut, pentru a afla taxele ce trebuie adăugate la valoarea
acelui produs.

Avantajele aplicării design pattern-ului Strategy asupra exemplului


considerat
Aplicând Strategy în proiectarea arhitecturii sistemului considerat,
coeziunea acestuia s-a îmbunătăţit: calcularea taxelor se face într-o clasă
separată; astfel, obiectul care se ocupă cu prelucrarea comenzii nu trebuie să
ştie ce tip de calculare a taxelor foloseşte. Pentru orice produs comandat, el va
şti să afle taxele aferente (datorită compoziţiei: orice obiect de tip SalesOrder
conţine un obiect de tip TaxCalculation), fără a avea însă cunoştinţă de felul în
care acestea sunt calculate. Pentru aceasta, obiectele de tip SalesOrder vor fi
create cu un TaxCalculation concret.
Alt avantaj este că flexibilitatea sistemului a crescut considerabil: atunci
când se doreşte adăugarea unei ţări, tot ce trebuie făcut este să creăm un nou
mod de calculare a taxelor, corespunzător acelei ţări. În cazul primei soluţii,
când nu se folosea design pattern-ul, la adăugarea unui stat, trebuia să creăm un
nou obiect de procesare a comenzilor şi să specificăm tuturor clienţilor clasei
SalesOrder că a mai apărut un tip de procesare a comenzilor. Aceasta ar fi fost
aproape imposibil într-un proiect mare, cum este cel de comerţ electronic.
Folosind Strategy, orice adăugare în sistemul de cerinţe nu impune modificarea
întregii soluţii (cum s-ar fi întâmplat dacă păstram prima soluţie, bazată pe
moştenire), ci completează soluţia existentă, făcând-o să corespundă cât mai
multor situaţii reale.
În cazul primei soluţii, obiectul care se ocupa de recepţionarea cererilor
(TaskController) trebuia să ştie ce tip de obiect de procesare a cererilor să
folosească. Cu Strategy, nu mai este nevoie de aşa ceva. Se poate crea un obiect
de configurare, care să decidă ce tip de TaxCalculation trebuie aplicat. Astfel,
se elimină acel switch mare din TaskController.

Implementarea soluţiei

25 25 25 25 25 25 25 25 25 25 25 25 25 25 25
Având arhitectura dată de diagrama de clase specificată, sistemul va
avea următoarea implementare:

public class TaskController


{
.
.
.
public void receiveOrder()
{
.
.
.
// s-a receptionat de la clientul
//customer o cerere
//de quantity unitati din produsul
//saleableItem

SalesOrder salesOrder = new


SalesOrder(
SalesConfig.getTaxCalc( customer ) );
salesOrder.calcTax( saleableItem,
quantity );
}
}

public class SalesConfig


{
public static TaxCalculation getTaxCalc(
Customer customer )
{
switch( customer.nationality )
{
case Nationality.AMERICAN :
return new USTaxCalculation();
case Nationality.CANADIAN :
return new
CanadianTaxCalculation();

262626262626262626262626262626
}
}
}

public class SalesOrder


{
private TaxCalculation taxCalculation;
private int totalValue = 0;
public SalesOrder( TaxCalculation
taxCalculation )
{
this.taxCalculation = taxCalculation;
}

public void calcTax( Saleable soldItem,


int quantity )
{
totalValue += soldItem .getPrice() +
taxCalculation.calcTax( soldItem,
quantity );
}
}

public abstract class TaxCalculation


{
public abstract int taxValue( Saleable
soldItem, int quantity );
}

public class USTaxCalculation extends


TaxCalculation
{
public int taxValue( Saleable soldItem,
int quantity )
{
return 1.2 * quantity *
soldItem.getPrice();
}
}

27 27 27 27 27 27 27 27 27 27 27 27 27 27 27
public class CanadianTaxCalculation extends
TaxCalculation
{
public int taxValue( Saleable soldItem,
int quantity )
{
return 1.3 * quantity *
soldItem.getPrice();
}
}

Structura design-ului
Vom încerca acum, pornind de la diagrama de clase prezentată în
exemplul anterior, să definim structura acestui design pattern.
În exemplul dat, SalesOrder reprezintă contextul în care apare
familia de algoritmi (SalesOrder se ocupă de mai multe operaţii ce trebuie
făcute la apariţia unei cereri, printre care şi calcularea taxelor. Aceasta din urmă
variază ca implementare, deci este reprezentată de o familie de algoritmi).
Familia de algoritmi reprezintă strategia ce poate fi urmată în cadrul
contextului. În exemplul nostru strategia este reprezentată de clasa abstractă
TaxCalculation. Aceasta încapsulează strategiile concrete, adică algoritmii
de calculare a taxelor.
Obţinem, astfel, structura design-pattern-ului Strategy:

282828282828282828282828282828
ConcreteStrategyA
ConcreteStrategyB
ConcreteStrategyC
Strategy
Context

++algorithmInterface()
contextInterface()

Figura 146. Structura şablonului de proiectare Strategy

Astfel, Strategy declară o interfaţă comună tuturor algoritmilor, pe care


Context o foloseşte pentru a apela un anumit algoritm implementatat de un
ConcreteStrategy. Pentru ca acest lucru să fie posibil, Context reţine o
referinţă către un Strategy, iar la creare este configurat, cu un anumit
ConcreteStrategy. Acesta îi este dat, de cele mai multe ori, de către clientul
care îl foloseşte.
Context poate avea o interfaţă prin care Strategy să aibă acces la
anumite date ale sale, în cazul în care în implementarea algoritmului este
nevoie de aceste date, sau poate să se trimită el însuşi ca parametru pentru unele
operaţii definite de Strategy.
În acest fel, Context şi Strategy interacţionează pentru a implemnta un
algoritm. Practic, contextul plasează cererile venite de la clientul său către o
strategie concretă, pentru a fi executate.

Avantajele utilizării design pattern-ului Strategy


 Ierarhia de clase Strategy defineşte o familie de
algoritmi care se pot folosi şi interschimba în cadrul unui
context. Datorită moştenirii, se pot „factoriza” operaţiile
comune tuturor algoritmilor, înglobându-le în părintele
familiei (în Strategy).
 Dacă nu am utiliza Strategy şi am deriva clasa Context

29 29 29 29 29 29 29 29 29 29 29 29 29 29 29
pentru a obţine comportamente diferite, am complica
structura şi comportamentul contextului. Aceasta ar însemna
să amestecăm implemntarea algoritmului cu contextul în
care acesta apare şi astfel contextul ar deveni greu de
înţeles, întreţinut sau extins. În plus, nu am mai putea varia
dinamic algoritmul folosit. Am avea o mulţime de clase
înrudite, cu un comportament complex, dar între care
singura diferenţă ar fi implementarea unui algoritm
(diferenţa ar consta într-o mică parte din întregul
comportament).
 Încapsulând însă algoritmul într-o strategie separată, vom
putea varia algoritmul independent de contextul său, şi astfel
va fi mai uşor să înţelegem, să schimbăm sau să extindem
algoritmul folosit.
 Atunci când înglobăm diverse comportamente într-o
singură clasă, suntem nevoiţi să folosim switch-uri şi
structuri alternative pentru a alege comportamentul dorit.
Folosind Strategy, acestea pot fi eliminate.
Dezavantajele design-ului
 Strategy e folosit pentru a încapsula diverse implementări
ale aceluiaşi comportament. Clientul care foloseşte
contextul în care apare Strategy, trebuie să cunoască în ce
constă fiecare implementare pentru a o alege pe cea
potrivită. Există, astfel, riscul de a expune clientul la
detaliile de implementare. De aceea, Strategy trebuie folosit
doar atunci când variaţia de comportament este relevantă
faţă de client.
 Comunicarea între obiecte s-ar putea încărca cu date care
nu sunt necesare. Aceasta se întâmplă datorită faptului că
Strategy defineşte o interfaţă comună tuturor algoritmilor.
Unii algoritmi, însă, nu au nevoie de toţi parametrii definiţi
în interfaţă, sau chiar de nici unul. Cu toate acestea,
contextul creează şi iniţializează parametri care s-ar putea să
nu fie folosiţi. Pentru a evita acest lucru, între context şi

303030303030303030303030303030
strategie trebuie să existe o cuplare mai strânsă.
 Un alt dezavantaj este acela că Strategy creşte numărul
de obiecte într-o aplicaţie. Acest lucru poate fi evitat atunci
când există un comportament implicit, pe care contextul îl
poate folosi atunci când nu e nevoie de alt algoritm.

Exemplu de Strategy într-o situaţie reală
Input dialog box-urile din Windows folosesc Strategy pentru validarea
textului introdus de utilizator. Pentru diferitele metode de verificare (pentru
numere, date calendaristice, string-uri care să reprezinte un anumit tip de
informaţie, etc.), există clase corespunzătoare, derivate din Strategy-ul
Validator. Input dialog box-urile sunt clienţii, iar Validator reprezintă familia
de algoritmi. Atunci când utilizatorul termină de completat un câmp, acesta
deleagă un validator concret pentru a testa corectitudinea datelor. Dacă se
doreşte adăugarea unui nou tip de validare, tot ce trebuie făcut este derivarea
unei noi clase din Validator.

Concluzii
Cunoscut şi sub numele de Policy, Strategy poate fi folosit atunci când:

 Mai multe clase înrudite diferă doar prin comportamentul


lor. Strategy oferă o modalitate de a configura o clasă cu un
anumit comportament.
 Este nevoie de diverşi algoritmi pentru a implementa
acelaşi comportament.
 Algoritmii folosesc structuri de date complexe, specifice
lor şi clienţii nu trebuie să fie expuşi la aceste detalii de
implemntare.
 O clasă defineşte mai multe comportamente care se
execută în funcţie de multiple structuri alternative care apar
în această clasă. Pentru a elimina structurile alternative, se
crează pentru fiecare ramură a acestora o nouă „strategie
concretă”.
Din toate aspectele prezentate mai sus, se observă cum Strategy
îndeplineşte scopul definit la începutul paragrafului 8.2.2.

31 31 31 31 31 31 31 31 31 31 31 31 31 31 31
8.3 Ataşarea dinamică a responsabilităţilor
utilizand Decorator
Scop
Ataşarea, în mod dinamic, a unor responsabilităţi unui obiect.

Un exemplu de aplicare
Vom considera acelaşi exemplu dat la design pattern-ul Strategy, punând
de această dată accent pe felul în care se realizează tipărirea chitanţei de
vânzare. Aşa cum am specificat în capitolul anterior, SalesOrder se ocupă, în
procesul de prelucrare a comenzilor, şi de tipărirea chitanţei de vânzare. Pentru
aceasta, el foloseşte un obiect specializat în acest sens, SalesTicket. Avem,
astfel, diagrama din Figura 147.

salesTicket.
CanadianTaxCalculation
USTaxCalculation
TaxCalculation
TaskController
SalesTicket
SalesOrder
print();

+ calcTax()
+ printTicket()
+ taxValue(…):int
+ print()

Figura 147

Să presupunem că după ce am implementat şi această parte, apare o


nouă cerinţă: să putem adăuga, opţional, un antet şi/sau o notă de subsol la
chitanţă.
O primă soluţie ar fi ca din clasa SalesTicket să derivăm clasele:

HeaderedTicket,

323232323232323232323232323232
FooteredTicket şi
HeaderedAndFooteredTicket.

Aceasta nu este, însă, o soluţie bună, deoarece creşte numărul de clase


ale aplicaţiei, iar dacă în viitor se va dori ca acele chitanţe care aveau header,
acum să aibă şi footer, sau din cele care aveau şi header şi footer, să se scoată
footer-ul, etc. (se observă că numărul de combinaţii posibile e destul de mare),
va trebui să modificăm codul aplicaţiei în foarte multe locuri, ceea ce ar fi
foarte dificil.
O altă soluţie ar fi ca tipărirea header-ului şi a footer-ului să se facă în
clase separate, specializate, pe care SalesTicket să le folosească. În metoda
print(), se va decide dacă este sau nu nevoie de antet şi/sau subsol pentru
chitanţa curentă:

-SalesTicket
SalesOrder
Footer
Header
apelează header.
printHeader(),
dacă e nevoie

+- tipăreşte
calcTax() chitanţa
printTicket()
+ printHeader()
printFooter()
+ print()
- apelează footer.
printFooter(),
dacă e cazul

Figura 148

Nici aceasta nu este o soluţie prea fericită. Dacă ar trebui ca sistemul să


funcţioneze pentru mai multe firme, fiecare cu stilul propriu de tipărire a
antetelor şi notelor de subsol, sau dacă pur şi simplu se doreşte utilizarea mai
multor tipuri diferite de header-e şi footer-e, acest model nu mai este util.
Un mod de a adapta soluţia şi la aceste cerintţe, ar fi să se folosească
Strategy: clasele Header şi Footer vor deveni clase abstracte, de tip Strategy,
care vor încapsula diferiţii algoritmi de printare. Contextul va fi, desigur,
SalesTicket iar clientul SalesOrder.
Acest design îmbunătăţeşte soluţia noastră, însă nici el nu corespunde
tuturor tipurilor de cerinţe: dacă, de exemplu, se doreşte ca pe aceeaşi chitanţă
să se tipărească mai multe header-e/footer-e diferite, fiecare cu semnificaţia
proprie? Sau dacă pe o chitanţă pe care apar mai multe antete şi/sau note de
subsol, se doreşte, la un moment dat, schimbarea ordinii în care acestea sunt
tipărite? Apare, astfel, o mulţime de combinaţii posibile, căruia soluţia propusă
nu-i mai poate face faţă. Vom vedea, în cele ce urmează cum Decorator poate
îndeplini şi aceste cerinte, într-un mod elegant.

33 33 33 33 33 33 33 33 33 33 33 33 33 33 33
Structura design-ului
Aşa cum am specificat la începutul acestui capitol, rolul design pattern-
ului Decorator este de a ataşa responabilităţi noi unui obiect, în mod dinamic.
Aceasta se face prin legarea obiectului considerat de alte obiecte, specializate în
functionalităţile pe care vrem să le adaugăm. Aceste obiecte „decorează”
obiectul iniţial, motiv pentru care se numesc decoratori.
Pentru a „împodobi” obiectul iniţial cu noile funcţionalităţi, se creează
un lanţ de obiecte, în care fiecare dintre ele îşi aplică funcţionalitatea asupra
obiectului situat „în dreapta” sa în lanţ:

obiect
decorator2
decorator3
decorator1
decorat
Figura 149

În Figura 149, obiectului decorat i se adaugă iniţial functionalităţile date


de decorator3, apoi cele ale lui decorator2 şi, în final, se adaugă
funcţionalităţile lui decorator1. Rezultă, astfel, un obiect care va avea atât
funcţionalităţile obiectului iniţial, cât şi pe cele ale decoratorilor.
Pentru ca această „decorare” în lanţ să fie posibilă, trebuie ca fiecare
decorator să conţină o referinţă, fie la un alt decorator, fie la obiectul ce trebuie
decorat şi să îi adauge acestei referinţe funcţionalităţile proprii. Aşadar,
decoratorii conţin un obiect căruia trebuie să îi adauge responsabilităţi. Pentru
ca acest obiect să poată referi, fie un alt decorator, fie un obiect concret,
înseamnă că el trebuie să fie părinte atât pentru orice decorator, cât şi pentru
orice obiect concret (obiectele concrete nu conţin referintă către acest parinte,
doar decoratorii). Vom denumi acest părinte comun Component. Din el va
deriva obiectul concret (ce poate fi decorat), ConcreteComponent şi părintele
tuturor decoratorilor, Decorator. Obţinem, astfel, structura design-ului din
Figura 150.

343434343434343434343434343434
ConcreteDecoratorA
ConcreteDecoratorB
ConcreteComponent
super.
Component
Decorator
component.
operation();
operation();
addedBehavior()

- component : Component
+ operation()
- addedBehavior()
+ operation()

+ operation()

Figura 150

În diagrama de mai sus, Component este clasă abstractă şi defineşte


interfaţa obiectelor cărora li se pot adăuga responsabilităţi în mod dinamic.
Metoda prin care se poate face acest lucru, este operation().
ConcreteComponent este tipul obiectului ce poate fi decorat. În
operation(), el îşi defineşte comportamentul propriu ce poate fi extins
prin decoratori. Decorator reprezintă clasa de bază pentru toţi decoratorii şi
este abstractă. Ea are interfaţa specificată de clasa părinte, Component, iar în
operation() apelează metoda cu acelaşi nume a componentei conţinute. În
acest fel se asigură faptul că decoratorul va păstra funcţionalităţile obiectului
conţinut. Decoratorii concreţi (ConcreteDecoratorX) îşi execută
functionalităţile proprii (addedBehavior()) înainte sau după apelarea
metodei operation() din părinte, realizându-se astfel adăugarea
responsabilităţilor noi înainte sau după cele ale componentei conţinute.

Aplicarea design-ului în exemplul dat


În exemplul dat la începutul prezentării acestui design pattern,
SalesTicket reprezintă obiectul concret, iar Header şi Footer sunt decoratorii
concreţi. Aplicând Decorator, obţinem următoarea arhitectură a sistemului.

35 35 35 35 35 35 35 35 35 35 35 35 35 35 35
FooterDecorator
TicketDecorator
HeaderDecorator
SalesTicket
SalesOrder
Component
if(printHeader();
super.print();
comp != null )
super.print();
comp.print();
printFooter();

- comp : Component
++ print()
calcTax()
-+ printHeader
printTicket()
printFooter ()
+ print()

+ print()

Figura 151

Spre deosebire de arhitecturile alese în soluţiile anterioare, Decorator


permite adăugarea unor responsabilităţi noi doar anumitor obiecte. În celelalte
soluţii, responsabilităţile noi îi erau adăugate clasei SalesTicket, deci tuturor
obiectelor care o instanţiau. Cu Decorator, însă, se adaugă funcţionalităţi noi
doar anumitor obiecte, iar setul de functionalităţi poate să varieze de la o
instanţă la alta. Astfel, avem o multitudine de posibilităţi de a decora obiectele
de tip SalesTicket şi de a obţine obiecte diferite, care au la bază însuşirile lui
SalesTicket, dar care sunt „învelite” cu diferite funcţionalităţi ale decoratorilor.
Obiectele rezultate în urma decorării, nu mai sunt de tip SalesTicket. Ca
tip, ele vor fi un decorator. Vom vedea cum este posibil acest lucru, studiind
următorul exemplu de implementare a arhitecturii de mai sus.

Implementarea soluţiei
Vom da, în continuare, un exemplu de cod care implementează structura
de clase prezentată mai sus. Pentru diversificare, vom considera că avem patru
decoratori: SimpleHeader, DetailedHeader, SimpleFooter şi DetailedFooter.
Header-ele şi footer-ele simple conţin doar informaţiile elementare, pe când
cele detaliate conţin şi alte informaţii suplimentare.

363636363636363636363636363636
public abstract class Component
{
public abstract void print();
}

public class SalesTicket extends Component


{
public void print()
{
// codul de tiparire a chitantei
}
}

public abstract class TicketDecorator extends


Component
{
private Component comp;

public TicketDecorator( Component comp )


{
this.comp = comp;
}

public void print()


{
if( comp != null )
comp.print();
}
}

public class SimpleHeader extends


TicketDecorator
{
public void print()
{
// codul de tiparire a header-ului
//simplu urmat de tiparirea
chitantei:

37 37 37 37 37 37 37 37 37 37 37 37 37 37 37
//super.print;
}
}

public class DetailedHeader extends


TicketDecorator
{
public void print()
{
//codul de tiparire a header-ului
//deataliat urmat de tiparirea
//chitantei: super.print();
}
}

public class SimpleFooter extends


TicketDecorator
{
public void print()
{
// se tipareste continutul chitantei:
//super.print();
//dupa care se tipareste nota de
//subsol:codul de tiparire a
footer- //ului simplu
}
}

public class DetailedFooter extends


TicketDecorator
{
public void print()
{
//se tipareste continutul chitantei:
//super.print(); dupa care se
tipareste //nota de subsol: codul de
tiparire a //footer-ului detaliat
}
}

383838383838383838383838383838
public class SalesOrder
{
.
.
.
//printTicket() primeste obiectul decorat
//ce trebuie tiparit. Clientul care
//apeleaza aceasta metoda este
responsabil //cu crearea si decorarea
chitantei cu
//antetele si notele de subsol dorite

public void printTicket( Component


salesTicket )
{
salesTicket.print();
}
}

Codul de mai sus poate fi folosit pentru a tipări o mulţime de combinaţii


de tipuri de chitanţe. Dacă dorim tipărirea unei chitanţe simple, vom avea un
apel de tipul:

salesOrder.printTicket(new SimpleHeader( new


SimpleFooter(new SalesTicket() ) ) );

Acest apel are următoarea semnificaţie: se creează un obiect de tip


SalesTicket. Acesta este trimis ca parametru constructorului unui obiect de tip
SimpleFooter. Se creează, astfel, un obiect de tipul SimpleFooter care conţine
o referinţă la un SalesTicket. Acest obiect este trimis ca parametru
constructorului lui SimpleHeader, creându-se astfel un SimpleHeader cu o
referinţă la un SimpleFooter.
Se obţine astfel următoarea structură de obiecte:
aSimpleFootert
aSimpleHeadert
aSalesTicket

comp

Figura 152

39 39 39 39 39 39 39 39 39 39 39 39 39 39 39
Astfel, metodei printTicket() a lui salesOrder îi este transmis un
SimpleHeader. Această metodă va apela metoda print() a obiectului de tip
SimpleHeader trimis ca parametru. În acest moment, se vor efectua
următoarele operaţii:

 Se tipăreşte antetul chitanţei


 Se apelează metoda print() a obiectului de tip
SimpleFooter. În cadrul acestei metode se execută următoarele
instrucţiuni:

o Se apelează metoda print() a obiectului de tip
SalesTicket conţinut de obiectul curent
o Se tipăreşte astfel conţinutul chitanţei
o Se revine în metoda print() a obiectului de tip
SimpleFooter şi se tipăreşte footer-ul chitanţei.

Să presupunem că vrem să tipărim o chitanţă care conţine: un header


simplu, un header detaliat, corpul chitanţei şi un footer simplu. Pentru aceasta,
vom avea următoarea instrucţiune:

salesOrder.printTicket( new SimpleHeader(


new DetailedHeader(new SimpleFooter(new
SalesTicket()))));

Astfel, se asigură tipărirea antetului simplu, apoi a celui detaliat, urmat


de conţinutul chitanţei şi footer-ul simplu (ordinea ultimelor două este dată de
faptul că obiectul de tip SalesFooter face întâi tipărirea obiectului conţinut (a
chitanţei) şi apoi realizează tipărirea proprie).
Alte exemple de aplicare a design pattern-ului
În cazul în care am avea de elaborat un mediu vizual de programare, ar
trebui să oferim suport pentru adăugarea de proprietăţi şi funcţionalităţi
componentelor vizuale. De exemplu, un text area este iniţial o zonă simplă în
care se poate edita text. Este foarte probabil, însă, ca programatorii care vor
utiliza mediul elaborat de noi să dorească să poată adăuga bare de derulare sau
să bordeze cu un anumit tip de linie unele componente.
Pentru aceasta, am putea deriva din clasa care defineşte un text area,
subclase care să specializeze componenta, adaugându-i bare de derulare sau

404040404040404040404040404040
borduri. Aceasta nu este însă o soluţie eficientă şi nici elegantă. Pe de o parte,
am avea un număr foarte mare de clase care ar trebui să deriveze din TextArea,
pentru a acoperi toate combinaţiile: text area cu bară de derulare verticală, cu
bară de derulare orizontală, cu ambele; text area bordat cu linie continuă sau
întreruptă, text area bordat cu linie continua şi cu bară de derulare verticală, etc.
Se observă, deci, că ar trebui să surprindem un număr foarte mare de combinaţii
(şi pentru fiecare astfel de combinaţie va trebui să derivăm câte o clasă) şi asta
doar pentru douaă tipuri de adăugări: bare de derulare şi borduri. Dacă apar şi
alte proprietăţi ce trebuie adăugate, numărul de combinaţii şi, deci, de subclase,
ar deveni copleşitor.
În plus, această metodă nu permite schimbarea dinamică a proprietăţilor
unei componente. Dacă dorim, de exemplu, să adăugăm o bară de derulare
verticală doar în momentul în care textul nu mai poate fi văzut în întregime, sau
să eliminăm bara atunci când nu este nevoie de ea pentru a vizualiza tot textul,
aceste operaţii nu mai pot fi făcute pe modelul actual.
Decorator propune, însă, următoarea soluţie: să definim o clasă de bază
pentru toate componentele vizuale, iar din aceasta să se deriveze atât
componentele vizuale, cât şi decoratorii lor(a se vedea Figura 153).
Un exemplu concret de aplicare a pattern-ului este felul în care se face
citirea de la tastatură în Java:

BufferedReader bufferedReader=new
BufferedReader( new
InputStreamReader( System.in ) );
String line = bufferedReader.readLine();

System.in este un obiect de tip InputStream şi reprezintă obiectul


concret din modelul design pattern-ului. BufferedReader şi
InputStreamReader sunt decoratori şi derivează din clasa abstractă
Reader (tipul generic de decorator). InputStream şi Reader derivează
direct din Object. Diagrama de clase (o parte din ea) este prezentată în Figura
154.

41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
super.draw();
super.draw();
ScrollDecorator
VisualComponent
BorderDecorator
Decorator
TextArea
comp.draw();
drawScrollBar();
drawBorder();

- comp : VisualComponent
+ +draw()
draw()
- -drawScrollBar()
drawBorder()
+ draw()

+ draw()

Figura 153

ObjectReader

InputStream
- lock : Object

BufferedReader InputStreamReader

Figura 154

424242424242424242424242424242
Avantajele utilizării design-ului

 Decorator oferă o modalitate mult mai flexibilă de a


adăuga responsabilităţi noi unor obiecte, decât utilizarea
moştenirii. Folosind decoratorii, funcţionalităţile noi pot fi
adăugate sau excluse în timpul execuţiei, pe când în cazul
utilizării moştenirii, funcţionalităţile se adaugă static şi nu
pot fi eliminate în timpul execuţiei. În plus, folosind
moştenirea, complexitatea sistemului creşte, datorită
numărului mare de clase care se adaugă pentru fiecare
funcţionalitate nouă în parte.

 Alt avantaj este acela că, folosind Decorator, se pot mixa
funcţionalităţile noi, rezultând o mulţime de variante de
obiecte noi. De asemenea, Decorator este singura
modalitate elegantă de a adăuga o proprietate de două sau
mai multe ori. De exemplu, dacă dorim să bordăm o
componentă vizuală cu trei linii continue, ar fi o adevărată
eroare de programare să moştenim clasa Border de trei ori!
Cu Decorator, nu trebuie decât să creăm trei obiecte Border.

 Alt avantaj al acestui design pattern este că se pot adăuga
oricând alţi decoratori, fără a influenţa obiectele decorate.
Acest lucru este posibil deoarece decoratorii sunt
responsabili doar cu funcţionalităţile proprii. În cazul
utilizării moştenirii, există posibilitatea (atunci când tipul
obiectului pe care dorim să îl specializăm este foarte
complex) de a transporta în subclase un bagaj mare de
funcţionalităţi. Astfel, se ajunge la o încărcare inutilă a
memoriei. De asemenea, în loc să încercăm să prevedem ce
comportamente ar putea fi adăugate la o clasă şi să încărcăm
acea clasă cu o mulţime de responsabilităţi, este mult mai
eficient să creăm o clasă simplă, căreia să îi adăugăm
decoratori de fiecare dată când apare o nouă cerinţă.

43 43 43 43 43 43 43 43 43 43 43 43 43 43 43
Specificaţii finale
Pentru a asigura funcţionarea design-ului, obiectele decorate şi
decoratorii trebuie să deriveze dintr-o clasă comună (Component). Este foarte
important ca această clasă să se axeze pe definirea unei interfeţe comune, nu pe
înmagazinarea datelor. Definirea unui comportament concret se va face în
descendenţi (în obiectele concrete). Altfel, riscăm să încărcăm memoria cu date
care nu sunt necesare, datorită apelurilor recursive.
Cunoscut şi sub denumirea de Wrapper, acest design realizează
„învelirea”, decorarea unui obiect cu funcţionalităţi noi, fără ca obiectul să ştie
absolut nimic de decoratorii săi (decorarea este transparentă faţă de obiect). O
altă alternativă de a adăuga funcţionalităţi noi, este folosind Strategy. În acest
caz, însă, contextul ştie de existenţa unor eventuale completări (strategii
concrete).
În concluzie, Decorator se foloseşte atunci când:

 dorim să adăugăm în mod dinamic şi transparent noi


responsabilităţi unui obiect;
 dorim să avem posibilitatea de a elimina în timpul
execuţiei anumite responsabilităţi ale unui obiect;
 extinderea unei clase pentru a adăuga noi responsabilităţi
este nepractică. Aceasta se întâmplă fie când trebuie să
adăugăm atât noi funcţionalităţi, cât şi toate combinaţiile
posibile (în acest caz am ajunge la un număr foarte mare de
clase), fie când clasa ce trebuie extinsă are o interfaţă ce nu
permite acest lucru.
Pentru o utilizare eficientă a design-ului, este bine ca acel client care
foloseşte obiectul decorat să nu se ocupe de crearea lanţului de funcţionalităţi.
În acest fel, clientul nu va fi afectat de adăugarea unor funcţionalităţi.
Pentru aceasta, se foloseşte o altă clasă, de configurare, care îi va da de
fiecare dată clientului obiectul de tip Component dorit.

8.4 Modificarea interfeţei unei clase folosind


Adapter
Scop
Face posibilă comunicarea între clase, convertind interfaţa unei clase
într-o interfaţă accesibilă clienţilor săi. Cu alte cuvinte, realizează o interfaţă

444444444444444444444444444444
pentru un obiect care are funcţionalităţile dorite de noi, dar a cărui interfaţă ne
este incomodă.

Exemplu de aplicabilitate
Să considerăm că trebuie să realizăm o aplicaţie care să permită
desenarea şi colorarea unor figuri geometrice simple, precum punctul, linia şi
pătratul, iar clientul care va folosi aplicaţia să poată lucra cu aceste figuri, fără a
ţine cont cu care dintre ele lucrează la un anumit moment dat. Clientul doreşte,
aşadar, să lucreze cu orice figură geometrică (linie, punct sau pătrat) în acelaşi
fel, fără a fi preocupat de diferenţele dintre ele.
Fiecare figură geometrică va fi responabilă cu desenarea, colorarea şi
ştergerea sa, iar clientul va putea cere oricărei figuri efectuarea acestor operaţii,
fără a şti cum se realizează execuţia lor. Pentru acesta, va trebui să încapsulăm
figurile geometrice într-o clasă abstractă, Shape. Aceasta va conţine interfaţa
pe care o afişează toate figurile geometrice. Se ajunge, astfel, la următoarea
structură:

Client Square
Shape
Point
Line

+ setColor()
+ display()
+ fill()
+ undisplay()

Figura 155

Datorită interfeţei comune (clasa abstractă Shape), clientul va lucra în


acelaşi fel cu figuri geometrice diferite. Ne folosim astfel de polimorfism.
După ce am stabilit această arhitectură, să presupunem că apare o nouă
cerinţă: clientul doreşte ca aplicaţia noastră să poată desena şi colora o nouă
figură geometrică: cercul. Analog modului de lucru cu figurile existente,

45 45 45 45 45 45 45 45 45 45 45 45 45 45 45
clientul doreşte să poată acum lucra cu puncte, linii, pătrate sau cercuri în
aceeaşi manieră, fără a fi interesat de diferenţele dintre ele.
Pentru a adapta sistemul noilor cerinţe, ar trebui ca şi cercul să fie tot un
Shape şi să implementeze metodele de desenare, colorare şi ştergere. Numai că,
dacă implementarea acestor metode a fost relativ simplă pentru punct, linie şi
pătrat, în cazul cercului ele se complică semnificativ. Să presupunem însă că
avem la dispoziţie o bibliotecă în care se găseşte o clasă ce realizează
desenarea, colorarea şi ştergerea unui cerc, dar cu o interfaţă complet diferită de
cea a clasei Shape (a se vedea Figura 156).
Avem astfel funcţionalitatea dorită, dar aparent nu o putem folosi:
ProfessionalCircle nu derivează din Shape (iar pentru a putea lucra cu acest
tip de cerc în acelaşi fel ca şi cu celelalte figuri geometrice, acest lucru este
esenţial), iar metodele care realizează desenarea, colorarea şi ştergerea nu au
denumirile specificate de noi în interfaţă.

ProfessionalCircle
+ drawEmptyCircle()
+ drawFullCircle()
+ delete()

Figura 156

Totuşi, dorim să folosim, cumva, implementările oferite de această clasă.


Cum nu avem acces la codul clasei (caz în care am fi modificat denumirile
clasei şi ale metodelor şi am fi derivat ProfessionalCircle din Shape), singura
soluţie ar fi să modificăm codul scris de noi: să redenumim Shape în
ProfessionalShape, iar metodele de desenare, colorare şi ştergere să aibă
signatura celor din ProfessionalCircle. Această soluţie nu este, însă, nici
elegantă, nici eficientă şi în plus necesită multă muncă. Pe de altă parte, toţi
clienţii lui Shape vor trebui să îşi modifice codul pentru a se conforma noii
situaţii (acest lucru este imposibil: nu le putem cere clienţilor să îşi modifice
codul pentru greşelile noastre de design). Pe de altă parte, dacă în timp se va
cere adăugarea unei noi figuri geometrice complexe pentru care găsim de
asemenea o librărie care să îi implementeze funcţionalitatea, dar cu o interfaţă
diferită, am recurge din nou la această metodă a redenumirii metodelor şi
claselor existente. Toate acestea duc la un sistem instabil pe care clienţii nu vor
dori să-l folosească. Aşadar, această soluţie nici macar nu poate fi luată în
calcul.

464646464646464646464646464646
Folosind Adapter, problema are o soluţie cât se poate de elegantă şi
eficientă: nu trebuie să modificăm nimic din ceea ce este deja scris, ci doar să
adaptăm. Trebuie găsită, aşadar, o modalitate de a „converti” interfaţa clasei
ProfessionalCircle la interfaţa creată de noi. Pentru aceasta, vom deriva din
Shape propria clasă Circle, care va avea funcţionalităţile specificate de Shape,
dar care va folosi ProfessionalCircle pentru a realiza aceste funcţionalităţi.
Circle va conţine un obiect de tip ProfessionalCircle, pe care îl va crea
în constructorul său. Astfel, pentru fiecare obiect Circle creat, se creează
obiectul ProfessionalCircle corespunzător. Operaţiile clasei Circle se vor
realiza prin intermediul clasei ProfessionalCircle. Astfel, display() va
apela drawEmptyCircle(), fill() va apela drawFullCircle(), iar
undisplay() va apela delete(). Astfel, funcţionalitatea lui Circle e
realizată de ProfessionalCircle.
Dacă în viitor apar alte operaţii ce trebuie implementate de fiecare
Shape (deci şi de Circle) şi ProfessionalCircle nu oferă suport pentru aceste
operaţii, ele vor putea fi implementate direct de Circle. De asemenea, dacă
unele metode din ProfessionalCircle ar fi avut mai mulţi / mai puţini parametri
decât metodele corespunzătoare din Shape, Circle s-ar fi ocupat de
transmiterea paramterilor corecţi. Astfel, sistemul rămâne stabil în faţa oricăror
schimbări.
Prezentăm în Figura 157 arhitectura soluţiei.

47 47 47 47 47 47 47 47 47 47 47 47 47 47 47
ProfessionalCircle
Square
Circle
Shape
Point
Line
pc.delete();
Client

- pc : ProfessionalCircle
+ setColor()
+ display()
+ drawEmptyCircle()
+ display()
+ fill()
+ drawFullCircle()
+ fill()
+ undisplay()
+ delete()
+ undisplay()

+ display()
+ fill()
+ undisplay()

1 1

Figura 157
Implementare
Soluţia prezentată mai sus poate fi implementată în felul următor:

public abstract class Shape


{
protected Color color;
public void setColor( Color c )
{
color = c;
}

public abstract void display();


public abstract void fill();

484848484848484848484848484848
public abstract void undisplay();
}

public class Circle extends Shape


{
private ProfessionalCircle pc;
public Circle()
{
pc = new ProfessionalCircle();
}
public void display()
{
pc.drawEmptyCircle();
}

public void fill()


{
pc.drawFullCircle();
}

public void undisplay()


{
pc.delete();
}
}

public class Client


{
private Shape[] myShapes;
.
.
.
public void colorShapes()
{
for( int i = 0; i < myShapes.length;
i++ )
myShapes[i].fill();
}
}

Structura design-ului

49 49 49 49 49 49 49 49 49 49 49 49 49 49 49
Design pattern-ul Adapter are două forme: Class Adapter şi Object
Adapter. În exemplul discutat mai sus, s-a folosit pentru proiectarea soluţiei,
Object Adapter. Class Adapter poate fi folosit doar în limbajele care oferă
suport pentru moştenirea multiplă (de exemplu, C++).
Făcând o analogie cu soluţia prezentată în exemplu, putem deduce
structura pentru Object Adapter prezentată în Figura 158.

Client Adaptee
Adapter
Target
adaptee.specificRequest();

- adaptee : Adaptee
+ specificRequest()
+ request

+ request()

Figura 158

În diagrama din Figura 158, Target defineşte interfaţa pe care clientul o


foloseşte. Adaptee are comportamentul dorit de client, dar interfaţa sa nu este
adecvată celei pe care clientul o foloseşte. De aceea, se foloseşte clasa
Adapter, care are rolul de a adapta interfaţa lui Adaptee la cea pe care o
aşteaptă clientul. Pentru aceasta, el derivează din Target, pentru a avea interfaţa
dorită de client şi conţine o referinţă la un obiect Adaptee prin intermediul
căreia realizează funcţionalitatea dorită de client. În acest fel, Adapter îi oferă
clientului funcţionalitatea de care are nevoie sub forma interfeţei pe care acesta
se aşteaptă să o folosească.
Class Adapter foloseşte conceptul de moştenire multiplă pentru a
adapta o interfaţă la alta:

505050505050505050505050505050
Adaptee
Adapter
Target
specificRequest();
Client

+ specificRequest()
+ request()

Figura 159

În acest caz, Adapter moşteneşte cele două interfeţe (cea pe care o


foloseşte clientul şi cea a clasei care oferă funcţionalitatea cerută), având astfel
acces atât la interfaţa dorită de client, cât şi la cea a implementării
comportamentului dorit. În momentul în care clientul pasează o cerere către
Adapter prin interfaţa definită de Target, acesta apelează metoda
corespunzătoare din Adaptee. Astfel, i se oferă clientului funcţionalitatea
cerută prin interfaţa dorită.

Caracteristici ale design-ului

Class Adapter
 Nu poate fi folosit atunci când vrem să adaptăm interfaţa
unei ierarhii de clase (atât interfaţa unei clase, cât şi
interfeţele corespunzătoare claselor care derivează din ea);
 Îi permite clasei Adapter să moştenească o parte din
comportamentul clasei adaptate; de aceea, din punct de
vedere conceptual, Adapter nu este un adaptator pur, el
fiind în acelaşi timp un adaptat (derivează din Adaptee);
 Permite accesul la comportamentul adaptat direct prin
obiectul Adapter, nu mai este nevoie de o referinţă
suplimentară la Adaptee (scade astfel numărul obiectelor
din aplicaţie, deci gradul de ocupare a memoriei).

Object Adapter

51 51 51 51 51 51 51 51 51 51 51 51 51 51 51
 Permite adaptarea interfeţelor mai multor clase, precum
şi a unei ierarhii de clase (deoarece Adapter conţine o
referinţă la un Adaptee, acea referinţă poate fi către orice
subclasă a lui Adaptee); de asemenea, se pot adăuga
funcţionalităţi tuturor claselor derivate din Adaptee;
 Pentru a extinde comportamentul clasei Adaptee şi
pentru a adapta noua clasă, specializată, trebuie schimbată
referinţa din Adapter către clasa specializată.
Un dezavantaj al acestui design pattern, este că nu oferă transparenţă
interfeţelor către mai mulţi clienţi. De cele mai multe ori, construim un adapter
pentru a adapta interfaţa unei clase la cea pe care o foloseşte un client. Astfel,
realizăm comunicarea între cele două clase. Dacă, însă, doi clienţi distincţi
doresc să acceseze funcţionalitatea aceluiaşi obiect în mod diferit, un simplu
Adapter nu mai poate realiza acest lucru. Pentru aceasta, este nevoie de un
adapter bivalent, sau un adapter în ambele sensuri. Acesta moşteneşte
interfeţele celor doi clienţi (folosind moştenirea multiplă sau implementarea a
două interfeţe) şi conţine o referinţă la obiectul adaptat. În acest fel, se
realizează comunicarea în ambele sensuri între cei doi clienţi şi obiectul
adaptat.
Printre dezavantajele modelului Class Adapter, era şi faptul că Adapter,
derivând din Adaptee, poate fi privit în acelaşi timp şi ca adaptator şi ca
adaptat. Acest inconvenient poate fi eliminat în C++ cu ajutorul modificatorilor
de acces. Astfel, Adapter poate deriva în mod public din Target, dar în mod
privat din Adaptee (se derivează public interfaţa şi privat implementarea). În
acest fel, Adapter nu va mai fi un Adaptee, ci un adaptator pur.

Ca şi concluzie, Adapter este un design pattern cu ajutorul căruia se pot


integra clase existente în sisteme noi, chiar şi în cazul în care interfeţele
claselor existente diferă considerabil de cele pe care le folosesc noile sisteme.

8.5 Utilizarea obiectelor înşelătoare cu ajutorul


Proxy-ului
Studiu de caz

Deseori, în dezvoltarea sistemelor şi aplicaţiilor software apare


necesitatea utilizării obiectelor complexe, ce consumă o cantitate însemnată de

525252525252525252525252525252
resurse. De exmplu, dacă luăm cazul unui procesor de texte, acesta poate
compune un document din text şi imagini. Aceste imagini, însă, pot fi deosebit
de complexe, pot conţine numeroase figuri (mai ales dacă sunt în forma
vectorială) şi pot ocupa o cantitate apreciabilă de memorie.
Evident, micşorarea complexităţii şi a timpului necesar încărcării acestor
imagini şi obiecte nu poate fi realizată. Ajungem astfel ca în momentul în care
deschidem un document ce conţine multe imagini să aşteptăm până când
editorul încarcă în memorie toate imaginile, cu toate că aceste imagini nu sunt
afişate imediat pe ecranul monitorului, fiind dispuse, eventual pe paginile
următoare. Apare, astfel, ideea de a amâna încărcarea lor de pe disc până când
utilizatorul ajunge efectiv la vizualizarea lor. Această tehnică este, de altfel,
utilizată de toate editoarele şi viewer-ele de text performante, de genul
Microsoft Office Word, Acrobat Reader, etc.
În consecinţă, problema cu care ne confruntăm în acest caz este
dimensiunea mare a imaginilor din document, şi dorim să avem posibilitatea de
a amâna încărcarea lor pânaă când utilizatorul comandă acest lucru (eventual
prin scroll-down până la pagina care conţine imaginile). Totuşi, nu putem
amâna tot procesul de încărcare a imaginilor, deoarece textul trebuie formatat
respectând dimensiunile şi locaţiile imaginilor.

Problema rezolvată de design pattern

Furnizarea unui surogat pentru a controla un obiect complex.


Motivul principal pentru care dorim să folosim un surogat în locul
obiectului complex efectiv este dimensiunea mică a obiectului surogat, denumit
în continuare proxy. Acest obiect are doar o mică parte din capacităţile
obiectului complex efectiv iar memoria ocupată este extrem de mică (de aici şi
timpul foarte mic necesar creării lui sau încărcării lui de pe disc).
Soluţia la problema prezentată mai sus este folosirea unei imagini proxy,
care conţine o referinţă la imaginea efectivă, pe care însă nu o încarcă decât
atunci când clientul îi cere să deseneze imaginea (acesta este, de fapt,
momentul efectiv în care imaginea trebuie încărcată de pe disc, pentru a fi
afişată utilizatorului).
Ajugem, astfel, la schema din Figura 160.

53 53 53 53 53 53 53 53 53 53 53 53 53 53 53
Figura 160

Documentul foloseşte image proxy-ul pentru a obţine datele despre


imagine, atunci când este necesară formatarea textului şi calcularea ariei
ocupate de imagine. Proxy-ul nu reţine decât numele fişierului ce conţine
imaginea, urmând ca în momentul în care editorul de texte este nevoit să
afişeze efectiv imaginea respectivă, proxy-ul să încarce de pe disc imaginea şi
să returneze (sau să comande desenarea ei) editorului de texte.
În consecinţă, mergând pe ideea de mai sus, ajungem la diagrama de
clase din Figura 161, pentru a rezolva problema editorului de texte :

Figura 161

În diagrama de clase de mai sus, DocumentEditor foloseşte o referinţă


de tip Graphic (se observă încă odată programarea la nivel de interfată, şi nu de
implementare) din care derivează două obiecte: Image (obiectul complex
efectiv) şi ImageProxy (obiectul surogat, care poate oricând lua locul unui
Image). ImageProxy reţine o referinţă la un Image, obiect ce este instanţiat
doar în momentul în care editorul de texte cere explicit acest lucru (prin
apelarea metodei Draw()).
Toate metodele, în afară de Draw(), pot fi manevrate de către
ImageProxy fără a necesita existenţa în memorie a unui Image complex.
Referinţa la Image a lui ImageProxy rămâne ne-setată (conţine
valoarea null) până când editorul de texte cere desenarea imaginii. În acest
moment ImageProxy-ul nu mai este capabil să se substituie imaginii efective,

545454545454545454545454545454
şi este nevoit să încarce de pe disc imaginea, şi să îi trimită acesteia mesajul de
Draw().

Generalizarea conceptului de Proxy


Diagramele de mai sus sunt construite pentru a rezolva problema
editorului de texte. Avem, deci, contextul particular în care este aplicat design
pattern-ul Proxy.
Însă, conform definiţiei, design pattern-ul Proxy rezolvă o problemă
generală de design. Această problemă se referă la utilizarea unui surogat pentru
manevrarea unui obiect complex, şi amânarea construirii obiectului până când
va apare efectiv această necesitate.
Reluăm în continuare diagramele de mai sus, generalizând puţin
conceptul. Ca participanţi în specificarea problemei, avem Clientul, Proxy-ul
şi Subiectul real. Clientul este cel care utilizează Subiectul real, şi reprezintă,
de fapt, sistemul software. Subiectul real este obiectul complex a cărui
construire dorim să o amânăm.
Diagrama de schimburi de mesaje este prezentată în Figura 162.

Figura 162

Clientul consideră că lucrează cu subiectul real, căruia îi poate


transmite toate mesajele publicate de interfaţa acestuia. În schimb, la
compunerea obiectelor, între client şi subiectul real se interpune un alt obiect,
proxy-ul, a cărui interfaţă este identică cu cea a subiectului real. Astfel, clientul
nu necesită nici un fel de modificări, utilizând proxy-ul ca pe un subiect real.
Toate mesajele care pot fi manevrate de proxy rămân la nivelul acestuia,
neajungând la subiectul real, dacă acest subiect nu a fost încărcat în memorie.
Din momentul în care subiectul real (în urma sosirii în proxy a unui mesaj ce nu
mai poate fi manevrat de acesta fără crearea subiectului) este construit, toate
mesajele sosite la proxy sunt trimise mai departe (prin folosirea delegării) către
subiectul real.
Acest mod de transmitere a mesajelor este susţinut de următoarea
diagramă de clase (Figura 163, care reprezintă diagrama generală a design
pattern-ului Proxy).

55 55 55 55 55 55 55 55 55 55 55 55 55 55 55
Consecinţe pozitive şi negative ale utilizării Proxy-ului
Design pattern-ul Proxy introduce un nou nivel de indirectare în
momentul în care vine vorba de accesarea unui obiect. Prin folosirea Proxy-
ului, apar următoarele consecinţe :
 Proxy-ul poate ascunde faţă de client faptul că obiectul
referit se află în spaţiul de adrese;

Figura 163

 Proxy-ul permite optimizarea creării obiectelor,


construind instanţe ale obiectelor doar în momentul în care
acest lucru este strict necesar. Deoarece acest lucru este
posibil să nu se întâmple niciodată (în exemplul de mai sus,
utilizatorul doar deschide documentul, află câteva
informaţii, şi îl închide, fără a trece peste pagini şi fără a
comanda încărcarea imaginilor) se obţine o optimizare a
timpului de încărcare şi a operaţiilor efectuate de procesor;
 Un Proxy poate asigura un nivel suplimentar de protecţie
asupra subiectului real, nepermiţând clenţilor să acceseze
obiectul decât după o autentificare prealabilă (sau dacă
respectivul client deţine rolul cerut pentru a accesa obiectul)
 Pe baza Proxy-ului funcţionează conceptul de smart-
pointers. Un smart pointer este un pointer ce reţine numărul
de obiecte ce referă subiectul real, eliminând obiectul din
memorie în momentul în care nici un alt obiect nu mai

565656565656565656565656565656
referă subiectul.
Exemplu de cod
În continuare sunt prezentate exemple de cod ce demonstrează modul în
care se poate implementa design pattern-ul Proxy în Java:

public interface Graphic


{
void draw();
Extent getExtent();
void load ( String fileName );
void store( String fileName );
}

Interfaţa Graphic este implementată atât de subiectul real cât şi de


ImageProxy, şi este folosită de către client pentru referirea uniformă la
imagini.

public class Image implements Graphic


{
private Extent thisExtent;

public void draw()


{
// efectueaza operatii complicate de
//desenare
}

public Extent getExtent()


{
return thisExtent;
}

public void load( String fileName )


{
// incarca imaginea de pe disc
}

public void store( String fileName )


{
// salveaza imaginea pe disc

57 57 57 57 57 57 57 57 57 57 57 57 57 57 57
}
}

Image reprezintă imaginea reală, obiectul complex pe care dorim să îl


ascundem de clienţi şi a cărui creare dorim să o amânăm.

public class ImageProxy implements Graphic


{
private Image imageImp = null;

private Extent localExtent = null;

public String fileName = null;

public void load( String fileName )


{
// nu se incarca imaginea de pe disc
// se incarca in schimb doar
//dimensiunile imaginii (extent)
this.fileName = fileName;
}

public void store( String fileName )


{
if ( null != imageImp )
{
imageImp.store( fileName );
}
}

public Extent getExtent()


{
if ( null != imageImp )
{
return imageImp.getExtent();
}
else
{

585858585858585858585858585858
return localExtent;
}
}

public void draw()


{
// in acest punct suntem nevoiti sa
//construim un Image, daca
acesta nu //exista
if ( null == imageImp )
{
imageImp = new Image();
imageImp.load( fileName );
}
imageImp.draw();
}
}

8.6 Consideraţii finale asupra lumii şabloanelor


de proiectare
Mica excursie în lumea şabloanelor de proiectare s-a încheiat. Marea
excursie în lumea şabloanelor de proiectare poate începe, după lectura atentă a
Capitolului 8, acuzând doar problemele de aprovizionare 7, dacă ne este
îngăduit să glumim. Aprovizionare ritmică cu date despre un număr cât mai
mare de şabloane de proiectare, catalogate de mult sau descoperite recent de
către design-erii experimentaţi. Apetitul mecanic pentru şablaonele de
proiectare va fi completat, treptat de un apetit filozofic pentru înţelegerea şi
utilizarea şabloanelor de proiectare în proiectele curente.
Bibliografia cărţii oferă câreva porţi de intrare în lumea şabloanelor de
proiectare. Internet-ul este un creier uriaş pe care îl putem folosi inteligent
pentru a fi la curent cu toate noutăţile din domeniul Design Pattern.

7
Trupele există, moralul lor este ridicat, inamicul a fost identificat, strategiile de
confruntare sunt elaborate, totul de pinde de aprovizionarea trupelor cu mijloace logistice
adecvate.

59 59 59 59 59 59 59 59 59 59 59 59 59 59 59

S-ar putea să vă placă și

  • Algoritmi
    Algoritmi
    Document18 pagini
    Algoritmi
    Paula Conut
    Încă nu există evaluări
  • Modulul 4
    Modulul 4
    Document60 pagini
    Modulul 4
    Paula Conut
    Încă nu există evaluări
  • Algoritmi
    Algoritmi
    Document18 pagini
    Algoritmi
    Paula Conut
    Încă nu există evaluări
  • Cuprins
    Cuprins
    Document50 pagini
    Cuprins
    Paula Conut
    Încă nu există evaluări
  • IC 06 Arbori
    IC 06 Arbori
    Document55 pagini
    IC 06 Arbori
    Paula Conut
    Încă nu există evaluări
  • LRN 234
    LRN 234
    Document2 pagini
    LRN 234
    Paula Conut
    Încă nu există evaluări
  • S4Capitolul1 FmaScurta PDF
    S4Capitolul1 FmaScurta PDF
    Document30 pagini
    S4Capitolul1 FmaScurta PDF
    Paula Conut
    Încă nu există evaluări
  • Plan
    Plan
    Document3 pagini
    Plan
    Paula Conut
    Încă nu există evaluări
  • Caiet Seminar
    Caiet Seminar
    Document42 pagini
    Caiet Seminar
    Paula Conut
    100% (1)
  • Model 2
    Model 2
    Document1 pagină
    Model 2
    Paula Conut
    Încă nu există evaluări
  • Examen
    Examen
    Document2 pagini
    Examen
    Paula Conut
    Încă nu există evaluări
  • Laborator12 2018 Vectori
    Laborator12 2018 Vectori
    Document4 pagini
    Laborator12 2018 Vectori
    Paula Conut
    Încă nu există evaluări
  • Laborator3 2018 Matrici PDF
    Laborator3 2018 Matrici PDF
    Document3 pagini
    Laborator3 2018 Matrici PDF
    Paula Conut
    Încă nu există evaluări