Biologie | Chimie | Didactica | Fizica | Geografie | Informatica | |
Istorie | Literatura | Matematica | Psihologie |
Principii de proiectare si sabloane de proiectare
Ce este arhitectura soft (sau cum ar trebui sa fie)? Raspunsul este: pe mai multe nivele (straturi). Pe primul nivel sunt dispuse sabloanele de arhitectura, care definesc forma si structura generala a aplicatiilor soft [Shaw96]. Pe nivelul urmator este arhitectura specifica scopului (domeniului) aplicatiei soft. Al treilea nivel corespunde arhitecturii modulelor si interconexiunilor dintre acestea. Aici vorbim despre sabloane de proiectare [GOF96], pachete, componente si clase. Capitolul de fata se ocupa in mod special de acest nivel.
Domeniul discursului din acest capitol este limitat. Este mult mai mult de spus despre principiile si sabloanele prezentate. Cititorilor interesati li se recomanda [Martin99].
1. Architectura si dependente
Ce nu este la locul lui in soft? Proiectul multor aplicatii soft incepe ca o imagine vitala in mintile proiectantilor acesteia. In momentul initial, proiectul este curat, elegant si compelling (usor de inteles), avand o frumusete data de simplitate care-i face pe proiectanti si implementatori nerabdatori sa-l vada la lucru. Unele dintre aplicatiile soft isi mentin puritatea proiectului pe parcursul dezvoltarii initiale si in prima versiune operationala.
Pe urma incepe sa se petreaca ceva. Aplicatia incepe sa se degradeze. La inceput, lucrurile nu sunt asa de rele. O buba urata aici, o crapatura neindemanatica acolo, care insa nu ascund frumusetea generala a proiectului. Pe masura ce procesul de degradare avanseaza, partile urate se acumuleaza pana cand incep sa domine proiectul aplicatiei. Programul devine o masa de cod pe care programatorii il pot intretine din ce in ce mai greu. Efortul necesar chiar pentru o modificare minora devine asa de mare incat programatorii si managerii lor directi cer insistent reproiectarea.
Astfel de reproiectari rareori au succes. Cu toate ca proiectantii pornesc cu cele mai bune intentii, pe parcurs constata ca au de tras (ochit) o tinta in miscare. Sistemul vechi continua sa evolueze si sa se schimbe, iar noul proiect trebuie sa tina cont de toate modificarile facute in acesta. Astfel, noul proiect va acumula warts and ulcers chiar inainte ca acesta sa ajunga la prima versiune operationala. La acel moment, de obicei mult mai tarziu decat a fost planificat, acumularea de probleme in proiectul noi poate fi atat de grava, incat sa-i faca pe proiectanti sa ceara o noua reproiectare.
1.1. Simptomele unui proiect degradat (rotting design)
Sunt patru simptome primare care caracterizeaza un proiect putred, degradat. Ele nu sunt ortogonale, ci sunt interdependente in moduri ce vor deveni evidente mai tarziu. Ne referim la: rigiditate, fragilitate, imobilism si vascozitate.
Rigiditatea. Rigiditatea este tendinta softului de a fi greu de modificat, chiar si in situatii simple. Orice modificare intr-un modul M provoaca o cascada de modificari in modulele dependente de M. Ceea ce initial pare a fi o simpla modificare ce se poate realiza in doua zile si care implica un singur modul se transforma intr-un maraton de mai multe saptamani ce implica modul dupa modul, pe masura ce se urmareste firul implicatiilor modificarilor in aplicatie.
Cand softul are un astfel de comportament, managerilor le este frica sa lase programatorii sa repare erorile non-critice. Motivul este ca nu se poate prevedea cand o asemenea sarcina se termina. Daca li s-ar permite programatorilor sa se ocupe de erorile non-critice, acestia ar deveni indisponibili pentru perioade de timp lungi, iar proiectul incepe sa aiba caracteristicile unui motel in care vin clienti (programatori), dar nu pleaca nimeni.
Cand frica managerilor devine asa de puternica incat ei refuza sa permita modificarea softului, se instaleaza oficial rigiditatea. Prin aceasta, o deficienta de proiectare se transforma intr-o politica de management contrara intereselor firmei.
Fragilitatea. Fragilitatea este strans legata de rigiditate. Ea se defineste ca tendinta aplicatiei de a produce erori in mai multe locuri, la fiecare modificare a codului sursa. Adesea erorile apar in locuri care nu au nici o relatie conceptuala cu locul in care s-au facut modificarile. Astfel de situatii provoaca amprehensiune pentru manageri: ori de cate ori autorizeaza o modificare, ei se tem ca dupa aceasta aplicatia va genera erori neprevazute. Pe masura ce fragilitatea creste, probabilitatea aparitiei de erori creste si ea, tinzand asimptotic la 1. O astfel de aplicatie este imposibil de intretinut. Orice reparatie o face mai proasta, introducand alte erori care trebuie rezolvate. In acest stadiu, managerii si clientii au impresia ca programatorii au pierdut controlul asupra softului. Domneste neincrederea si credibilitatea personalului tehnic este pierduta.
[BP] Murphy:
Orice solutie genereaza noi probleme
Inainte era mai bine. In program erau doua erori care se compensau. Eliminand una, am dezvaluit-o pe cealalta.
[end BP]
Imobilitatea. Immobilitatea este imposibilitatea refolosirii softului din alte proiecte sau din parti ale aceluiasi proiect. Adesea, un programator descopera ca are nevoie de un modul M similar cu unul scris de alt programator. In multe astfel de situatii, modulul M deja scris depinde la randul sau de multe alte module, fapt ce ingreuneaza refolosirea lui. Dupa multa munca de adaptare, programatorii implicati descopera ca munca si riscurile necesare pentru separarea partilor dorite din modulul M deja scris de cele nenecesare sunt prea mari pentru a actiona astfel. Drept consecinta, noul modul M este rescris, in loc de a se utiliza modulul M deja existent.
Vascozitatea. Vascozitatea apare in doua forme: viscozitatea proiectului si vascozitatea mediului de dezvoltare. Cand trebuie facuta o modificare, exista mai multe modalitati de rezolvare. Unele dintre acestea conserva proiectul, pe cand altele (de exemplu hack-urile, trucurile) nu. Se spune ca vascozitatea proiectului este mare cand modalitatile de rezolvare care conserva proiectul (structura proiectului) sunt mai dificil de aplicat decat trucurile. Cu alte cuvinte, este mai usor sa faci prostii decat lucruri bune.
Vascozitatea mediului de dezvoltare apare cand acesta este lent si ineficient. De exemplu, daca timpii de compilare sunt foarte mari, programatorii vor fi tentati sa faca modificari care sa nu necesite recompilari masive, chiar daca respectivele modificari nu sunt optimale din punctul de vedere al proiectului (structurii proiectului). Daca sistemul de control al codului sursa are nevoie de mai multe ore pentru a verifica cateva fisiere, atunci programatorii vor fi tentati sa faca doar acele modificari care sa implice cat mai putine verificari, indiferent daca acestea conserva sau nu proiectul.
Aceste patru simptome sunt semne vizibile ale unei arhitecturi slabe. Orice aplicatie care le expune are un proiect ce se degradeaza din interior spre in afara. Urmatoarea intrebare este: de ce este provocata degradarea?
1.2. Cerinte intr-o continua schimbare
Cauza imediata a degradarii proiectelor este bine inteleasa. Cerintele se modifica in moduri neanticipate de proiectul initial. Pe urma modificarile trebuie efectuate rapid, si de regula sunt facute de programatori care nu sunt familiarizati cu filozofia proiectului initial. Procedandu-se astfel, se violeaza proiectul initial, cu toate ca modificarea acestuia este functionala. Pas cu pas, apar cerinte noi, iar implementarea lor face ca alterarea proiectului initial sa se accentueze, pana intr-un punct in care devine evidenta degradarea.
Desigur, aparitia de cerinte noi nu poate fi blamata pentru degradarea proiectelor. In calitate de informaticieni, suntem constienti de modificarea cerintelor. Multi dintre noi stiu ca documentul ce precizeaza cerintele este cel mai volatil document al proiectului. Daca proiectele noastre esueaza datorita avalansei constante de cerinte ce se modifica, vina este a proiectelor, nu a cerintelor sau a mediului care provoaca schimbarea lor. Trebuie gasita o modalitate prin care proiectele sa reziste modificarilor si sa nu intre in procesul de degradare.
1.3. Gestiunea dependentelor Dependency Management
Ce fel de modificari provoaca degradarea proiectelor? Raspunsul este: acele schimbari care introduc dependente noi sau neplanificate. Fiecare dintre cele patru simptome amintite mai sus este provocata direct sau indirect de dependentele improprii dintre modulele aplicatiei. Ceea ce se degradeaza este arhitectura dependentelor, si odata cu ea gradul de intretinere a softului respectiv.
Pentru a evita degradarea arhitecturii dependentelor, trebuie atent gestionate dependentele dintre modulele aplicatiei. Procesul de gestiune consta in crearea unor ziduri de protectie a dependentelor. In afara acestor ziduri, dependentele nu se propaga.
Proiectarea orientata pe obiecte (OOD, Object Oriented Design) abunda de principii si tehnici de construire a unor astfel de ziduri si de gestionare a dependentelor intre module. In cele ce urmeaza, acest capitol discuta principiile si tehnicile respective, incepand cu principiile si continuand cu tehnicile (sabloanele de proiectare), care ajuta la conservarea arhitecturii dependentelor aplicatiei.
2. Principiile OO ale proiectarii claselor Principles of Object Oriented Class Design
In cele ce urmeaza sunt discutate urmatoarele principii
The Open Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Dependency Inversion Principle (DSP)
The Interface Segregation Principle (ISP)
2.1. Principiul Deschis Inchis The Open Closed Principle (OCP) [OCP97]
Orice modul trebuie sa fie deschis la extindere si inchis la modificare.
A module should be open for extension but closed for modification.
Dintre toate principiile OOD, acesta este cel mai important. El isi are originea in cartea lui Bertrand Meyer [OOSC98] si se explica astfel: modulele pe care le scriem trebuie sa se poata extinde fara a fi modificate. Cu alte cuvinte, dorim sa putem modifica ceea ce fac modulele, fara a le modifica codul lor sursa.
Cerinta de mai sus pare contradictorie, cu toate ca exista mai multe tehnici de realizare a ei pe scara larga. Toate tehnicile mentionate se bazeaza pe abstractizare. Intr-adevar, abstractizarea este cheia realizarii OCP. Cateva dintre tehnici sunt descrise in cele ce urmeaza.
Polimorfismul dinamic. In listing-ul 2-1, functia LogOn trebuie modificata ori de cate ori apare un tip nou de modem. Mai rau, deoarece fiecare tip de modem depinde de enumerarea Modem::Type, fiecare structura modem trebuie recompilata ori de cate ori se adauga un tip nou de modem.
Listingul 2-1: Functia Logon trebuie modificata pentru a fi extinsa. |
struct Modem ; struct Hayes ; struct Courrier ; struct Ernie ; void LogOn(Modem& m, string& pno, string& user, string& pw) |
Desigur, nu acesta este cel mai slab atribut al acestui tip de proiect. Programele proiectate in aceasta maniera abunda in folosirea unor instructiuni if/else sau switch. Ori de cate ori se doreste ca modemul sa execute alta functionalitate, va fi nevoie de o alta instructiune if/else sau switch care sa selecteze functionalitatea dorita. Cand se adauga modemuri noi, sau cand politica modemurilor se modifica, intreg codul sursa ce contine instructiunile de selectie trebuie analizat, iar fiecare dintre ele trebuie modificata corespunzator.
Si mai rau, programatorii pot folosi optimizari locale (subprograme, de exemplu) care ascund structura instructiunilor de selectie. De exemplu, s-ar putea ca aceeasi functie sa corespunda atat pentru modemul Hayes, cat si pentru modemul Courier. In acest caz, codul ar putea arata astfel:
if (modem.type == Modem::ernie) SendErnie((Ernie&)modem, c); else SendHayes((Hayes&)modem, c); |
In mod evident, astfel de structuri fac sistemul mai greu de intretinut si sunt surse potentiale de erori.
Drept exemplu pentru OCP, sa consideram figura 2-13, in care functia LogOn depinde numai de interfata Modem. Modemurile adaugate ulterior nu vor provoca modificarea functiei LogOn. In acest fel am creat un modul care se poate extinde cu noi modemuri, fara a necesita modificarea. Vezi listingul 2-2.
Figura 2-13
Listingul 2-2: Functia LogOn a fost inchisa la modificare. |
class Modem ; void LogOn(Modem& m, string& pno, string& user, string& pw) |
Polimorfismul static. O alta tehnica care este conforma OCP este folosirea genericitatii. Listingul 2-3 arata cum se realizeaza acest lucru. Functia LogOn se poate extinde cu diverse alte tipuri de modemuri fara a fi necesara modificarea corpului sau.
Listingul 2-3: Functia LogOn este inchisa la modificare prin polimorfism static |
template <typename MODEM> void LogOn(MODEM& m, string& pno, string& user, string& pw) |
Obiectivele arhitecturale ale OCP. Prin folosirea acestor tehnici de conformare la OCP, se pot crea module extensibile fara a necesita modificarea codului sursa. Aceasta inseamna ca, daca ne gandim putin inainte de a scrie codul, putem sa adaugam caracteristici noi (comportament nou) la codul existent, fara a modifica codul existent, ci doar prin adaugarea de cod suplimentar. Acesta este un ideal care nu este usor de atins.
Chiar daca OCP nu este indeplinit in totalitate, ci doar partial, aceasta poate aduce imbunatatiri dramatice structurii proiectului (aplicatiei). Totdeauna este de preferat ca modificarile facute la un moment dat sa nu se propage in codul deja scris si testat, care deja functioneaza. Daca codul scris, testat si care functioneaza bine nu este modificat, sunt sanse mai mici ca el sa provoace erori.
2.2. Principiul substitutiei al lui [Barbara] Liskov The Liskov Substitution Principle (LSP) [LSP97]
Subclasele trebuie sa poata inlocui clasele lor de baza.
Subclasses should be substitutable for their base classes.
Acest principiu se datoreaza Barbarei Liskov [Liksov88] si este expus in cartea sa privitoare la abstractizarea datelor si teoria tipurilor. De asemenea, el deriva din conceptul de proiectare pe baza de contract (Design by Contract, DBC) dezvoltat de Bertrand Meyer [OOSC98]
Conceptul este ilustrat in figura 2-14 si afirma ca clasele derivate trebuie sa poata substitui clasele lor de baza. Cu alte cuvinte, un utilizator al clasei de baza trebuie sa continue sa functioneze normal si daca lui i se trimite o derivata a clasei de baza.
Figura
2-14: Schema LSP.
De exemplu, daca o functie numita User are un parametru de tipul Base, atunci, asa cum se arata in listing-ul 2-4, trebuie sa fie legala folosirea unui parametru actual de tipul Derived in apelul functiei User.
Listingul 2-4: Exemplu cu functia User si clasele Base si Derived |
void User(Base& b); Derived d; User(d); |
La prima vedere, lucrul este evident. O analiza mai atenta scoate in evidenta subtilitati care merita discutate. Exemplul uzual este cunoscut sub numele de dilema Circle/Ellipse.
Dilema Cerc/Elipsa (Circle/Ellipse Dilemma). Din liceu (geometrie analitica?) stim ca cercul este o forma degenerata (un caz particular) de elipsa. Toate cercurile sunt elipse ale caror focare coincid. Aceasta relatie is-a ne tenteaza sa modelam cercurile si elipsele folosind mostenirea, dupa cum este ilustrat in figura 2-15.
Figura
2-15:
Modelul
conceptual este verificat, desi sunt unele deficiente. O privire mai
atenta a declaratiei clasei Ellipse din figura 2-16 incepe sa le
dezvaluie. Sa observam ca Ellipse are trei membri date.
Primii doi sunt cele doua focare, iar al treilea este lungimea axei mari.
Figura
2-16: Declaratia clasei
Ellipse
Cu toate acestea, daca ignoram risipa de spatiu, clasa Circle se poate face sa se comporte corespunzator, prin re-scrierea metodei SetFoci in asa fel ca sa ne asiguram ca ambele focare au aceeasi valoare, cf listingului 2-5. Astfel, fiecare focar va actiona drept centru al cercului, iar axa mare va fi diametrul acestuia.
Listingul 2-5: Cum facem focarele sa coincida |
void Circle::SetFoci(const Point& a, const Point& b) |
Clientii ruineaza totul. In mod sigur, modelul creat este (auto)consistent.
O instanta a clasei Circle respecta toate regulile de comportament ale unui cerc. Daca se foloseste un obiect Circle, nu se pot viola regulile respective. La fel pentru obiectele Ellipse. Clasele formeaza un model consistent si dragut, char daca Circle are prea multe date membri.
In lumea reala, obiectele Circle si Ellipse nu traiesc singure in universul lor. Ele coabiteaza cu alte entitati si-si ofera interfetele altor entitati. Interfetele implica un contract. Chiar daca contractul nu este exprimat explicit, el exista inevitabil, in forma subinteleasa. De exemplu, clientii obiectelor Ellipse au tot dreptul sa considere corect urmatorul fragment de cod sursa:
void f(Ellipse& e) |
In exemplul de mai sus, functia f asteapta sa lucreze cu un obiect Ellipse. Prin urmare, ea asteapta sa poata seta focarele si axa mare, si apoi sa poata verifica daca acestea au fost setate corespunzator. Daca parametrul actual in apelul acestei functii este instanta a clasei Ellipse, totul este OK.
In schimb, daca parametrul de apel este instanta a clasei Circle, atunci comportamentul functiei f va fi necorespunzator. Intr-adevar, daca am explicita contractul clasei Ellipse, am putea vedea ca postconditia metodei SetFoci garanteaza ca valorile de intrare sunt copiate in variabilele membru si ca variabila semiaxa mare ramane nemodificata. In mod clar clasa Circle violeaza o parte a postconditiei, deoarece ignora a doua variabila a metodei SetFoci.
Proiectare pe baza de contract. Design by Contract. Reformuland LSP, putem spune ca, pentru a asigura substitutia, contractul clasei de baza trebuie sa fie respectat de clasa derivata. Deoarece clasa Circle nu respecta contractul implicit al clasei Ellipse, ea nu poate face obiectul substituirii si deci violeaza LSP.
Explicitarea contractelor este o directie de cercetare deschisa de Bertrand Meyer. El a inventat limbajul de programare Eiffel, in care contractele sunt precizate explicit pentru fiecare metoda si sunt verificate explicit la fiecare apel de metoda. Acei dintre noi care nu folosim limbajul Eiffel putem face aceleasi lucruri folosind asertiuni si comentarii.
Pentru a preciza contractul unei metode, trebuie sa specificam preconditia (ce predicat verifica parametrii de intrare - ce trebuie sa fie adevarat inainte de apelul metodei) si postconditia (ce predicat trebuie sa verifice parametrii de iesire la terminarea apelului metodei - ce garanteaza metoda ca este adevarat cand isi termina executia). Daca preconditia nu este adevarata, rezultatele metodei sunt nedefinite si in consecinta metoda n-ar trebui apelata. Daca o metoda nu-si indeplineste postconditia, ea nu trebuie sa redea controlul clientului sau.
Reformuland LSP inca odata, de data aceasta folosind notiunea de contract, putem spune ca o clasa derivata D poate sa inlocuiasca clasele sale de baza B daca pentru orice metoda M a sa:
Precondtiile metodei D::M nu sunt mai tari decat preconditiile metodei B::M.
Postcondtiile metodei D::M nu sunt mai slabe decat postconditiile metodei B::M.
Cu alte cuvinte, metodele din clasa derivata trebuie sa nu astepte mai mult si sa nu ofere mai putin decat metodele omonime din clasa de baza.
Repercursiuni ale violarii LSP. Din pacate, violarile LSP sunt greu de detectat, efectele fiind observate de regula cand este prea tarziu. In exemplul Circle/Ellipse, totul a fost OK pana cand un client (functia f) a observat de unul singur ca acel contract implicit a fost violat. Daca proiectul este folosit intensiv, costul repararii violarii LSP ar putea fi prea mare pentru a putea fi suportat. S-ar putea sa nu fie economic fezabil sa se revina la refacerea proiectului, urmata de reconstructia si retestarea tuturor clientilor existenti. Prin urmare, rezolvarea ar fi includerea intr-o instructiune de selectie a apelului functiei f, numai in cazul cand parametrul de apel este de tip Ellipse si nu de tip Circle. In listingul 2-6 se da o astfel de rezolvare.
Listingul 2-6: O rezolvare neeleganta a violarii LSP |
void f(Ellipse& e) else throw NotAnEllipse(e); |
O examinare atenta a listing-ului 2-6 scoate in evidenta o violare a OCP. Acum, ori de cate ori se creeaza o noua clasa D derivata din Ellipse, functia trebuie modificata pentru a preciza daca i se permite sau nu sa opereze cu un parametru actual de tipul D. Prin urmare, violarile LSP sunt violari latente ale OCP.
2.3. Principiul inversarii dependentei The Dependency Inversion Principle (DIP) [DIP97]
Sa depinzi de abstractiuni. Sa nu depinzi de lucruri concrete.
Depend upon abstractions. Do not depend upon concretions.
Daca OCP precizeaza scopul arhitecturii OO, DIP precizeaza mecanismul primordial. Inversarea dependentei este strategia de a depinde de interfete sau functii si clase abstracte, in loc de a depinde de functii si clase concrete. Acest principiu este forta mobilizatoare din spatele proiectarii componentelor COM, CORBA, EJB, etc.
Proiectele procedurale expun o clasa particulara de structura de dependenta. Dupa cum se observa in figura 2-17, aceasta structura pleaca de la varf si refera detaliile de dedesubt. Modulele de nivel inalt depind de modulele de nivele mai joase, care la randul lor depind de module de nivele si mai joase, samd.
Un pic de gandire ar trebui sa expuna slabiciunile acestei structuri de dependenta. Modulele de nivel inalt opereaza cu politicile de nivel inalt ale aplicatiei. Aceste politici dau putina importanta detaliilor care le implementeaza. Prin urmare, de ce aceste module de nivel inalt sa depinda direct de modulele de implementare?
O arhitectura orientata pe obiecte se bazeaza pe o structura de dependente foarte diferita, in care majoritatea dependentelor puncteaza spre abstractii. Mai mult, abstractiile nu sunt dependente de modulele care contin implementarea detaliata ci, din contra, modulele de implementare depind ele insele de abstractii. Prin urmare, dependentele dintre module s-au inversat, dupa cum rezulta din figura 2-18.
Dependenta de abstractiuni Depending upon Abstractions. Implicatiile acestui principiu sunt relativ simple:
Orice dependenta din proiect trebuie sa indice spre o interfata sau o clasa abstracta (Every dependency in the design should target an interface, or an abstract class).
Nici o dependenta nu trebuie sa indice spre o clasa concreta (No dependency should target a concrete class).
Figura 2-17: Structura de dependenta a arhitecturii procedurale
Figura 2-18: Structura de dependenta a arhitecturii orientate pe obiecte
Este clar ca astfel de restrictii sunt draconice, si ca exista circumstante pe care le vom explora in continuare. Insa, in masura in care este posibil, principiul trebuie respectat. Motivul este simplu: lucrurile concrete se modifica frecvent, pe cand elementele abstracte se modifica rareori. Mai mult, abstractiunile sunt puncte de conexiune, de articulare, ele reprezentand locurile in care proiectul se poate modifica sau extinde, fara ca ele insele sa fie modificate.
Modele precum COM intaresc acest principiu, cel putin intre componente. Singura parte vizibila a unei componente COM este interfata sa abstracta (interfetele sale abstracte). Prin urmare in COM exista o slaba deviere de la DIP.
Forte moderatoare Mitigating Forces. O motivatie ce sta la baza DIP este sa previna clientii de a depinde de module volatile. DIP pleaca de la ipoteza ca orice este concret este supus schimbarii, adica este volatil. Intr-adevar, in multe situatii se intampla asa, in special la inceputul dezvoltarii; exista insa si exceptii. De exemplu, biblioteca standard string.h din C este foarte concreta, dar nu este deloc volatila. Un client care depinde de ea intr-un mediu ce lucreaza cu string-uri ANSI nu este deloc in pericol.
In mod similar, daca ati folosit module concrete, dar nevolatile, a depinde de ele nu este asa de rau. Deoarece este putin probabil ca astfel de module sa fie modificate, in consecinta este putin probabil ca ele sa induca volatilitate in proiectul in care sunt folosite.
Desigur, trebuie procedat cu precautie. O dependenta de string.h poate deveni foarte urata cand cerintele proiectului se schimba si va forteaza sa treceti la caractere UNICODE. Non-volatilitatea nu este un inlocuitor pentru substitutabilitatea unei interfete abstracte.
Crearea obiectelor Object Creation. Una dintre situatiile frecvente unde proiectele depind de clase concrete se refera la crearea instantelor. Prin definitie, clasele abstracte nu au instante. Prin urmare, pentru a crea o instanta trebuie sa se recurga la o clasa concreta.
Crearea instantelor are loc in orice loc al arhitecturii proiectului. Deci se pare ca nu este loc de scapare si ca intreaga arhitectura va fi o aglomerare de dependente de clasele concrete. Din fericire, exista o solutie eleganta a acestei probleme, numita FABRICA ABSTRACTA - ABSTRACTFACTORY [GOF96] p?? -- un sablon de proiectare ce va fi examinat mai atent la sfarsitul acestui capitol.
2.4. Principiul separarii (factorizarii) interfetelor The Interface Segregation Principle (ISP) [ISP97]
Mai multe interfete specifice clientilor sunt mai bune decat o singura interfata generala.
Many client specific interfaces are better than one general purpose interface
ISP este un alt principiu ce fundamenteaza tehnologiile bazate pe componente precum COM. Fara ISP, componentele si clasele ar fi mai putin utile si portabile. Esenta principiului este simpla. Daca este nevoie de o clasa C ce are mai multi clienti diferiti, se creeaza interfete specifice pentru fiecare client si apoi se construieste clasa C prin mostenire multipla din acestea.
Figura 2-19 ilustreaza o clasa Service ce are mai multi clienti si o singura interfata, dedicata sa-i serveasca (multumeasca) pe toti.
Sa observam ca ori de cate ori se face o modificare in metodele pe care le foloseste ClientA, s-ar putea sa fie afectati si clientii ceilalti, adica ClientB si ClientC. Prin urmare, este poate necesar sa fie recompilati si reinstalati, ceea ce nu este de dorit.
Figura
2-19: Clasa 'grasa'
Service cu interfete integrate
O tehnica mai buna este ilustrata in figura 2-20. Metodele cerute de fiecare tip de client sunt grupate in interfete speciale, specifice tipului respectiv de client. Ele sunt pe urma mostenite (multiplu) de clasa Service, care le implementeaza.
In acest caz, daca apare nevoia schimbarii interfetei pentru ClientA, clientii ClientB si ClientC vor ramane neafectati. Prin urmare, nu este necesara recompilarea si reinstalarea lor.
Ce inseamna specific clientului? What does Client Specific Mean? ISP nu recomanda ca orice clasa care foloseste un serviciu sa aiba clasa sa speciala de interfata de la care serviciul trebuie sa mosteneasca. Daca s-ar intampla asa, serviciul ar depinde de fiecare client intr-un mod bizar si nesanatos. Prin urmare, clientii trebuie categorizati in functie de tipul lor, iar interfetele trebuie create doar pentru fiecare tip de client.
Daca doua sau mai multe tipuri de clienti au nevoie de aceeasi metoda, aceasta trebuie inclusa in toate interfatele tipurilor respective. Aceasta nu provoaca nici daune, nici confuzie pentru client.
Figura 2-20: Interfete separate
Modificarea interfetelor Changing Interfaces. La intretinerea aplicatiilor OO, interfetele claselor si componentelor existente se modifica deseori. Sunt situatii cand aceste modificari au un impact imens si forteaza recompilarea si reinstalarea unei parti insemnate a arhitecturii aplicatiei. Acest impact poate fi redus prin adaugarea de interfete noi la clasele existente, in locul modificarii interfetelor existente. Clientii interfetelor deja existente care doresc sa acceseze metodele din noile interfete pot interoga obiectele pentru a stabili daca ele implementeaza sau nu noile interfete, dupa cum rezulta din codul urmator.
void Client(Service* s) |
La fel ca si in cazul celorlalte principii, trebuie acordata atentie sa nu se cada in extrema cealalta. Spectrul unei clase cu sute de interfete diferite, unele separate dupa criteriul clienti, altele separate dupa criteriul evolutiv, este cel putin la fel de infricosator ca si spectrul unei clase cu o singura interfata si sute de metode.
3. Principiile arhitecturii pachetelor Principles of Package Architecture
Clasele sunt un mijloc necesar, dar insuficient, de organizare a proiectului. Este necesara granularitatea mai mare a pachetelor pentru a induce ordine. Intrebarea care se pune este urmatoarea: cum decidem in ce pachet se pune o anumita clasa? In cele ce urmeaza se prezinta trei principii, cunoscute colectiv sub numele Principii de coeziune a pachetelor, Package Cohesion Principles, in incercarea de a ajuta arhitectul software.
3.1. Principiul echivalentei realizarii (livrarii, versiunii) si reutilizarii The Release Reuse Equivalency Principle (REP) [Granularity97]
Granularitatea reutilizarii este granularitatea realizarii.
The granule of reuse is the granule of release.
Un element reutilizabil, fie el componenta, clasa sau grup de clase, nu se poate reutiliza pana cand el nu este gestionat de un sistem de livrare de un anume fel. Utilizatorii nu vor dori sa-l foloseasca daca ei sunt nevoiti sa-l aduca la zi ori de cate ori autorul il modifica. Prin urmare, chiar daca autorul a produs o noua versiune a elementului reutilizabil, el trebuie sa doreasca sa sprijine si sa intretina versiunile mai vechi folosite de clientii care manifesta o dorinta mai putin pronuntata de punere la zi. In consecinta, clientii vor refuza sa reutilizeze un element pana cand autorul acestuia promite ca va gestiona numerele de versiune si va intretine pentru o perioada de timp versiunile mai vechi.
Prin urmare, un criteriu de grupare a claselor in pachete este reutilizarea. Deoarece pachetele sunt unitati de realizare, ele sunt de asemenea si unitati de reutilizare. In concluzie, arhitectii procedeaza bine cand grupeaza clasele reutilizabile in pachete.
3.2. Principiul inchiderii comune The Common Closure Principle (CCP) [Granularity97]
Clasele care se modifica simultan se grupeaza impreuna.
Classes that change together, belong together.
Un proiect mare de dezvoltare se subdivide intr-o retea larga de pachete intercorelate. Munca de gestiune, testare si livrare a acestor pachete nu este una triviala. Cu cat sunt mai multe pachete modificate intr-o realizare data, cu atat este mai laborioasa munca de re-construire, testare si instalare a realizarii respective. Prin urmare, este de dorit minimizarea numarului de pachete care sunt modificate in orice ciclu de realizare dat al produsului.
Pentru a obtine acest deziderat, se vor grupa impreuna clasele despre care credem ca se vor modifica impreuna. A sti care clase se vor modifica impreuna necesita un anumit nivel de previziune, deoarece trebuie anticipate tipurile de modificari care se pot cere. Pe urma, daca grupam in acelasi pachete clasele care se modifica concomitent, atunci impactul pachetelor de la o realizare la alta va fi minimizat.
3.3. Principiul reutilizarii comune The Common Reuse Principle (CRP) [Granularity97]
Clasele care nu se reutilizeaza impreuna nu trebuie grupate impreuna.
Classes that aren't reused together should not be grouped together.
Dependenta fata de un pachet este dependenta fata de orice este in acel pachet. Daca pachetul se modifica, iar numarul sau de realizare este incrementat, toti clientii pachetului trebuie sa verifice daca functioneaza corect cu noul pachet - chiar daca nimic din ceea ce ei folosesc din pachet nu s-a modificat.
Situatii frecvente de tipul celei expuse mai sus sunt atunci cand producatorul sistemului de operare livreaza un nou (o noua versiune de) SO. Mai devreme sau mai tarziu suntem fortati sa trecem la noua versiune deoarece versiunea veche nu va fi sprijinita la infinit. Prin urmare, chiar daca noua versiune nu aduce nimic interesant pentru noi, suntem fortati sa facem efortul de aducere la zi si de revalidare.
Acelasi lucru se poate intampla cu pachetele daca un pachet contine clase care nu sunt folosie inpreuna. Modificarile dintr-o clasa pe care un anume client n-o foloseste vor produce o noua realizare a pachetului, si-l vor forta pe client sa faca efortul de aducere la zi si de revalidare.
3.4. Tensiuni intre principiile coeziunii pachetelor Tension between the Package Cohesion Principles
Principiile de mai sus se exclud reciproc, neputand fi satisfacute concomitent. Aceasta se datoreste faptului ca fiecare principiu este dedicat unui grup specific de oameni. De principiile REP si CRP beneficiaza reutilizatorii, pe cand CCP este dedicat celor ce se ocupa de intretinere. CCP cere ca pachetele sa fie cat mai cuprinzatoare (daca toate clasele ar apartine unui singur pachet, atunci la fiecare realizare se modifica un singur pachet). Pe de alta parte, CRP impune ca pachetele sa fie cat mai mici, sa contina un numar cat mai restrans de clase.
Din fericire, pachetele nu sunt batute in cuie. Natura lor intrinseca face ca ele sa se modifice pe parcursul dezvoltarii. In fazele timpurii ale proiectului, arhitectii pot stabili structura de pachete astfel incat sa domine CCP; in acest fel dezvoltarea si intretinerea sunt sprijinite. Mai tarziu, pe masura ce arhitectura se stabilizeaza, arhitectii ar putea refactoriza structura de pachete pentru a maximiza REP si CEP pentru reutilizatorii externi.
3.5. Principiile cuplarii pachetelor The Package Coupling Principles
Urmatoarele trei principii guverneaza relatiile dintre pachete. Aplicatiile tind sa devina mari retele de pachete interconectate. Regulile care guverneaza relatiile dintre pachete sunt de prima importanta in arhitectura orientata pe obiecte.
3.5.1. Principiul dependentelor aciclice The Acyclic Dependencies Principle (ADP) [Granularity97]
Dependentele dintre pachete nu trebuie sa formeze cicluri
The dependencies betwen packages must not form cycles.
Deoarece pachetele sunt granule de livrare, ele tind sa focalizeze forta de lucru. Programatorii vor lucra de regula, la un moment dat, la un singur pachet, nu la o duzina. Aceasta tendinta este amplificata de principiile de coeziune a pachetelor, deoarece acestea tind sa grupeze la un loc clasele inrudite. In consecinta, programatorii vor constata ca schimbarile afecteaza numai cateva pachete. Dupa ce modificarile necesare au fost facute, pachetele afectate se pot reintegra in restul proiectului.
Inaintea integrarii pachetelor modificate in proiect, trebuie facuta testarea modificarilor. Pentru aceasta, pachetele modificate se compileaza si se construiesc impreuna cu toate celelalte pachete de care depind. Se spera ca numarul acestora este mic.
Sa consideram figura 2-21. Cititorii versati vor constata ca arhitectura are cateva defecte de proiectare. DIP pare a fi abandonat, impreuna cu OCP. Interfata grafica cu utilizatorul GUI depinde direct de pachetul de comunicatii si, cel putin aparent, este responsabila pentru transportarea datelor la pachetul de analiza.
Figure
2-21: Retea aciclica
de pachete
Aceasta structura mai degraba urata va fi folosita pentru cateva (contra)exemple. Sa presupunem ca se cere o noua versiune a pachetului Protocol. Programatorii vor trebui sa o construiasca folosind ultima versiune a pachetului CommError, si apoi sa execute testele. Pachetul Protocol nu are alte dependente, deci nu este nevoie de alte pachete. Testarea si livrarea se pot face cu efort minim.
Apare un ciclu A Cycle Creeps In. Sa presupunem ca un programator care lucreaza la pachetul
CommError a decis ca are nevoie sa afiseze un mesaj pe ecran. Deoarece ecranul este controlat de pachetul GUI, se va trimite un mesaj la unul dintre obiectele expuse de GUI, care va afisa mesajul pe ecran. Prin aceasta, pachetul CommError devine dependent de pachetul GUI, ca in figura 2-22.
Figure 2-22: Aparitia unui ciclu.
Sa examinam acum ce se intampla cand programatorii care lucreaza la pachetul Protocol doresc sa livreze o noua realizare a acestuia. Ei trebuie sa-si construiasca bateria de teste cu pachetele CommError, GUI, Comm, ModemControl, Analysis si Database! Acesta este clar un dezastru! Efortul pe care ei trebuie sa-l depuna a crescut considerabil din cauza unei mici dependente care a scapat de sub control.
De aici rezulta concluzia ca trebuie ca sa existe o persoana care sa supravegheze cu regularitate structura de dependente a pachetelor si sa elimine ciclurile, ori de cate ori acestea apar. Daca ciclurile nu sunt eliminate, atunci dependentele tranzitive dintre module vor face in final ca orice modul sa depinda de toate celelalte.
Eliminarea unui ciclu Breaking a Cycle. Exista doua modalitati de eliminare a ciclurilor:
(1) crearea unui nou pachet
(2) folosirea DSP si ISP
Figura 2-23 ilustreaza cum se elimina un ciclu prin crearea unui nou pachet. Clasele de care are nevoie pachetul CommError sunt extrase din pachetul GUI si se pun intr-un nou pachet, numit
MessageManager. Ambele pachete modificate, GUI si CommError vor fi dependente de acest nou pachet.
Acest exemplu ilustreaza modul in care structura pachetelor tinde sa se alungeasca si sa se deplaseze, adica evolutia acesteia in timpul dezvoltarii. Apar pachete noi si/sau clasele migreaza din pachetele vechi in cele noi, pentru a permite eliminarea ciclurilor.
Figura 2-23: Eliminarea ciclurilor prin crearea unui nou pachet
Figura 2-24 ilustreaza a doua modalitate de eliminare a ciclurilor, folosind DSP si ISP. Ea prezinta situatia dinainte si de dupa aplicarea acestor principii. Initial, avem doua pachete legate printr-un ciclu: clasa A depinde de clasa X, iar clasa Y depinde de clasa B. Ciclul se elimina prin inversarea dependentei dintre Y si B: se adauga o noua interfata, BY, la B. Aceasta interfata are toate metodele lui B de care are nevoie Y. Y foloseste aceasta interfata, iar B o implementeaza.
Sa observam unde este plasata interfata BY: in pachetul ce contine clasa care o foloseste. Acesta este un sablon pe care o sa-l vedem repetat in aproape toate exemplele cu pachete. Interfetele sunt adesea incluse in pachetul care le foloseste, si nu in pachetul care le implementeaza.
Figura 2-24: Eliminarea ciclurilor folosind ISP si DSP
3.5.2. Principiul dependentelor stabile The Stable Dependencies Principle (SDP) [Stability97]
Dependenta trebuie sa fie in directia stabilitatii.
Depend in the direction of stability.
Desi acest principiu pare evident, el trebuie explicat in detaliu, deoarece termenul de stabilitate are sensuri multiple.
Stabilitatea Stability. Ce intelegem prin stabilitate in acest context? Daca punem o moneda pe o masa pe cant - in plan vertical - nu putem spune ca e stabila in acea pozitie, desi daca nu este atinsa sau daca masa nu se clatina. De aici rezulta ca stabilitatea nu este direct influentata de frecventa modificarilor. Moneda nu se modifica, dar nu putem spune ca e stabila.
Stabilitatea este legata de cantitatea de efort necesara pentru a face o modificare. Moneda nu este stabila deoarece ea necesita doar o mica atingere pentru a fi rasturnata. Pe de alta parte, masa este mult mai stabila deoarece trebuie depusa o mare cantitate de efort pentru a o clatina sau rasturna.
Cum se aplica aceste constatari la software? Exista mai multi factori care fac un pachet software greu de modificat: dimensiunea, complexitatea, claritatea, etc. Ii vom ignora si ne vom indrepta atentia spre altceva. O cale sigura de a face pachetul software P greu de modificat este sa avem multe alte pachete care depind de P. Daca pachetul P are o multime de dependente de intrare, el este foarte stabil deoarece orice modificare in el necesita un efort imens de reconciliere cu toate pachetele ce depind de el.
Figura
2-25: X este un pachet stabil
Figura 2-25 ne arata ca X este un pachet stabil. El are trei pachete ce depind de el, deci trei motive sa nu fie modificat. Spunem ca X este responsabil pentru aceste trei pachete dependente. Pe de alta parte, X nu depinde de nici un alt pachet, deci nu are nici o influenta (cerere) externa de modificare. Spunem ca X este independent.
Pe de alta parte, figura 2-26 ilustreaza un pachet foarte instabil. Pachetul Y nu are pachete ce depind de el; spunem ca Y este iresponsabil. In schimb, Y are trei pachete de care el depinde, deci cererile de modificare pot veni din trei surse diferite. Spunem ca Y este dependent.
Figure
2-26: Y este instabil.
Metrici de stabilitate Stability Metrics. Stabilitatea unui pachet se poate calculaa folosind trei metrici simple.
Ca Cuplarea aferenta: numarul de clase din afara pachetului care depind de clasele din interiorul acestuia (adica dependentele de intrare)
Ce Cuplarea eferenta: numarul de clase din afara pachetului de care depind clasele din interiorul acestuia (adica dependentele de iesire)
I Instabilitatea. . Are valori in intervalul [0,1].
Daca nu exista dependente de iesire, atunci I = 0 si pachetul este stabil. Daca nu exista dependente de intrare, atunci I = 1 si pachetul este instabil. Cu ajutorul acestui indicator, SDP se poate reformula astfel: Un pachet P trebuie sa depinda numai de pachetele a caror metrica I este mai mica decat a lui.
Discutie Rationale. Toate pachetele trebuie sa fie stabile? Unul dintre cele mai importante atribute ale softului bine proiectat este usurinta in modificare. Softul care este flexibil in prezenta cerintelor in continua schimbare se considera ca este bine proiectat. Insa acest soft, conform definitiei de mai sus, este instabil.
Intr-adevar, dorim cu mare ardoare ca anumite portiuni ale softului sa fie instabile. Dorim ca anumite module (pachete) sa fie usor de modificat, astfel ca, la receptia unor cerinte noi, proiectul sa poata raspunde cu usurinta la modificarile cerute.
Figura
2-27: Violarea SDP.
Figura 2-27 ilustreaza cum poate fi violat SDP. Pachetul Flexible este dorit a fi unul usor de modificat, deci vrem ca el sa fie instabil. La un moment dat, un programator ce lucreaza la modificarea pachetului numit Stable, introduce o dependenta de pachetul Flexible. Aceasta violeaza SDP deoarece metrica I pentru Stable este mult mai mica decat metrica I pentru pachetul Flexible. Ca rezultat, pachetul Flexible nu-si pastreaza flexibilitatea, usurinta de a fi modificat. Orice modificare in Flexible va forta testarea lui Stable impreuna cu toate pachetele ce depind de el.
3.5.3. Principiul abstractiilor stabile The Stable Abstractions Principle (SAP) [Stability97]
Pachetele stabile trebuie sa fie pachete abstracte.
Stable packages should be abstract packages.
Ne putem imagina structura pachetelor unei aplicatii ca o multime de pachete interconectate, in care pachetele instabile sunt pe nivelele superioare, iar cele stabile pe nivelele inferioare. In acest mod, toate dependentele sunt de sus in jos.
Pachetele de pe nivelele superioare sunt instabile si flexibile. In schimb, cele de pe nivelele inferioare sunt foarte dificil de modificat. Aceasta ne conduce la o dilema: dorim pachete dificil de modificat in proiectul nostru?
Evident, cu cat avem mai multe pachete greu de modificat in proiect, cu atat proiectul in ansamblu este greu de modificat. Din fericire, exista o idee care merita exploatata. Cele mai stabile pachete sunt in partea de jos a retelei de dependente, deci sunt foarte greu de modificat. Dar, in conformitate cu OCP, ele nu sunt greu de extins!
Daca pachetele stabile din partea inferioara a retelei sunt si foarte abstracte, atunci ele se pot extinde cu usurinta. Acesta este un fapt pozitiv.
In consecinta, SAP este doar o reformulare a DIP. El afirma ca pachetele cu cele mai multe dependente de intrare trebuie sa fie si cele mai abstracte. Dar cum masuram gradul de abstractizare?
Metrici de abstractizare The Abstractness Metrics. Se poate deduce o alta tripleta de metrici ce permit calcularea gradului de abstractizare a unui pachet.
Nc Numarul de clase din pachet.
Na Numarul de clase abstracte din pachet. O clasa este abstracta daca ea are cel putin o interfata pura si nu poate fi instantiata.
A Gradul de abstractizare. .
Metrica A are valori in intervalul [0,1], ca si metrica I. Daca A = 0, atunci pachetul nu contine clase abstracte. Daca A = 1, atunci pachetul contine numai clase abstracte.
Graficul lui A in functie de I The I vs A graph. Principiul SAP se poate reformula acum folosind metricile I si A: I trebuie sa creasca cand A descreste. Cu alte cuvinte, pachetele concrete trebuie sa fie instabile, iar pachetele abstracte trebuie sa fie stabile. Dependenta lui A de I este ilustrata in figura 2-28.
Figure
2-28: Dependenta lui A de I
Este evident ca pachetele trebuie sa apara intr-una din cele doua puncte din figura de mai sus. Cele de pe axa A sunt complet abstracte si foarte stabile. Cele de pe axa I sunt complet concrete si foarte instabile.
Ce se intampla cu situatiile intermediare, cum este cazul pachetului X din figura 2-29. Unde trebuie pus?
Figure
2-29: Unde punem pachetul X pe
graficul A in functie de I?
Putem sa determinam unde trebuie pus pachetul X prin analizarea locurilor unde nu-l vrem a fi pus. Coltul din dreapta sus al graficului AI corespunde zonei cu pachetele care sunt foarte abstracte si de care nu depinde nimeni. Aceasta zona este cea a inutilitatii. In mod sigur, X nu are loc aici. Pe de alta parte, coltul din stanga jos al graficului AI reprezinta zona cu pachetele foarte concrete si cu foarte multe dependente de intrare. Pachetele din aceasta zona reprezinta cazurile cele mai nefavorabile: deoarece elementele sunt concrete, ele nu se pot extinde in maniera in care se extind entitatile abstracte; pe de alta parte, modificarea lor va fi foarte costisitoare deoarece ele au o multime de dependente de intrare. Cu siguranta nu dorim ca X sa fie nici in aceasta zona.
Dorim sa-l plasam pe X la distanta maxima fata de fiecare din zonele discutate, ceea ce corespunde liniei marcata cu numele main sequence, secventa principala. Plasarea pachetului X pe aceasta linie inseamna ca X este abstract proportional cu dependentele sale de intrare si este concret proportional cu dependentele sale de iesire. Cu alte cuvinte, clasele din pachetul X respecta DIP.
Metrici de distanta Distance Metrics. Determinarea pozitiei pachet X se face cu ajutorul a doua metrici, bazate pe metricile I si A. Pe baza acestora, putem sa determinam distanta pozitiei pachetului X fata de secventa principala.
D Distanta. . Ia valori in intervalul [0,~0.707].
D Distanta normalizata. . Este mai convenabila decat D deoarece ia valori in intervalul [0,1]. D = 0 indica faptul ca pachetul este pe secventa principala, iar D ca pachetul este la distanta maxima de secventa principala.
Metricile de mai sus caracterizeaza ahitectura OO. Ele sunt imperfecte si nu este deloc indicat a recurge numai la ele pentru a caracteriza arhitectura. Desigur, ele ajuta la masurarea structurii de dependente a unei aplicatii.
4. Sabloane de arhitectura OO Patterns of Object Oriented Architecture
Daca incercam sa aplicam principiile descrise mai sus pentru a crea arhitecturi OO, vom constata ca vom ajunge repetat la aceleasi structuri. Aceste structuri de proiectare si arhitectura repetabile sunt cunoscute sub numele de sabloane de proiectare (design patterns) [GOF96].
Definitia esentiala a unui sablon de proiectare este: o solutie binecunoscuta si folosita pentru o problema (clasa de probleme). Sabloanele de proiectare nu sunt ceva nou. Din contra, ele sunt tehnici consacrate care si-au demonstrat utilitatea intr-o perioada de mai multi ani.
Unele dintre sabloanele de proiectare cele mai folosite sunt descrise in cele ce urmeaza. Ele se vor aplica in studiile de caz de la sfarsitul cartii. Trebuie precizat ca tematica legata de sabloanele de proiectare nu se poate epuiza intr-un capitol de carte. Cititorul interesat este incurajat sa citeasca [GOF96].
4.1. Abstract Server
Cand un client depinde direct de server, este violat DIP. Modificarile facute in server se vor propaga in client, iar clientul nu va fi capabil sa foloseasca alte servere similare cu acela pentru care a fost construit.
Situatia de mai sus se poate imbunatati prin inserarea unei interfete abstracte intre client si server, asa cum se vede in figura 2-30.
Figure 2-30: Abstract Server
Interfata abstracta devine un punct de conexiune, de articulare, in care proiectul devine flexibil. Prin el, diverse implementari ale serverului se pot lega de clientul neavizat.
4.2. Adapter
Cand inserarea unei interfete abstracte nu este posibila deoarece serverul este produs de o alta companie (third party ISV) sau are foarte multe dependente de intrare care-l fac greu de modificat, se poate folosi un ADAPTER pentru a lega interfata abstracta de server, ca in figura 2-31.
Figure
2-31: Adapter
Adaptorul este un obiect care implementeaza interfata abstracta prin delegare la server. Fiecare metoda a adaptorului face traducere si apoi deleaga.
4.3. Observer
In multe situatii, un element al proiectului (actor) trebuie sa efectueze o anumita actiune cand alt element (detector) descopera ca s-a produs un eveniment. Frecvent se doreste ca detectorul sa aiba cunostinta despre actor.
Sa consideram exemplul unui instrument de masura care afiseaza starea unui senzor. Dorim ca instrumentul sa afiseze noua valoare ori de cate ori senzorul modifica valoarea citita, fara ca senzorul sa aiba vreo informatie despre instrument.
Aceasta situatie se modeleaza cu sablonul OBSERVER, ca in figura 2-32. Clasa Sensor
este derivata dintr-o clasa numita Subject, iar clasa Meter deriva dintr-o interfata numita Observer. Un obiect Subject contine o lista de obiecte Observer. Lista este incarcata de catre metoda Register a clasei Subject. Pentru a fi instiintat de evenimentele care apar, obiectul Meter trebuie sa se inregistreze in obiectul Subject, clasa de baza a lui Sensor.
Figure 2-32: Structura sablonului Observer
Figura 2-33 descrie dinamica colaborarii. O entitate (client) da controlul obiectului Sensor, care stabileste ca valoarea citita difera de cea anterioara. Obiectul Sensor apeleaza metoda Notify a obiectului sau Subject. La randul sau, obiectul Subject parcurge toate obiectele Observer
inregistrate in el si apeleaza pentru
fiecare dintre ele metoda Update. Metoda Update cu receptorul obiectul Meter va
citi noua valoare din Sensor si o va afisa.
Figure 2-33
4.4. Bridge
Una dintre dificultatile de implementare a unei clase abstracte folosind mostenirea este aceea ca clasa derivata este prea strans cuplata cu clasa de baza. Aceasta poate crea necazuri cand alti clienti doresc sa foloseasca metodele clasei derivate fara a cara tot bagajul ierarhiei clasei de baza.
De exemplu, sa consideram o clasa sintetizator de sunete. Clasa de baza translateaza intrarea MIDI intr-o multime de apeluri de primitive EmitVoice, care sunt implementate de o clasa derivata.
Sa mai remarcam ca functia EmitVoice din clasa derivata este utila si in afara contextului clasei. Din pacate, ea este legata inexorabil de clasa MusicSynthesizer, mai exact de functia PlayMidi a acesteia. Nu avem nici o posibilitate sa dispunem de metoda PlayMidi fara a cara dupa ea si toata clasa de baza MusicSynthesizer. De asemenea, nu se pot crea alte implementari ale metodei PlayMidi care sa se poata folosi in functia EmitVoice. In rezumat, ierarhia este prea strans cuplata.
Figure
2-34: Ierarhie cuplata
prost
Sablonul BRIDGE rezolva aceasta problema prin crearea unei separari puternice intre interfata si implementare. Figura 2-35 ilustreaza modul in care aceasta functioneaza. Clasa MusicSynthesizer contine o functie abstracta PlayMidi care este implementata de MusicSynthesizer_I. Ea apeleaza functia EmitVoice implementata in MusicSynthesizer pentru a delega la interfata VoiceEmitter. Aceasta interfata este implementata de VoiceEmitter_I si emite sunetele necesare.
Acum este posibil ca functiile EmitVoice si PlayMidi sa fie implementate separat una de alta. Cele doua functii au fost decuplate The two functions have been decoupled. EmitVoice can be called without
bringing along all the MusicSynthesizer baggage, and PlayMidi can be
implemented any number of different ways, while still using the same EmitVoice
function.
Figure 2-35
Hierarchy decoupled with Bridge
4.5. Abstract Factory
The DIP strongly recommends that modules not depend upon concrete classes. However,
in order ot create an instance of a class, you must depend upon the concrete
class. ABSTRACTFACTORY is a pattern that allows that dependency upon the concrete
class to exist in one, and only one, place.
Figure
2-36
Abstract Factory
Figure 2-36 shows how this is accomplished for the Modem example. All the users
who wish to create modems use an interface called ModemFactory. A pointer to
this interface is held in a global variable named GtheFactory. The users call the
Make function passing in a string that uniquely defines the particular subclass of
Modem that they want. The Make function returns a pointer to a Modem interface.
The ModemFactory interface it implemented by ModemFactory_I. This class is
created by main, and a pointer to it is loaded into the GtheFactory global. Thus,
no module in the system knows about the concrete modem classes except for
ModemFactory_I, and no module knows about ModemFactory_I except for
main.
5. Conclusion
This chapter has introduced the concept of object oriented architecture and defined it
as the structure of classes and packages that keeps the software application flexible,
robust, reusable, and developable. The principles and patterns presented here support
such architectures, and have been proven over time to be powerful aids in software
architecture.
This has been an overview. There is much more to be said about the topic of OO
architecture than can be said in the few pages of this chapter, indeed by foreshortening
the topic so much, we have run the risk of doing the reader a disservice. It has
been said that a little knowledge is a dangerous thing, and this chapter has provided a
little knowledge. We strongly urge you to search out the books and papers in the citings
of this chapter to learn more.
Bibliography
[Shaw96]: Patterns of Software Architecture (???), Garlan and Shaw,
[GOF96]: Design Patterns. Elements of Reusable OO Architectures
[OOSC98]: Object-Oriented Software Construction, Bertrand Mayer..
[OCP97]: The Open Closed Principle, Robert C. Martin
[LSP97]: The Liskov Substitution Principle, Robert C. Martin
[DIP97]: The Dependency Inversion Principle, Robert C. Martin
[ISP97]: The Interface Segregation Principle, Robert C. Martin
[Granularity97]: Granularity, Robert C. Martin
[Stability97]: Stability, Robert C. Martin
[Liskov88]: Data Abstraction and Hierarchy
[Martin99]: Designing Object Oriented Applications using UML, 2d. ed., Robert C. Martin, Prentice Hall, 1999.
Copyright © 2024 - Toate drepturile rezervate