Interfete. Programarea Bazata pe Interfete.
Interfete.
Tipuri. Ierarhia de Interfete. [Gamma95]
Fiecare
declaratie a unei functii membru pentru o clasa de obiecte specifica numele
functiei, obiectele pe care le primeste ca parametri, si tipului valorii
returnate de functie. Acestea formeaza impreuna signatura functiei.
Multimea
tuturor signaturilor functiilor ce pot fi apelate de catre clientii acelei
clase de obiecte formeaza interfata clasei. Interfata
unui obiect descrie setul complet de cereri (mesaje) care pot fi trimise
catre un obiect al acelei clase.
Un tip este un nume pentru o anumita interfata. Spunem ca un obiect are tipul "List" daca poate executa toate operatiile prevazute in interfata numita "List". Un obiect poate avea mai multe tipuri; iar mai multe obiecte total diferite pot apartine aceluiasi tip! O parte a interfetei unui obiect poate fi caracterizat printr-un anumit tip, in timp ce o alta parte a interfetei sale poate fi caracterizata printr-un alt tip. Doua obiecte care apartin acluiasi tip trebuie sa aiba in comun doar o parte a interfetei lor, si anume acea parte care caracterizeaza tipul caruia ii apartin.
Spunem ca un tip este subtipul altui tip daca interfata sa contine integral interfata supertipului sau. Astfel, putem spune ca un subtip mosteneste intefata supertipului sau, putandu-se defini prin urmare o ierarhizarea intre tipuri.
Mostenirea
Claselor vs. Mostenirea Interfetelor
Este
important sa facem distinctia intre clasa unui obiect si
tipul
sau. Clasa unui obiect defineste modul in care un
obiect este implementat. Clasa defineste starea interna si implementarea
operatiilor sale. In contrast cu aceasta, tipul unui obiect se refera
doar la interfata sa, adica setul de solicitari la care poate raspunde.
Desigur, exista o relatie stransa intre clasa si tip. Intrucat clasa defineste operatiile pe care un obiect le poate efectua, ea defineste automat si tipul obiectului. Atunci cand spunem ca un obiect este instanta unei clase, spunem implicit si ca obiectul are interfata definita de clasa a carui instanta este.
In acest context, este deasemenea important sa intelegem diferenta intre mostenirea claselor si mostenirea interfetelor. Prin relatia de mostenire intre clase implementarea unui obiect este definita in termenii implementarii altui obiect. Asadar mostenirea intre clase este un mecanism care asigura reutilizarea codului si a reprezentarii. In contrast cu mostenirea claselor, mostenirea interfetelor descrie cand poate fi folosit un obiect in locul altui obiect. Aceste doua concepte pot fi usor confundate deoarece multe limbaje, printre care si C++, nu fac o distinctie explicita intre ele. In C++, modalitatea standard de a mosteni o interfata este aceea de a mosteni public de la o clasa de baza virtuala , in timp ce mostenirea claselor se poate exprima cel mai bine prin mostenirea privata.
Legarea
Dinamica a Operatiilor. Polimorfism.
Interfetele
sunt fundamentale in sistemele orientate pe obiecte. Obiectele sunt cunoscute
in sistem doar prin interfetele lor. Nu exista nici o alta cale de a afla
ceva despre un obiect sau de a-i cere sa faca ceva decat prin intermediul
intefetei sale. Interfata unui obiect nu spune nimic despre implementarea
sa - diferitele obiecte pot astfel sa implementeze o aceeasi interfata
in mod diferit. Ceea ce inseamna ca doua obiecte cu interfete identice,
pot avea implementari total diferite!
Atunci cand se solicita o anumita operatie, modul in care aceasta va fi indeplinita depinde nu doar de operatia solicitata, ci si de obiectul care va primi solicitarea si o va executa apeland una din functiile sale membru. Acest lucru se datoreaza faptului ca pot exista mai multe obiecte care sa poata raspunde la acea solicitare. Cu alte cuvinte, prin operatia solicitata se specifica serviciul dorit, iar prin obiectul concret se alege o anumita implementare a respectivului serviciu. Asocierea dintre o operatie solicitata si obiectul care va pune la dispozitie implementare concreta a operatiei printr-una din functiile sale membru se numeste legare. Functie de momentul in care se face aceasta legare, exista doua tipuri de legare:
Programarea
Bazata pe Interfete
Mostenirea
claselor este in esenta doar un mecanism de extindere a functionalitatii
unei aplicatii prin reutilizarea functionalitatii din clasele de baza.
Aceasta permite definirea facila si rapida a unei noi clase de obiecte
in functie de o clasa existenta, obtinand o implementare extrem de ieftina,
prin faptul ca se majoritatea lucrurilor necesare se mostenesc de la clase
existente.
Pe langa reutilizarea implementarii mostenirea este si mecanismul prin care se pot defini familii de obiecte cu interfete identice (de obicei prin mostenirea de la o clasa abstracta). Atunci cand mostenirea este utilizata corespunzator toate clasele derivate dintr-o clasa abstracta vor avea o interfata comuna. Astfel, in mod normal, o subclasa va redefini sau va adauga operatii la interfata mostenita si nu va ascunde operatii definite in clasa parinte. In felul acesta, obiectele tuturor subclaselor vor putea raspunde atunci cand este solicitata o operatie pentru un obiect al clasei de baza (abstracta). Prin urmare, relatia de mostenire este unul din mecanismele fundamentale care contribuie la realizare polimorfismul.
Exista doua avantaje major ale manipularii obiectelor in termenii interfetei definite de o clasa abstracta:
Corolar:
Nu
declarati variabilele ca instante ale unor clase concrete particulare,
ci operati mereu prin intermediul interfetei definite de clasa abstracta!
Functii Virtuale
Polimorfismul
Partial.
In
lucrarea
trecuta am vazut ca un pointer la clasa de baza poate fi utilizat pentru
a adresa un obiect al oricarei clase derivate din ea. Prin intermediul
unui astfel de pointer se pote apela toate functiile-membru ale clasei
de baza comune pentru clasele derivate. Aceasta este o forma partiala
de polimorfism intrucat acelasi pointer poate fi folosit pentru
a manipula obiecte de diferite forme. Astfel se poate declara un pointer
care sa indice in mod valid spre orice obiect al unei clase aflata
intr-o relatie de mostenire (derivare) fata de clasa utilizata pentru declararea
pointerului. Utilizand acel pointer, toate aceste obiecte pot fi manipulate
uniform fara a se tine cont de clasa concreta de care apartine obiectul:
fiecare obiect va fi tratat in termenii tipului sau general (tipul
definit de clasa de baza a ierarhiei).
Acest tip de polimorfism - bazata exclusiv pe relatia de mostenire si pe conversia implicita a pointerilor spre clasa de baza - prezinta urmatoarele dezavantaje:
| class
Employee {
public: double salary() { return sal;} double computeRaise() { return 25;} ~Employee() {...} protected: Employee (double salary} { sal = salary;} //nu pot crea direct instante!! private: . . . double sal; }; class
Manager : public Employee {
class
SecurityGuard : public Employee {
main()
{
double johnRaise = john->computeRaise(); // 100
|
Polimorfismul
Complet.
Pentru
a putea vorbi de un polimorfism complet, care sa permita
manipularea omogena a tuturor claselor aflate intr-o relatie de mostenire,
trebuie sa fie indeplinite urmatoarele doua conditii:
Definirea
Functiilor Virtuale in C++
Pentru
a realiza polimorfismul complet in C++ trebuie doar sa declaram ca virtuale
acele
functii-membru din clasa de baza pentru care dorim sa se apeleze implementarea
din clasele derivate, atunci cand invocarea se produce printr-un pointer
la clasa de baza, dar care indica spre un obiect al unei clasei derivate.
Declararea unei functii virtuale se face, prefixand declaratia (nu definitia!!)
functiei prin cuvantul virtual.
Sa
vedem cum se va modifica exemplul prezentat anterior:
Observam
ca singura modificare pe care am operat-o este aceea de a declara
ca virtuale functiile-membru ale clasei Employee. Apelul functiei
computeRaise() se va executa acum corect:
| main()
{
Employee *george = john; . . .
double
georgeRaise = george->computeRaise();
// corect:returneaza 100,
george
= new SecurityGuard;
|
Clasa de baza in mod uzual va defini si o implementare pentru functiile virtuale care poate fi mostenita de catre o clasa derivata daca acesta din urma nu doreste sa o redefineasca. In exemplul de mai sus acesta este cazul functiei salary(). Desi nu este redefinita in nici una din clasele de baza, aceasta functie a fost declarata ca virtuala intrucat ea ar putea fi redefinita la un moment ulterior al dezovltarii aplicatiei, fie intr-o noua clasa derivata, fie chiar in una din cele doua clase derivate deja existente.
Ceea ce distinge functiile virtuale de functiile obisnuite este prioritatea pe care o are intotdeauna implementarea functiei din clasa derivata, chiar si atunci cand obiectul clasei derivate apare intr-un context in care este asteptat un obiect al clasei de baza (evident indicat prin pointer sau referinta).
Figura de mai jos ilustreaza modul de identificarea functiei invocate are loc la executie in functie de tipul obiectului indicat si nu functie de tipul pointerului care indica obiectul.

In
Spatele Scenei . . .
Atunci
cand declaram virtual
anumite functii membru ale unei clase de baza, compilatorul va genera un
cod
suplimentar pentru aceste functii pe baza caruia va putea fi selectata
la executie, dintre implementarile definite in clasele derivate, versiunea
corecta pentru acea functie, pe baza tipului obiectului. In acest scop,
oridecateori se creaza un obiect dintr-o clasa cu functii virtuale, compilatorul
va retine intr-un camp special clasa din care a fost instantiat
obiectul respectiv. Acest camp este utilizat doar de catre compilator si
limbajul nu are nici un mecanism prin care sa faca acest camp accesibil
programatorului. Pe baza acestui camp, se va putea selecta clasa concreta
a carei implementarea va fi utilizata pentru executia operatiei. respective.
Destructori Virtuali
Dintre
functiile declarate virtual in clasa Employee
una
anume este surprinzatoare la prima vedere: destructorul clasei! Acest lucru
pare neobisnuit intrucat in mod normal nu ne gandim la a apela un
destructor. La o privire mai atenta insa constatam ca declarea destructorului
ca virtual este importanta. Sa consideram fragmentul de cod de mai
jos:
| class
Employee {
. . . ~Employee(); . . . }; . . . Employee
*boss = new Manager;
|
Ce se intampla in ultima linie din exemplul de mai sus? La apelul operatorului delete acesta nu are de unde sa stie ca obiectul spre care indica este de fapt o instanta a clasei Manager si in plus nu exista nimic in clasa Employee care sa determine selectarea la executie a destructorului ce trebuie apelat. Ceea ce inseamna ca daca constructorul clasei Manager a alocat reurse care se intentiona a fi eliberate prin intermediul destructorului acestei clase, acestea nu vor fi eliberate, ramand alocate inutil ("garbage").
Daca
destructorul clasei de baza este declarat virtual, atunci destructorul
apelat va fi selectat la executie in functie de obiectul efectiv
indicat de pointer, exact ca si in cazul celorlalte functii virtuale. Astfel,
in exemplul de mai sus va fi invocat destructorul Manager::~Manager()
ceea
ce va produce eliberarea resurselor alocate prin constructorul acestei
clase derivate. Desigur, in cele din urma va fi apelat si destructorul
clasei Employee
in
conformitate cu regulile
de apel ale destructorilor pentru clase derivate.
Functii Virtuale Pure. Clase Abstracte.
Sa revenim din nou la varianta initiala a exemplului Employee; am declarat acolo constructorul in sectiune protected ceea ce face imposibila crearea de instante a clasei Employee . Instante pot fi create numai din clasele derivate. De fapt nu putem face nimic cu clasa abstracta ... ! De ce? Pentru ca nu putem implementa functia computeRaise() pentru anagajati in general; putem implementa aceasta functie doar pentru categorii concrete de angajati (vezi clasele Manager si SecurityGuard)
In
limbajul C++ functiile dintr-o clasa de baza
pentru care nu se poate defini - sau nu se doreste - nici o implementare
se numesc functii virtuale pure. In
primul rand remarcam ca aceste functii sunt virtuale, ceea
ce inseamna ca vom putea invoca diferitele lor implementari existente in
clasele derivate fara a trebui sa stim exact tipul obiectului sau implementarea
functiei. In al doilea rand, aceste functii sunt "pure" in
sensul in care nu se defineste pentru ele in acea clasa un corp, ci doar
o signatura pura. Intrucat nu putem implementa pentru astlfe de
functii un corp general, vom "lega" corpul functiei la pointerul
vid :
| class
Employee {
public: virtual double computeRaise() = 0 // functie virtuala pura double salary() { return sal;} Employee (double salary} { sal = salary;} virtual ~Employee() {...} . . . }; |
Acum va fi imposibil sa mai cream o instanta a clasei Telephone pentru simplul motiv ca, neexistand o implementare a metodei ring(), compilatorul nu va stii ce sa faca in cazul in care o astfel de metoda ar fi invocata printr-un obiect al acelei clase. O clasa care nu mai poate fi instantiata intrucat contine functii virtuale pure se numeste clasa de baza abstracta sau clasa partiala.O astfel de clasa este abstracta sau partiala in sensul in care ea defineste o anumita comportare fara a defini insa si implementarea acesteia. Ea impune claselor derivate sa implementeze aceste functii, specificand astfel care sunt metodele ce trebuie in mod obligatoriu redefinite.
O functie
virtuala pura, desi nu trebuie sa aiba un corp, poate avea
un corp. Acesta va putea fi invocat prin intermediul operatorului de specificare
a domeniului (::).
Mostenirea Multipla
Uneori un singur lant de mostenire nu este suficient; In unele cazuri o clasa are nevoie sa mosteneasca proprietati de la doua sau mai multe clase parinte. Daca putem stabili la compilare care sunt aceste clase de baza putem utiliza mostenirea multipla pentru a combina comportamentele mai multor clase de baza intr-o singura clasa derivata.
Exemplu
Un
exemplu uzual este utilizarea mostenirii multiple pentru crearea unui sistem
de ferestre terminal. Dorim ca aceste sistem de ferestre sa fie portabil
pe mai multe tipuri de terminale. De aceea din clasa Window
am derivat clasele CursesWindow
si XWindow
ce corespund la doua tehnologii de implementare a ferestrelor (Curses si
XWindow). Majoritatea membrilor clasei Window
sunt virtuali ceea ce inseamna ca implementarea lor concreta se realizeaza
in clasele derivate. Avantajul acestei abordari este acela ca la nivelul
aplicatiei putem lucra ca si cum obiecte manipulate ar fi de tip
Window, in timp ce ele vor corespunde in realitate clasei corespunzatoare
tehonologiei utilizate.
Cea
de-a treia clasa derivata (EditWindow)
este o specializarea a unei ferestre pentru utilizarea in cadrul unui editor
de texte. Dar ceea ce am dori de fapt ar fi o clasa care pe de o parte
sa aiba caracteristicile unei ferestre de editare - deci sa fie derivate
din EditWindow
- dar in acelasi timp sa fie specifica unei anumite tehnologii, de exemplu
Curses; prin urmare clasa ar trebui sa fie derivata si din clasa CursesWindow.
Acest lucru il putem realiza prin intermediul mostenirii multiple. Urmatoare
secventa de cod creaza astfel de clasa, si apoi creaza o instanta a acelei
clase:
| class
EditWnd_Curses : public EditWindow, public
CursesWindow {
}; EditWnd_Curses aNewWindow; |
Prezentam in figura de mai jos si o schita (simplificata) a ierarhiei de clase pentru exemplul acesta:

Noua clasa EditWnd_Curses mosteneste toate proprietatile claselor din care este derivata (direct sau indirect). Aceasta insa ridica urmatoarele doua probleme:
In
exemplul de mai sus, clasa derivata EditWnd_Curses
rezolva
ambiguitatea redefinind functia
scroll().
In implementarea functiei sunt apelate versiunile functiei scroll() definite
in clasele de baza, intr-o ordine impusa de logica problemei. Aceeasi situatie
este si in cazul functiei
clear().
Completam prin urmare definitia clasei EditWnd_Curses
dupa
cum urmeaza.
| class
EditWnd_Curses : public EditWindow, public CursesWindow {
public: void clear() {CursesWindow::clear(); } void scroll() { EditWindow::scroll(); CursesWindow::scroll(); } }; EditWnd_Curses aNewWindow; |
Mai pot aparea ambiguitati datorate datelor membru mostenite. Aceste ambiguitati vor fi eliminate prin referirea respectivelor variabile/obiecte nu direct, ci cu ajutorul operatorului de specificarea a domeniului (::) exact cum am procedat mai sus cu functiile membru.
Clase
de Baza Virtuale
Cea
de-a doua problema mentionata mai sus este legata de existenta unei clase
comune pe diferitele canale prin care mosteneste o clasa derivata prin
mostenire multipla. Aceasta tip de mostenire se numeste mostenire
in diamant. Se recomanda pe cat posibil
evitarea definirii unor astfel de relatii de mostenire.
Problema
critica in acest caz este mostenirea multipla a datelor din respectiva
clasa comuna. Aceasta problema poate fi rezolvata prin declararea ca virtuale
a claselor de baza:
| class
Window {
... }; class
EditWindow : public virtual Window
{
class
CursesWindow : public virtual Window
{
class
EditWnd_Curses : public EditWindow, public CursesWindow {
|
Prin aceasta declaratie, chiar daca clasa Window este mostenita de o clasa derivata prin mai multe canale de mostenire, datele membru vor aparea o singura data intr-o instanta a clasei EditWnd_Curses. Daca clasa Window nu ar fi mostenita virtual, datele sale s-ar regasi in clasa EditWnd_Curses de un numar de ori egal cu numarul de canale prin care este mostenita clasa.
Evitarea
Apelurilor Redundante ale Metodelor din CBV
In
cazul unei mosteniri in diamant mai apare o problema, redat de secventa
de mai jos:
| class
Window {
public: virtual void scroll() { ... } }; class
EditWindow : public virtual Window
{
class
CursesWindow : public virtual Window
{
class
EditWnd_Curses : public EditWindow, public CursesWindow {
|
Clasa Window ofera o implementare utila a functiei scroll(), care este utilizata in redefinirea acestei functii din clasele EditWindow si CursesWindow, iar implementarea functiei din clasa EditWnd_Curses, apeleaza ambele functii. Aceasta face ca Window::scroll()sa se execute de doua ori. Acesta este un apel redundant al unei functii dintr-o CBV.
Atentie: In unele cazuri un astfel de apel nu este doar o problema de redundanta (ceva ce se eceuta inutil), ci poate avea efecte negative, constituind o perfida greseala ce va afecta logica programului! Un motiv in plus sa ne ferim de mostenirea in diamant.
Solutia
acestei probleme este redata mai jos:
| class
Window {
public: virtual void scroll() { ... } }; class
EditWindow : public virtual Window {
class
CursesWindow : public virtual Window {
class
EditWnd_Curses : public EditWindow, public CursesWindow {
|
Probleme
1.
Sa se implementeze ierarhia de clase descrisa mai jos:
Initial
instantele clasei Set vor fi multimi vide sau multimi cu cate un singur
element (dat ca parametru constructorului). Completarea unei multimi cu
elemente se poate face in 2 variante:
-adaugand elementele pe rand, cu ajutorul functiei 'add'; aceasta functie
va fi redefinita pentru clasa Set pe baza functiei 'add' de la clasa Collection
(pentru Set trebuie sa se prevada un test care sa evite inserarea a 2 elemente
identice);
-utilizand operatia de reuniune in care unul din operanzi este multimea
initiala, iar celalalt operand - multimea formata din elementul de adaugat.
Se vor redefini functiile virtuale 'nameOf', 'printOn' si 'isEqual'.
Bibliografie
[Cop91]
J. Coplien - "Advanced C++ Programming Styles and Idioms",
Addison-Wesley, 1991
[Gam95]
E.Gamma, R.Helm, R.Johnson, J.Vlissides - Design Patterns, Addison-Wesley,
1995
L. Negrescu - "Limbajele C/C++ pentru Incepatori vol. II", Editura Microinformatica
Cluj, 1994