Documente Academic
Documente Profesional
Documente Cultură
STUDIUL ŞABLOANELOR DE
PROIECTARE UTILIZÂND
FORMALISMUL UML1
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.
Î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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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;
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.
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.
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).
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 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ă).
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 void onClick()
{
for( int index = 0;
index < listeners.size();
index++ )
{
listeners.elementAt( index ).
handleEvent( this );
}
}
202020202020202020202020202020
private OtherField field;
if ( eventSource == otherField )
{
// comportament aferent
// campului otherField
}
}
}
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:
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.
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:
if(customer.nationality== Nationality.AMERICAN
)
salesOrder = new SalesOrder();
else salesOrder=new CanadianSalesOrder();
salesOrder.processOrder( saleable );
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
+ calcTax()
Figura 145
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.
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:
262626262626262626262626262626
}
}
}
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()
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:
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
HeaderedTicket,
323232323232323232323232323232
FooteredTicket şi
HeaderedAndFooteredTicket.
-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
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
343434343434343434343434343434
ConcreteDecoratorA
ConcreteDecoratorB
ConcreteComponent
super.
Component
Decorator
component.
operation();
operation();
addedBehavior()
- component : Component
+ operation()
- addedBehavior()
+ operation()
+ operation()
Figura 150
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
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();
}
37 37 37 37 37 37 37 37 37 37 37 37 37 37 37
//super.print;
}
}
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
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:
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();
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
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:
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
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
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:
484848484848484848484848484848
public abstract void undisplay();
}
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
505050505050505050505050505050
Adaptee
Adapter
Target
specificRequest();
Client
+ specificRequest()
+ request()
Figura 159
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.
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.
53 53 53 53 53 53 53 53 53 53 53 53 53 53 53
Figura 160
Figura 161
545454545454545454545454545454
şi este nevoit să încarce de pe disc imaginea, şi să îi trimită acesteia mesajul de
Draw().
Figura 162
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
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:
57 57 57 57 57 57 57 57 57 57 57 57 57 57 57
}
}
585858585858585858585858585858
return localExtent;
}
}
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