Tutorial Java - Notiuni introductive - Despre obiecte

Tutorial Java - Notiuni introductive - Despre obiecte

Postby smith » 08 May 2011, 21:39

Despre obiecte


Computerele nu sunt simple mașini, ci sunt unelte care ar trebui să amplifice gândirea umană (Steve Jobs zicea că sunt "biciclete pentru minte"). Aceste unelte încep să nu mai semene așa mult cu mașinării, ci să semene cu părți ale gândirii noastre și să reprezinte forme de exprimare asemănătoare, cum ar fi pictura, animația, sculptura, scrisul etc. Paradigma programării orientată pe obiecte (POO sau OOP) face parte și ea din această mișcare înspre a folosi computerul ca mediu expresiv.

Acest capitol va încerca să vă introducă în conceptele de bază ale programării orientate pe obiecte. Acest capitol va cuprinde unele teorii și principii de bază generale ale OOP. Limbajul, deocamdată, nu este important, ci doar conceptele. V-aș sfătui să citiți cu atenție aceast capitol.


Procesul de abstractizare


DEX wrote:ABSTRÁCT, -Ă, abstracți, -te, adj., s. n. 1. Adj. Care rezultă din separarea și generalizarea însușirilor caracteristice ale unui grup de obiecte sau de fenomene; care este considerat independent, detașat de obiecte, de fenomene sau de relațiile în care există în realitate.

Toate limbajele de programare oferă un nivel de abstractizare. Când încerci să rezolvi o problemă, ideal ar fi să nu trebuiască să gândești în termenii în care gândește calculatorul - abstractizarea unor concepte ajută în acest sens.

Limbajele de asamblare oferă o abstractizare ușoară a mașinii. Limbajele imperative (precum C sau Basic) au fost abstractizarea unor limbaje precum limbajele de asamblare. Deși au fost un progres real față de de limbajele de asamblarea, tot erai nevoit să gândești în termeni de-ai computerului în loc de termenii problemei pe care încerci să o rezolvi.

Din această cauză, efortul programatorului este mai mare, codul este mai greu de menținut/scris și, în același timp, este mai puțin expresiv și mai greu de citit. Alternativa modelării mașinăriei este de a modela problema care dorești să o rezolvi. Limbaje precum LISP, Prolog sau APL au încercat o astfel de abordare în diferite feluri. Ele poate sunt bune pentru a rezolva o serie de probleme, dar lucrurile devin mai ciudate când încercăm să rezolvăm alte tipuri de probleme, pentru care aceste limbaje n-au fost destinate.

Programarea orientată pe obiecte face un pas înainte înspre a crea unelte pentru programator astfel încât acesta să poată crea elemente și entități care să reprezinte părți din problemă - programatorul să gândească într-un mod cât mai natural. Reprezentarea acestor elemente este destul de generală încât nu limitează programatorul la a rezolva doar un set de probleme.

Vom da elementelor din problemă și reprezentarea/abstractizarea lor în cod numele de obiecte. Uneori vei avea nevoie de obiecte care nu apar în mod direct în problemă. Ideea este de a scrie cod în care creezi obiecte după cum ai nevoie, iar atunci când citești codul să citești cuvinte cheie care descriu problema. Acesta este un model de abstractizare mult mai flexibil și puternic decât ceea ce am avut înainte. Deci OOP îți dă posibilitatea de a descrie problema în cod sursă prin termeni cât mai apropiați de problemă. Obiectele vor semăna cu cele din realitate - toate au caracteristici, stări și comportamente.

