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:
Aveți grijă să faceți clic dreapta pe numele proiectului, nu pe soluție. Apoi, în fereastra de dialog care apare, selectați Interface:
Scrieț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:
1 2 3 4 5 6 7 8 |
namespace BunaLume { public interface IMasina { bool Start(); bool Stop(); } } |
Acum, să creăm o clasă numită Automobil, la care vom implementa interfața IMasina:
1 2 3 4 5 6 7 |
namespace BunaLume { public class Automobil : 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:
Dacă faceți clic pe „Implement Interface”, veți primi brusc aceste coduri:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace BunaLume { public class Automobil : IMasina { public bool Start() { throw new System.NotImplementedException(); } public bool Stop() { throw new System.NotImplementedException(); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System; namespace BunaLume { public class Automobil : IMasina { public bool Start() { Console.WriteLine("Automobil pornit"); return true; } public bool Stop() { Console.WriteLine("Automobil oprit"); return true; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
using System; namespace BunaLume { public class Automobil : IMasina { public bool Start() { Console.WriteLine("Automobil pornit"); return true; } public bool Stop() { Console.WriteLine("Automobil oprit"); return true; } } public class MasinaDeTunsIarba : IMasina { public bool Start() { Console.WriteLine("Masina de tuns iarba pornita"); return true; } public bool Stop() { Console.WriteLine("Masina de tuns iarba oprita"); return true; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
using System; namespace BunaLume { public class Automobil : IMasina { public bool Start() { Console.WriteLine("Automobil pornit"); return true; } public bool Stop() { Console.WriteLine("Automobil oprit"); return true; } } public class MasinaDeTunsIarba : IMasina { public bool Start() { Console.WriteLine("Masina de tuns iarba pornita"); return true; } public bool Stop() { Console.WriteLine("Masina de tuns iarba oprita"); return true; } } public class Program { public static void Main() { Automobil automobil = new Automobil(); MasinaDeTunsIarba masinaDeTunsIarba = new MasinaDeTunsIarba(); automobil.Start(); automobil.Stop(); masinaDeTunsIarba.Start(); masinaDeTunsIarba.Stop(); Console.Read(); } } } |
Acum, 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
using System; namespace BunaLume { public class Automobil : IMasina { public bool Start() { Console.WriteLine("Automobil pornit"); return true; } public bool Stop() { Console.WriteLine("Automobil oprit"); return true; } } public class MasinaDeTunsIarba : IMasina { public bool Start() { Console.WriteLine("Masina de tuns iarba pornita"); return true; } public bool Stop() { Console.WriteLine("Masina de tuns iarba oprita"); return true; } } public class Program { public static void Main() { Automobil automobil = new Automobil(); MasinaDeTunsIarba masinaDeTunsIarba = new MasinaDeTunsIarba(); automobil.Start(); automobil.Stop(); masinaDeTunsIarba.Start(); masinaDeTunsIarba.Stop(); Console.Read(); } public static void Start(Automobil automobil) { automobil.Start(); } public static void Start(MasinaDeTunsIarba masinaDeTunsIarba) { masinaDeTunsIarba.Start(); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
using System; namespace BunaLume { public class Automobil : IMasina { public bool Start() { Console.WriteLine("Automobil pornit"); return true; } public bool Stop() { Console.WriteLine("Automobil oprit"); return true; } } public class MasinaDeTunsIarba : IMasina { public bool Start() { Console.WriteLine("Masina de tuns iarba pornita"); return true; } public bool Stop() { Console.WriteLine("Masina de tuns iarba oprita"); return true; } } public class Program { public static void Main() { Automobil automobil = new Automobil(); MasinaDeTunsIarba masinaDeTunsIarba = new MasinaDeTunsIarba(); automobil.Start(); automobil.Stop(); masinaDeTunsIarba.Start(); masinaDeTunsIarba.Stop(); Console.Read(); } public static void Start(IMasina masina) { masina.Start(); } } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
using System; namespace BunaLume { public class Automobil : IMasina { public bool Start() { Console.WriteLine("Automobil pornit"); return true; } public bool Stop() { Console.WriteLine("Automobil oprit"); return true; } } public class MasinaDeTunsIarba : IMasina { public bool Start() { Console.WriteLine("Masina de tuns iarba pornita"); return true; } public bool Stop() { Console.WriteLine("Masina de tuns iarba oprita"); return true; } } public class Program { public static void Main() { Automobil automobil = new Automobil(); MasinaDeTunsIarba masinaDeTunsIarba = new MasinaDeTunsIarba(); Start(automobil); Start(masinaDeTunsIarba); Console.Read(); } public static void Start(IMasina masina) { masina.Start(); } } } |
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:
1 2 |
IMasina automobil = new Automobil(); IMasina masinaDeTunsIarba = new MasinaDeTunsIarba(); |
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.