Thursday, December 03, 2020 07:36

Cuprins >> Programarea Orientată Pe Obiecte > Interfețe

Interfețe

Interfețele, la fel ca și pointerii din C/C++, sunt unul dintre acele subiecte de care programatorii începători și chiar și cei intermediari se tem, pentru că nu le înțeleg. De fapt, adevărul este că sunt simple de înțeles, iar adevărata dificultate vine atunci când pun întrebarea „de ce să le folosesc/unde trebuie să le folosesc?”. În această lecție, sper să clarific aceste ambiguități.

Deci, să începem cu partea grea, definiția academică, conform MSDN: O interfață conține definiții pentru un grup de funcționalități înrudite pe care trebuie să le implementeze o clasă non-abstractă sau o structură.

Nu prea avem ce înțelege de aici. Acum, să explicăm același lucru, în cuvinte simple. Să presupunem că avem trei mașini: o mașină de tuns iarba, un aspirator și un cuptor cu microunde. Ce au aceste mașini în comun? Aparent nimic. Cu toate că, la o inspecție mai atentă, putem afirma că toate au în comun două butoane: unul de Start și unul de Stop. Și asta este tot ce trebuie să știți pentru a înțelege interfețele. Trei obiecte diferite, cu trei moduri diferite de a efectua unele acțiuni, dar toate cu două butoane similare, pe care le folosim pentru a efectua acele acțiuni (pornire și oprire). Când apăsăm butonul Start, nu ne interesează cu adevărat ce obiect este cel care le implementează și nici modul în care obiectul îndeplinește acțiunea de pornire. Știm doar că ne oferă o interfață pentru pornire, interfață care ne garantează că obiectul poate fi pornit.

Numai din acest motiv, putem concluziona prima proprietate a interfețelor C#: acestea oferă consistență. Dacă avem o interfață care declară un comportament Start și Stop, ORICE lucru care implementează această interfață este GARANTAT să ne ofere posibilitatea de a porni și opri, folosind același concept comun.

A doua proprietate a interfețelor este aceea că ele reprezintă un contract. În lumea reală, când cumpărați o casă, semnați un contract prin care sunteți de acord să plătiți o taxă lunară până la acoperirea costurilor. În C#, o interfață este exact ca un contract care în principiu spune „Sunt de acord să pun în aplicare orice metodă această interfață declară ” (în cazul nostru, metodele Start() și Stop()).

În al treilea rând, interfețele sunt foarte, foarte simple. Ele nu oferă deloc implementări. De fapt, interfețele nu fac nimic, cu excepția furnizării de tipuri de date și nume.

În al patrulea rând, la fel ca și clasele abstracte, interfețele nu pot fi instanțiate. De fapt, interfețele, prin definiție, sunt și ele abstracte.

Prin convenție, numele interfețelor încep cu majuscula I (I de la Interfață), urmate de un substantiv sau adjectiv.

Să creăm un exemplu concret de interfață. Se recomandă întotdeauna plasarea unei interfețe într-un fișier separat. Deci, să facem asta. În Visual Studio, faceți clic dreapta pe numele proiectului din tab-ul Solution Explorer și alegeți Add – New Item din meniul care apare:

Adăugarea unui nou element in Visual StudioAveți grijă să faceți clic dreapta pe numele proiectului, nu pe soluție. Apoi, în fereastra de dialog care apare, selectați Interface:

Adăugarea unei interfețe noi în Visual StudioScrieți un nume semnificativ. O voi numi pe a mea IMasina. De asemenea, voi defini codurile care îmi vor permite să implementez comportamentul Start și Stop:

Și asta este totul. Probabil vă uitați la ea și vă spuneți „bine, dar nu face nimic…” și ați avea dreptate. Interfețele nu fac nimic. Acestea definesc pur și simplu diferiți membri, în acest caz, Start() și Stop(), și ceea ce returnează (un bool), dar nu implementează nicio funcționalitate.

Acum, să creăm o clasă numită Automobil, la care vom implementa interfața IMasina:

Veți observa rapid că implementăm o interfață în același mod în care efectuăm moștenirea, folosind operatorul :. Deși, ar trebui să vă amintiți, atunci când este utilizat cu o clasă, se numește moștenire, atunci când este utilizat cu o interfață, se numește implementare. Deci, obiectul nostru Automobil implementează acum interfața IMasina. Doar că nu o face, cel puțin în această etapă. Veți observa că veți primi de fapt erori în Visual Studio: Eroarea CS0535 „Automobil” nu implementează membrul interfeței „IMasina.Start()” (Error CS0535 ‘Automobil’ does not implement interface member ‘IMasina.Start()’). Și această explicație ar trebui să fie deja evidentă, deoarece am spus deja că o interfață este un contract prin care acceptăm să implementăm orice funcționalitate declarată în interfața respectivă. Compilatorul nu face altceva decât să se plângă de faptul că încălcăm obligațiile noastre contractuale. Am fost de acord să punem în aplicare această funcționalitate și nu am făcut-o! Prin urmare, trebuie să facem acest lucru. O modalitate de a automatiza acest lucru este plasarea cursorului mouse-ului peste numelui interfeței, subliniată cu o linie roșie care indică eroarea, și efectuarea de clic pe butonul mic care apare:

Implementarea automatizată a unei interfețe în Visual StudioDacă faceți clic pe „Implement Interface”, veți primi brusc aceste coduri:

Acum compilatorul este destul de fericit, ne-am îndeplinit acordul contractual cu interfața IMasina și am implementat toate comportamentele sale. Acum, în cadrul clasei Automobil, putem începe să furnizăm funcționalitatea reală care se întâmplă atunci când o pornim sau o oprim:
În acest moment, veți putea compila și rula programul fără erori, dar probabil vă veți scărpina capul și vă veți spune „mmmmda, dar nu prea sunt sigur cu ce ne avantajează asta…” și veți avea dreptate, parțial. Am fi putut implementa metodele Start() și Stop() direct, fără a fi necesar să creăm un fișier suplimentar pentru interfață. Dar aveți răbdare, curând vă voi explica de ce am făcut-o. Deocamdată, să adăugăm o altă clasă numită MasinaDeTunsIarba, care implementează și ea IMasina:
Acum, în cadrul metodei Main(), haideți să instanțiem ambele obiecte și să apelăm metodele pe care le-au implementat:
Veți vedea următoarea fereastră:

Implementarea comportamentului interfețelorAcum, să presupunem că am avea o mulțime de automobile și o mulțime de mașini de tuns iarbă de pornit și oprit. Curând, va deveni obositor să apelăm metoda Start() asupra tuturor. În schimb, putem utiliza supraîncărcarea metodelor, adăugând o metodă de pornire care acceptă un parametru Automobil și o metodă de pornire care acceptă o MasinaDeTunsIarba:

Cu siguranță acest comportament este mai bun, dar încă nu este minunat. Și aici devin utile interfețele. În loc să creez două metode Start(), fiecare acceptând ca parametru un tip de mașină diferit, pot declara o singură metodă Start() care acceptă un parametru de tip IMasina!
Și știu că mulți dintre voi se vor uita de fapt la codul de mai sus și vor spune: „bine, dacă metoda Start() acceptă un parametru de tip IMasina și din moment ce interfețele nu pot fi instanțate, acest cod este total inutil. Nu pot crea o nouă instanță IMasina, pentru a o trece ca parametru când apelez metoda Start() „. Exact același lucru am gândit și eu când am văzut codul de mai sus prima dată, cu mulți ani în urmă. Și momentul meu „Evrika!” a venit în clipa în care mi-am dat seama că metoda Start() nu solicită de fapt un parametru de tip IMasina, ci cere de fapt orice implementează IMasina. Știm că Automobil implementează IMasina și știm că MasinaDeTunsIarba implementează IMasina, fiind astfel candidate la a fi plasate ca parametri.