Alan Kay a enumerat cinci caracteristici de bază ale limbajului Smalltalk (primul limbaj cu succes care acceptă paradigma de OOP și în urma căruia a fost creat și Java). Aceste caracteristici reprezintă o abordare orientată pur pe obiecte:

  • Totul este un obiect. Gândiți-vă la un obiect că ar reprezenta o variabilă mai specială. Stochează informații și puteți să îi "trimiteți mesaje/comenzi" să efectueze unele lucruri. În teorie ar trebui să puteți să luați orice element conceptual din problemă și să îl transformați în obiect în programul dumneavoastră.
  • Un program reprezintă o serie de obiecte care interacționează și își trimit mesaje ca să își spună ce să facă. Pentru a cere unui obiect ceva, îi "trimiteți" un mesaj obiectului respectiv. Altfel spus, vă gândiți la un mesaj ca fiind un apel către o funcție care aparține acelui obiect.
  • Fiecare obiect poate fi format din mai multe obiecte care, în final, vor ocupa o anumită cantitate de memorie. Deci puteți să creați obiecte ca fiind un pachet de alte obiecte. Astfel puteți construi complexitate într-un program, dar să o ascundeți în spatele simplității obiectelor.
  • Fiecare obiect este de un anumit TIP. Altfel spus, fiecare obiect este o instanță a unei CLASE (unde clasă este sinonim cu tip). O clasă reprezintă un fel de rețetă după care se crează un obiect.
  • Toate obiectele de același tip pot primi același mesaj. Această propoziție poate fi interpretată în mai multe moduri, după cum vom vedea mai târziu. Deoarece un obiect de tip CERC poate fi, de asemenea, de tip FORMĂ (pentru că toate cercurile sunt forme), obiectul de tip cerc are garanția că poate primi aceleași mesaje care le poate primi și un obiect de tip formă. Acest lucru înseamnă că puteți scrie cod care "vorbește" cu obiecte de tip formă, dar în același timp poate să se descurce și când vine vorba de orice obiect derivat din tipul formă (pătrate, cercuri, triunghiuri etc.). Acest concept de substituție este unul din cele mai puternice concepte din programarea orientată pe obiecte și se numește polimorfism.

Deci un obiect are un anumit tip, poate stoca informații, are funcții pentru a-i da comportament și poate fi deosebit de orice alt obiect prin faptul că ocupă un spațiu dedicat în memorie - are o adresă unică în memorie.

Un obiect are o interfață


Aristotel a fost probabil primul om care a început să studieze conceptul de tip. El vorbea despre "clase de pești" și "clase de păsări". Ideea că "toate obiectele sunt unice și fac parte dintr-o clasă de obiecte care au unele caracteristici și comportamente în comun" a fost folosită direct în primul limbaj obiect orientat, Simula-67, care folosea cuvântul cheie class și cu ajutorul căruia se pot crea noi tipuri de date/obiecte.

În paradigma programării orientate pe obiecte creăm tipuri noi de date/obiecte. Aproape toate limbajele de programare care acceptă această paradigmă, conțin cuvântul cheie "class" pentru a crea un nou tip. O clasă descrie unele caracteristici și funcționalități ale unui obiect, după cum am explicat mai sus. O clasă nu este altceva decât un tip nou de date. Și tipul Integer (din Pascal, de exemplu) are unele caracteristici și funcționalitați/comportamente. Practic, nu există o diferență între o clasă nouă și un tip de date built-in din limbaj.

Avantajul de a crea tipuri noi de date este că ne dă posibilitatea de a ne apropia codul mai mult de problema pe care încercăm să o rezolvăm și nu trebuie să fim forțați de limbaj să folosim doar tipuri de date built-in. Un astfel de limbaj, care suportă clase - crearea de tipuri noi de date - va accepta orice clasă nou creată și se va comporta cu ea ca și cu orice tip de date built-in.

Odată ce o clasă este definită, se pot crea o mulțime de obiecte de acel tip (suntem totuși constrânși de memoria disponibilă). După ce obiectele sunt create, ne putem folosi de ele ca și cum ne-am folosi de elementele problemei pentru a o rezolva. Cum ne folosim de un obiect astfel încât să ne fie de ajutor? Trebuie să fie o modalitate prin care să putem să facem o cerere unui obiect (cum ar fi să afișeze ceva pe ecran, să calculeze ceva etc.). Tipurile de cereri pe care le poate primi un obiect sunt definite de interfața acestuia. Tipul este acela care determină interfața. Iată o ilustrație simplă a unui bec:
Image

Observăm că clasa nouă se va numi Light. Interfața acestei clase va conține patru funcții - fiecare cu rolul ei și implementarea ei.
Să zicem că avem următoarea bucată de cod:
  1. Light beculMeu = new Light();
  2. beculMeu.on();

În acest cod creăm un obiect de tip Light. După semnul egal se află operatorul new și o funcție specială Light() care vom vedea că este constructor pentru clasa Light (constructorii au același nume cu numele clasei). Acest constructor alocă memorie și face inițializările necesare pentru obiectul în cauză.
Astfel, cerem creerea unui obiect de tip Light. După ce l-am creat, apelăm o funcție a obiectului (funcția on()). Putem accesa date, funcții și orice alt membru din interiorul obiectului cu ajutorul punctului. Sintaxa ar fi următoarea:

Codul este destul de intuitiv, deci probabil ați înțeles că intenția noastră a fost de a aprinde un bec (chiar dacă am ascuns implementarea propriu-zisă - este doar un exemplu fictiv).
Am spus mai sus că putem accesa orice membru al clasei. Acest lucru nu este tocmai adevărat. Pot exista membrii privați sau protejați pe care nu îi vom putea accesa - despre acest lucru vom vorbi în lecția despre specificatorii de acces.

Ascunderea implementării (encapsulation)


Ar fi bine să împărțim oamenii (programatorii) în două grupuri: creatorii de clase(cei care crează tipuri noi de date - cei care lucră la librării, api-uri și framwork-uri) și programatorii care se folosesc de clase (client programmers).

Scopul celor care fac parte din grupa client programmers este să adune o colecție de clase care îi vor ajuta să creeze rapid o aplicație. Scopul celor care sunt creatori de clase este să creeze o clasă care oferă funcții și facilități NECESARE celor din grupa client programmers și restul implementării/complexității să fie ascunsă - astfel încât client-programmer-ul să nu poată accesa orice, decât ceea ce este necesar. Acest lucru oferă un avantaj, deoarece creatorul de clase are puterea de a modifica implementarea clasei, fără să îl afecteze pe programatorul client în vreun fel.

Un exemplu:
Să presupunem că avem o clasă care ne poate afișa a N-a zecimală a numărului PI. Clasa se ve numi PiGenerator, iar funcția care ne va returna a N-a zecimală se va numi getNthDecimal(). Codul ar arăta cam așa:
  1. PiGenerator myPiGenerator = new PiGenerator();
  2. myPiGenerator.getNthDecimal(10); // vrem a 10-a zecimală

Acesta va fi codul client-programmer-ului. Să presupunem că cel care a creat inițial clasa se gândește să facă o optimizare masivă - pentru a returna mult mai repede această zecimală. Creatorul clasei a implementat în interiorul clasei PiGenerator o funcție generatePi() de care se folosește în interiorul clasei pentru a returna a N-a zecimală, dar care este ascunsă de client-programmer (nu poate fi acccesată). Creatorul va putea modifica implementarea funcției generatePi() făcând-o mult mai eficientă și rapidă. Când programatorul client își va face un "update" la noua versiune de clasă/librărie/framework, codul lui va rula, fiind și mai rapid, în același timp.

Deci dezvoltatorii de librării și pachete de clase, pot lucra "în culise" fără să afecteze codul client-programmer-ului obișnuit. Faptul că unele funcții din clasa PiGenerator sunt private (nu pot fi accesate) este un lucru bun pentru că nu permite oricui accesul în locuri în care n-ar trebui să se umble - acest lucru ne scutește de bug-uri, erori etc.

Refolosirea implementării (reusability)


Odată ce o clasă a fost creată și testată, ar trebui să fie o bucată de cod folositoare (ar fi ideal). Se pare că refolosirea codului este mai grea decât pare. Trebuie multă experiență și multă viziune în viitor încât să produci un design pentru un cod care poate fi refolosit. Odată ce ai un astfel de cod, el va fi refolosit foarte des. Refolosirea codului este și ea una din avantajele programării obiect orientate.