Acum, deoarece IMasina este un contract care afirmă că orice lucru care îl implementează este garantat să aibă o metodă Start(), compilatorul mă va lăsa să apelez metoda Start() asupra parametrului masina. Știe că parametrul furnizat trebuie să conțină o metodă Start(), altfel programul nu ar fi compilat, deoarece am încălca contractul nostru, așa cum am descris mai sus.

Și aici ar trebui să aveți și voi momentul „Evrika!”. Ce pornește această metodă? Poate să pornească ORICE implementează interfața IMasina! Dacă este vorba de o mașină de tuns iarba, un cuptor cu microunde sau chiar un OZN, dacă ele implementează IMasina, această metodă le poate porni. Nu o prea interesează ce pornește, știe doar că va primi parametri care au capacitatea de a fi porniți.

Să o vedem în acțiune:

Cu acest rezultat:

Și, dacă nu v-ați dat seama deja, acesta este și un comportament polimorf. Iar acest lucru ne permite de asemenea să ne asigurăm că aplicațiile noastre vor fi compatibile si rezistente la proba timpului. Dacă la un moment dat decidem că dorim să adăugăm și o clasă Avion, nu trebuie să modificăm metoda Start() sau să creăm o altă supraîncărcare. Trebuie doar să facem clasa Avion să implementeze și ea IMasina și am putea să o pornim folosind aceeași metodă existentă, fără a schimba o singură bucată de cod.

Putem observa acest comportament polimorf chiar mai detaliat, în acest exemplu:

Acum, automobil și masinaDeTunsIarba sunt ceea ce se numește o variabilă de tip interfață, iar acum acceptă să fie inițializate cu orice lucru care implementează IMasina.

Deci, să revizuim câteva aspecte cheie:

  • interfețele sunt contracte, acestea garantează că orice le va implementa va implementa toată funcționalitatea declarată
  • o interfață declară doar proprietăți, metode, indexatori și evenimente. Este responsabilitatea clasei care o implementeaza să definească ceea ce fac aceste metode în mod exact.
  • membrii interfeței au modificatorul de acces public în mod implicit. De fapt, nici nu avem voie să definim un modificator de acces. Membrii interfeței definesc doar tipuri de returnare și nume.
  • dacă aveți două interfețe, ambele cu o metodă numită Start(), puteți utiliza ceea ce se numește implementare explicită, în care folosiți numele interfeței ca prefix, urmat de un punct și de numele metodei. Exemplu: public bool IMasina.Start()
  • spre deosebire de moștenire, clasele pot implementa mai multe interfețe
  • interfețele pot moșteni ele însele alte interfețe. Clasa care implementează interfața este de asemenea obligată să implementeze toți membrii interfețelor moștenite.

Ca ultim punct, să diferențiem interfețele de clasele abstracte. Scopul unei clase abstracte este de a defini un comportament comun care poate fi moștenit de mai multe clase, fără a implementa întreaga clasă. Interfețele forțează clasa care le pune în aplicare să implementeze toți membrii. Interfețele sunt ca niște scheletele. Dacă doriți să construiți un om, ar trebui să folosiți acel schelet. Clasele abstracte sunt ca niște schelete, dar și cu niște carne pe ele. Sunt acolo pentru a vă facilita munca. Puteți considera o clasă abstractă ca o interfață care are deja o anumită implementare.

Pentru a încheia această lecție, este posibil să auziți la un moment dat în cariera voastră de programatori despre conceptul de „programare către o interfață” (programming to an interface). Subiectul este destul de greu de explicat în întregime, dar conceptul de bază este deja descris în această lecție: în loc să creăm o metodă care acceptă o clasă ca parametru, fie că este un Automobil sau MasinaDeTunsIarba, am creat o metodă care a acceptat o interfață și astfel, am făcut codul mai modular, decuplat, generic. Pentru a citi mai multe despre „programarea către o interfață”, citiți aceste răspunsuri superbe pe StackOverflow.

Cam atât despre interfețe. Sper că acum aveți cunoștințe solide nu numai despre ceea ce sunt, ci și despre când și de ce ar trebui să le utilizați.

Comments

comments

Tags: , ,

Leave a Reply



Do NOT follow this link or you will be banned from the site!