Cea mai simplă metodă de a refolosi o clasă este de a crea un obiect din aceaa clasă. O altă modalitate ar fi să folosești un obiect în interiorul unei clase. Astfel creăm un obiect membru în interiorul clasei respective. Acest proces se numește compunerea de clase (composition). Compunerea se referă la o relație "ARE-UN/O", la fel ca și în propoziția: "O mașină are un motor". Iată o diagramă:

Image

Această diagramă, la fel și cu cea de sus, reprezintă diagrame UML. În standardul UML, săgeata cu un romb umplut reprezintă compunerea. Compunerea vine cu un nivel ridicat de flexibilitate. De obicei, obiectele membre în alte clase sunt private/protejate. Astfel se asigură faptul că programatorul client nu va accesa lucruri care nu trebuie accesate. De asemenea, se pot schimba implementări ale obiectelor membre, fără a afecta sau a deranja codul client.

Moștenirea (inheritance)


Din cauză că moștenirea reprezintă un concept atât de important în programarea orientată pe obiecte, mulți vor abuza de acest concept, folosind moștenirea la tot pasul. Acest lucru va genera design foarte complicat. Odată ce aveți experiență va fi destul de ușor să decideți când să folosiți moștenirea.

Ideea de obiect este una foarte convenabilă. Ai posibilitatea de incapsula date și funcționalitate, deci poți reprezenta un element cât mai apropiat de realitate. Ar fi păcat, totuși, să trebuiască să scriem și să rescriem cod pentru clase care au funcționalitate asemănătoare. Ar fi mult mai plăcut dacă am putea să luăm codul deja scris și să îl folosim, să adăugăm sau să modificăm lucruri. Acest lucru se poate obține cu ajutorul moștenirii.

Astfel, vom avea o clasă de origine (clasă de bază, părinte sau superclass) și o clasă derivată(moștenitoare, copil sau subclass). Toate schimbările din clasa de bază se vor reflecta în clasele derivate. Un tip de date (sau o clasă) nu descrie doar caracteristicile unui obiect de acel tip. Un tip de date definește și unele relații cu alte tipuri de date. Două tipuri de date pot avea caracteristici și funcționalități în comun, dar unul din ele poate avea mai multe caracteristici decât celălalt sau poate avea mai multe funcționalități. Moștenirea exprimă aceste relații între tipuri. Astfel, un tip de bază va conține toate caracteristicile de bază și vor fi distribuite în rândul tipurilor care derivă din acest tip de bază.

Exemplu:
Să presupunem că avem un o mașină care sortează gunoi. Gunoiul este tipul de bază care are caracteristici: mărime, greutate etc. Fiecare tip derivat (ex: sticlă, conserve, etc.) va avea alte caracteristi sau funcționalități adiționale (ex: poate fi topit, are proprietăți magnetice etc.). Folosind moștenirea se poate crea o ierarhie care să semene cât mai mult cu problema pe care încercați să o rezolvați.

Exemplu:
Să presupunem că lucrăm la un soft de grafică și desen. Tipul de bază va fi "formă". Formele au mai multe caracteristici (mărime, culoare, poziție etc.) și funcționalități (pot fi afișate pe ecran, șterse, mutate, colorate etc.). Din acest tip de bază (formă) pot fi derivate alte tipuri: cerc, pătrat, triunghi, dreptunghi etc. - fiecare având caracteristici și funcționalități în plus. Unele funcționalități pot fi diferite: calcularea ariei în cazul diferitelor forme se face diferit.
Iată un exemplu de ierarhie:

Image

Când derivezi un tip de date existent, creezi un tip nou. Acest tip nou conține toți membrii tipului de bază (înafara celor privați), inclusiv interfața - poți trimite mesaje tipului derivat ca și cum ai trimite același mesaj tipului de bază. Am putea spune că tipul derivat este de același tip ca și tipul de bază. În exemplul anterior, un cerc este o formă (dar o formă NU este neapărat un cerc!).

Când derivezi, poți să diferențiezi noile tipuri de tipul de bază prin două modalități: adăugare sau modificare.
Iată un exemplu în care s-a adăugat funcționalitate unui tip derivat:
Image

Cealatlă metodă este de a modifica unele funcționalități. Acest lucru se poate face prin a supra-definirea unei funcționalități existente din clasa de baza. Iată un exemplu:
Image

Pentru a suprascrie o funcție din clasa de bază în clasa derivată, pur și simplu definim din nou în felul în care dorim funcția respectivă în clasa derivată.

Polimorfism (polymorphism)


Când avem de a face cu ierarhii, trebuie să tratăm unele obiecte ca fiind obiecte de tipul clasei de baze. Acest lucru ne oferă posibilitatea să scriem cod care nu este dependent de anumite tipuri fixate. În exemplul cu Forme, funcțiile manipulau forme generice și nu conta de ce tip sunt formele respective. Toate formele pot fi desenate, șterse sau mutate - deci aceste funcții doar trimit mesaje către un obiect de tip formă. Nu contează cum anume se descurcă obiectul cu mesajele.

Cod de genul celui descris mai sus rămâne neafectat și reutilizabil chiar dacă adăugăm tipuri noi de forme, cum ar fi: pentagon, hexagon etc. Acest tip de design este unul foarte avantajos și reduce mult timpul și costurile de întreținere .

Să considerăm din nou exemplul cu forme. Ca să demonstrăm polimorfismul vrem să scriem o bucată de cod care ignoră detaliile despre tipul obiectelor și comunică doar cu clasa de bază. Acest cod va fi mai simplu de înțeles și mai ușor de scris. Și după cum ziceam, putem adăuga forme noi, iar codul va rămâne la fel.

Iată un exemplu de funcție, chiar dacă nu am învățat despre ele (mai târziu vom învăța despre funcții, care de fapt se vor numi metode și vom afla de ce se numesc așa) :

  1. //functie pentru rasturnarea unei forme
  2. void flip(Shape shape) {
  3.    shape.erase();
  4.    //... cod pentru rasturnarea formei
  5.    shape.draw();
  6. }


Această funcție vorbește cu orice obiect de tip Shape(forma), deci este independentă de ce tip de formă introducem ca parametru.
Să presupunem că folosim funcția flip() în altă parte în programul nostru:
  1. Circle circle = new Circle();
  2. Triangle triangle = new Triangle();
  3. //... cod
  4. flip(circle);
  5. flip(triangle);

Apelurile la funcția flip() funcționează corect și automat, fără să conteze tipul formei.
Dacă considerăm linia:

Aici pasăm un obiect de tip Circle(cerc) unei funcții care așteaptă un parametru de tip Shape(forma), dar Circle este de fapt de tip Shape. Deci Circle poate fi tratat ca fiind de tip Shape.

Numim acest proces de a trata un obiect de tip derivat ca fiind un tip de bază: "upcasting". De noțiunea de casting(procesul prin care tratăm o variabilă/obiect de un tip ca fiind de alt tip) poate sunteți familiarizați din alte limbaje, iar "up" vine de la faptul că pe diagramă mergem în sus în ierarhia tipurilor:
Image

În codul de mai sus, observați că nu avem nici un fel de structuri condiționale care sa verifice dacă un anumit obiect este de tipul "Circle" sau de tipul "Triangle". Dacă ar fi să scriem astfel de cod, care verifică tipul obiectelor, ne-am trezi cu un cod destul de dezordonat și urât. De asemenea, ar trebui să modificăm codul de fiecare dată când adăugăm o formă nouă (ex.:Hexagon).

Cu polimorfism reușim sa facem ceva foarte impresionant. Funcția flip() reușește cumva să apeleze codul care trebuie, indiferent de forma pe care lucrează. Compilatorul și sistemul run-time reușesc să se ocupe de detalii. Tot ce trebuie să știm este că se întâmplă ce trebuie chiar și când apare procesul de upcasting.
6p / 1 votes
Ilea Cristian
User avatar
smith
Enum
 
Joined: 29 Dec 2009
Location: Cluj-Napoca
Status: 82

Return to Tutoriale Java

Who is online

Users browsing this forum: No registered users and 0 guests