nav_dugme codeBlog codeBlog
  • početna Početna stranica
  • Sačuvani članci Sačuvani članci
  • Učionica
  • Saveti
  • Zanimljivosti
  • Kontakt

Uvod u objektno orijentisano programiranje

Viber
zoom_plus zoom_minus bookmark

Uvod

Objektno orijentisano programiranje je programska paradigma (pristup/metoda) koja podrazumeva rešavanje problema, odnosno projektovanje programa, kreiranjem i međusobnom interakcijom objekata:

  • objekte možemo shvatiti kao celine u programskom kodu koje odlikuje određeno stanje i ponašanje
  • objekti nastaju instanciranjem klasa (postupak izvođenja objekata sa konkretnim svojstvima, shodno unapred definisanom planu mogućih svojstava)
  • klase predstavljaju programski kod kojim se definišu moguće odlike budućih objekata

Možemo reći da su simulacije objekata iz spoljnjeg sveta u računarskim sistemima (razne simulacije vožnje, letenja i sl), najočiglednija i najrazumljivija manifestacija objektno orijentisanog pristupa, ali, nikako i jedina. Objekti su takođe i padajući meniji u browseru u kome trenutno čitate članak, ikone na desktopu, kao i brojni drugi objekti koji se ne vide (već "samo" podržavaju strukturu različitih programa koje koristimo).

Tematika objektno orijentisanog programiranja je složena, potrebno je pristupiti joj na ozbiljan način i uložiti odgovarajući trud da bi se postigli pravi rezultati, pa ćemo se potruditi da u nastavku predočimo sve što je neophodno za početno razumevanje.

Još nekoliko uvodnih napomena ....

Za bavljenje objektno orijentisanim programiranjem potrebno je solidno predznanje o proceduralnom programiranju, nezanemarljivo iskustvo u pisanju jednostavnijih programa i potrebno je biti upoznat sa tim šta OOP pristup (uopšte) podrazumeva.

U daljem tekstu i budućim člancima, za objektno orijentisano programiranje ćemo takođe povremeno koristiti i (veoma) uobičajenu skraćenicu OOP.

U slučaju većine čitalaca (i drugih pojedinaca) koji započinju upoznavanje sa objektno orijentisanim programiranjem, ukoliko su prva dva prethodno navedena uslova zadovoljena, tipično postoji i (bar) osnovna predstava o smislu i značaju OOP pristupa (vrlo često i o ponekim 'tehnikalijama'), ali ćemo - "za svaki slučaj" - pre nego što pređemo na osnovne pojmove objektno orijentisanog programiranja (klase, objekte i primere), prodiskutovati prvo o razlikama između proceduralnog i OOP pristupa, a iznećemo i kraći istorijat objektno orijentisanog programiranja (uz osvrt na to koje bi jezike trebalo koristiti, a koje izbegavati, pri početnom upoznavanju sa OOP pristupom) ....

Osnovne postavke, razlike u odnosu na proceduralni pristup

U proceduralnom programiranju (što je, da se podsetimo, zvanični naziv za "obično" programiranje sa kakvim već imate iskustva), primenjuje se takozvani pristup "odozgo-nadole" (engl. top-down), koji podrazumeva da se problem prvo sagledava u celosti, a zatim (po potrebi) pravilno razbija na potprobleme koji se, u praktičnom smislu, mogu pretočiti u funkcije (koje po pravilu ne bi trebalo da sadrže više od tridesetak linija koda).

Ovakav pristup primereno je koristiti u određenim situacijama, ali - ne i uvek.

U velikim projektima, složenost celokupnog posla je tipično tolika da ne dozvoljava da se problem sagleda odjednom (i zatim podeli na manje celine).

Umesto toga, koristi se pristup koji podrazumeva da se prvo detaljno definišu osnovni delovi sistema (objekti), pojedinačni način funkcionisanja svakog od delova, kao i načini njihovog povezivanja. Na kraju, celokupan sistem nastaje spajanjem i interakcijom pojedinačnih elemenata.

To je takozvani pristup "odozdo-nagore" (engl. bottom-up).

Ako uzmemo (svima dobro poznat) primer simulacija vožnje na računaru, lako možemo razumeti da se kao objekti prirodno javljaju automobili i staze * i da je prvo potrebno detaljno definisati pravila po kojima svaki od objekata funkcioniše, a potom i međusobne odnose koji odgovara poznatim zakonitostima fizike iz spoljnjeg sveta (gravitacija, inercija, detekcija sudara i sl), čime se na kraju kreira funkcionalan sistem.

* Pretpostavićemo da je jasno da se u navedenim objektima prepoznaju i "podobjekti" koje je potrebno simulirati: pogonski mehanizam automobila (motor), prenosni mehanizam i sl.

Naravno, funkcionisanje sistema koji smo prethodno opisali zavisi od raspoloživih resursa (da li ima dovoljno memorije za smeštaj podataka i da li procesor može da pokreće simulaciju dovoljnom brzinom), ali, slični zahtevi važe i za "ne-OOP" programe (rekli bismo da se sve što se tiče hardverskih zahteva - jednostavno podrazumeva).

Ako bismo pokušali da navedeni problem simulacije sagledamo "odjednom" (top-down pristup) .... verovatno bismo izazvali "blago" preopterećenje sopstvenog aparata za razmišljanje. :)

Kad smo se već dotakli automobila, možda bi i jedno poređenje sa auto industrijom moglo da bude od koristi, u razumevanju toga kakvo mesto OOP pristup zauzima u računarskoj industriji: kao što je motor sa unutrašnjim sagorevanjem (još uvek) najuobičajeniji vid pogona motornih vozila, iako nije jedini, tako je i OOP uobičajeni pristup u razvoju većine softvera: nije jedini, ali - jeste najuobičajeniji.

Kada koristiti proceduralni pristup, a kada objektno orijentisani

Ako smo u simulaciji vožnje na prirodan način prepoznavali računarske objekte koji nastaju po uzoru na objekte iz spoljnjeg sveta, sa nekim drugim zadacima to verovatno ne bi bio slučaj.

Jednostavno rečeno - ne moraju se svi zadaci rešavati korišćenjem OOP pristupa.

Ako bismo kao primer uzeli program koji treba da ukloni sve pojave uzastopnih praznih redova (tako da svuda ostane samo jedan) u svim datotekama u direktorijumu, rešenje se može lako sagledati u celosti i nema potrebe za objektima i klasama.

Problem (sasvim jednostavno) rešava petlja koja prolazi kroz sve fajlove, unutar koje se poziva funkcija koja analizira pojedinačnu datoteku tako da se na izlaz šalje samo prvi prazan red (dok se ostali susedni prazni redovi zanemaruju).

OOP pristup nije "bolji" od proceduralnog, niti "gori" (a važi naravno i obrnuto), već je samo u određenoj situaciji jedan od dva navedena pristupa primereniji i oba - i te kako - imaju mesto u informacionim tehnologijama.

OOP pristup se tipično vezuje za veće projekte u kojima učestvuju (veći) timovi programera (naravno, ima i projekata manjeg obima čija je struktura kompleksna i za koje je primereno koristiti OOP pristup), pri čemu je potrebno voditi računa o dobrom dizajnu klasa, čitljivosti koda i održivosti na duže staze.

Za manje programe jednostavne strukture, pomoćne skripte (i slično), objektno orijentisani pristup nije obavezan i proceduralni će poslužiti sasvim dobro.

U praksi naravno postoji i veći broj proceduralnih programa i skripti koji "nisu mali" (recimo, imaju preko 500, 1000 pa i više linija koda), veoma su kompleksni i zahtevni za razumevanje (i pored dobre optimizacije) i takođe, jako bitni u određenom informacionom sistemu, ali - jednostavno ne zahtevaju OOP pristup.

Takođe, mnogi naoko obimni programi napisani preko OOP tehnika su zapravo relativno jednostavni i "rutinski" (na primer, mnoge aplikacije koje se povezuju sa bazama podataka, zarad pregleda prometa u maloprodaji i sl).

Situacija ima raznih. :)

Kao što smo već naglasili: i jedan i drugi pristup imaju svrhu, a poenta OOP-a (naravno) nije da se mali programi učine komplikovani(ji)m, već da se veliki programi učine jednostavnijim i zapišu na pregledniji način.

Tematika vezana za prednosti i nedostatke top-down i bottom-up pristupa koje smo spominjali svakako je složenija (i zanimljivija) od onoga što ste mogli pročitati do sada, a isto važi i za istorijat OOP-a (sledeće poglavlje).

Stoga ćemo se potruditi da široj diskusiji o navedenim temama posvetimo zaseban članak (da članak koji trenutno čitate ne bi izgubio fokus).

Kratak istorijat OOP-a

Postoji svojevrstan "Mandela efekat" kod mnogih programera koji smatraju da istorija OOP-a počinje sredinom 80-ih godina dvadesetog veka, međutim, objektno orijentisani pristup prvi put se pojavio već početkom šezdesetih godina.

U to vreme, dvojica norveških programera, Ole-Johan Dal (Ole-Johan Dahl) i Kristen Najgard (Kristen Nygaard), razvili su programski jezik SIMULA (prva verzija pojavila se 1961), koji je još na početku imao mnoge odlike današnjih objektno orijentisanih programskih jezika.

Ipak, skromni kapaciteti računara iz šezdesetih i sedamdesetih godina su pomalo (ili - "malo više") kočili pravi napredak OOP pristupa, pa u praktičnom smislu možemo reći da je tek osamdesetih godina, a posebno od 1985, kada je danski programer Bjarne Stravstrup (Bjarne Stroustrup) objavio prvu zvaničnu verziju jezika C++, objektno orijentisani pristup počeo da doživljava pravu ekspanziju.

.... a inače su upravo osamdesete i vreme kada su softverski paketi (po izgledu i funkcionalnosti) već uveliko počeli da liče na današnje.

Mlađi čitaoci koji bi se vratili u šezdesete, pa čak i sedamdesete, godine dvadesetog veka, verovatno bi bili pomalo začuđeni time kako sve izgleda i funkcioniše. Ako biste se vratili u osamdesete, verovatno ne bi bilo tako.

Grafički interfejsi su bili manji i "kockastiji", memorijski kapaciteti su bili znatno manji i sve je bilo mnogo sporije, ali, okvirno - "to je otprilike (već) bilo to".

Vredi pomenuti i da mnogi poznati softverski paketi (na primer Photoshop, Autocad i sl), doslovno "vuku korene" (prve verzije) upravo iz navedenog perioda.

Sredinom devedesetih godina, uporedo sa vrhuncem ekspanzije C++ - a (smatramo, kao i mnogi, da je upravo to bilo vreme svojevrsne dominacije ovog jezika), pojavio se programski jezik Java, čije su glavne odlike bile: izvršavanje koda preko virtuelne mašine i (važnije za ovaj članak) - izrazita orijentisanost ka OOP pristupu.

Druga navedena odlika imala je veći (posredan) uticaj i na mnoge programske jezike koji OOP metodologiji do tada nisu pridavali previše značaja i Javu u opštem smislu možemo smatrati "najočiglednijim" primerom jezika koji je usmeren na objektno orijentisani pristup, pa ćemo stoga posvetiti nešto više pažnje sintaksi programskog jezika Java (i uopšte, pristupu koji je svojstven ovom jeziku).

Ako C++ (na primer) dozvoljava da se proceduralni pristup koristi samostalno ili u kombinaciji sa OOP pristupom (a ima i drugih jezika koji nude takve mogućnosti), sa Javom to nije slučaj: u Javi sve mora biti definisano preko klasa (pa makar program u praktičnom smislu bio najobičniji proceduralni program od nekoliko instrukcija), svaka klasa mora biti zapisana u zasebnoj datoteci, a svojstvenost ovog jezika su i dugački, deskriptivni nazivi klasa i njihovih elemenata.

I zaista, ako pogledamo prost "zdravo svete" program napisan u Javi ....

        
class Ispis {
    public static void main(String[] args) {
        System.out.println("Dobar dan! :)");
  }
}
        
    
Slika 1. - "Hello world" program u Javi.

.... sve deluje pomalo kao "karikatura" na prvi pogled, pogotovo ako takav kod uporedimo sa "očiglednim" primerom iz jezika koji nije "izrazito orijentisan na OOP pristup":

        
print("Dobar dan! :)")
        
    
Slika 2. - "Hello world" program u Python-u.

(Preskočili smo C/C++ i "zarad efekta" koristili Python, koji omogućava da se sve izvede u jednoj liniji koda.) *

Međutim, pojasnili smo već da je prava svrha OOP pristupa bolja organizacija većih projekata, a ne rešavanje ovakvih jednostavnih zadataka (doduše, na kursu Jave na početnim godinama studija, prost "hello world" program ćete pisati upravo onako kako ste videli iznad).

Java je vrlo očigledan primer jezika koji se "drži OOP-a", neki OOP jezici (kao na primer C++) nisu toliko "isključivi", a postoji i veći broj jezika koji nisu (na očigledan način) usmereni na neki od pristupa (proceduralni ili OOP), ali svakako podržavaju upotrebu klasa i objekata.

U svakom slučaju, možemo reći da je tokom vremena - a pogotovo uporedo sa ekspanzijom Jave i pod njenim uticajem (kao što smo već nagovestili), OOP pristup (bilo kao "glavna atrakcija", ili samo kao "jedna od mogućnosti"), našao mesto u velikoj većini iole popularnih i korišćenih programskih jezika. **

* Python (kao verovatno najočigledniji predstavnik "jednostavnih" skriptnih jezika), svoju izrazitu jednostavnost pokazuje (zapravo) samo onda kada su u pitanju veoma jednostavni programi. Čim kompleksnost programa (iole) počne da raste, razlike između skriptnih jezika kao što je Python i "zvaničnih" jezika kao što je Java - vrlo brzo "izblede".

** Jedan od najpoznatijih i 'najočiglednijih' izuzetaka (koji su inače retki), svakako je programski jezik C, ali, s obzirom na njegov ogroman značaj u programiranju, možemo ga (uz nešto "pesničke slobode") poistovetiti sa uvaženim starijim članom porodice koji je porodici obezbedio ugled i imetak.

Na nedeljnom ručku, ovaj šarmantni prosedi gospodin u belom odelu, sedi na čelu stola, mlađi mu prilaze sa poštovanjem i niko ne pravi pitanje oko toga zašto pater familias jede sladoled pre obroka (i neće da usvoji principe OOP-a) - zaslužio je.

Kada je u pitanju jezik koji ćemo u članku koristiti za primere, izabrali smo C# (ali, možete po želji lako podesiti da se za primere koristi neki od drugih jezika).

C# je jezik koji je poput Jave usmeren na klase, ali, rekli bismo ne baš u istoj meri. C# je ponešto fleksibilniji od Jave, smatramo da je sintaksa C#-a preglednija od sintakse drugih popularnih OOP jezika - i upravo zato smatramo da je C# najprikladniji jezik za početno upoznavanje sa OOP pristupom.

Python (da pomenemo "još koji" jezik), je svakako malo "ležerniji" po pitanju implementacije OOP tehnika (ali ćemo ipak prikazati i primere u Python-u), dok je Javascript znatno ležerniji, pa ćemo primere u JS-u preskočiti, jer OOP pristup svojstven JS-u nikako nije primeren za početno upoznavanje sa tematikom.

Python ne preporučujemo za početno upoznavanje sa tematikom OOP-a, ali smo ipak prikazali i primere u ovom jeziku, zarad opšteg obrazovanja i zarad "razbijanja monotonije" (jer su primeri u C#-u, C++-u i Javi, na osnovnom nivou - zapravo prilično slični).

Međutim, kada je u pitanju Javascript, tu smo već izričiti i preporučujemo da OOP elemente ovog jezika "zaobiđete u širokom luku", sve dok dobro ne savladate OOP kroz C++, C# ili Javu.

Osnovni pojmovi u objektno orijentisanom programiranju

Pošto smo se ukratko upoznali sa najopštijim odrednicama OOP-a i sa time kako je i zašto takav pristup našao mesto u računarskoj industriji, vreme je da se vratimo na tehnikalije, pa ćemo se za početak prvo upoznati sa najvažnijim konceptima (ili, kako neko voli da kaže - "stubovima") objektno orijentisanog programiranja.

Nakon toga, kroz jednostavan primer (sasvim prikladan za sam početak), koji ćemo razvijati/dopunjavati kroz poglavlja članka - proučićemo kako sve što navodimo funkcioniše u praksi.

Osnovni pojmovi u objektno orijentisanom programiranju su:

  • klase
  • objekti
  • apstrakcija podataka
  • enkapsulacija
  • nasleđivanje
  • polimorfizam

Svakom od ovih pojmova posvetićemo zaseban odeljak u nastavku.

Klase, objekti i apstrakcija podataka

Klasa je programski kod koji predstavlja obrazac po kome se kreiraju (budući) objekti.

Ovakav kod je tipično, ali ne i obavezno (videti napomenu) zapisan u zasebnoj datoteci i sadrži sve elemente koji opisuju moguća stanja i ponašanje objekata koji će biti kreirani upotrebom klase.

Elementi (odnosno, članovi) klasa, preko kojih se ostvaruje navedeno, su:

  • polja (zapisani podaci koji definišu stanje objekta)
  • metode (funkcije koje definišu radnje koje će budući objekti moći da obavljaju nad svojim sopstvenim podacima, ili sa drugim podacima u programu)

Kao što smo već pomenuli, u nekim programskim jezicima (kao što je na primer Java), doslovno je obavezno zapisati svaku klasu u zasebnu datoteku, ali, većina OOP programskih jezika je ipak ponešto fleksibilnija u tom smislu i dozvoljeno je definisati više klasa unutar jedne datoteke (jedan od jezika koji to dozvoljava je i C#).

Međutim, savetujemo da se ni tada ne "opuštate" previše, već da takvu slobodu koristite samo ako je potrebno "usput" definisati neku manju, pomoćnu klasu, a da za svaku iole veću i ozbiljniju klasu i dalje odvojite zasebnu (i prigodno imenovanu) datoteku.

Pojam apstrakcije (kao jedan od osnovnih principa u dizajnu klasa), podrazumeva odabir (samo) onih svojstava koja su bitna za definisanje objekata i izvršavanje programa i odbacivanje svojstava koja u datom kontekstu nisu bitna.

U dizajnu klase je dakle potrebno izabrati i opisati:

  • koji će podaci biti predstavljeni
  • kako će biti predstavljeni
  • kako će biti međusobno povezani
  • mehanizme za obradu podataka

Recimo, pojam osobe - koji je inače definisan mnogim svojstvima, kao što su telesna građa, duhovne odlike i sklonosti, odnos sa drugim osobama, svetom oko sebe (i mnogo čime još) - u dizajnu klase, u smislu odabira podataka (to jest, apstrakcije), bio bi sveden samo na one odlike koje su za datu aplikaciju bitne.

Na primer: u programu za poslovnu administraciju, možemo zanemariti većinu navedenih svojstava i ostaviti samo: datum rođenja, adresu stanovanja i podatke o učinku na radnom mestu.

Odnos između klase i objekta

Da bismo ilustrovali sve do sad navedeno i prikazali odnos između klase i objekta (odnosno, objekata, u množini), za primer ćemo uzeti klasu koja definiše pravilne mnogouglove zarad iscrtavanja na ekranu (znatno jednostavniji primer od onih koje smo do sada spominjali, ali, sasvim ilustrativan):

Opšti prikaz veze između klase i objekata
Slika 3. - Opšti prikaz veze između klase i objekata: klasa definiše moguća stanja, odnosno, opšte odlike (budućih) objekata; objekti predstavljaju konkretne izvedbe.

U smislu apstrakcije podataka, izabrali smo samo ono što je neophodno: opštim matematičkim svojstvima pravilnih mnogouglova dodali smo ona svojstva koja se koriste u prikazu na ekranu.

Odnos između klase i objekata je sledeći:

  • klasa je skup opštih karakteristika i mogućih stanja (koja se zajedno mogu pripisati budućim objektima)
  • objekat je konkretna realizacija klase, pri čemu je - među mogućim vrednostima različitih svojstava - izabrana određena kombinacija konkretnih vrednosti.

Prikazana klasa svakako jeste veoma jednostavna, ali sadrži specijalizovane podatke koji su izabrani prema konkretnim potrebama, a, što se tiče odnosa između klase i objekata u konkretnom slučaju: među opštim/mogućim karakteristikama (3 ili više stranica; poluprečnik predstavljen kao decimalna vrednost veća od 0, boja zadata kao skup RGB vrednosti), objekti (mnogouglovi) ispoljavaju određenu kombinaciju konkretnih svojstava.

Pri definisanju klasa (u opštem smislu), ne postoje pravila o tome šta se "mora" (ili ne mora) uzeti u obzir.

Naravno da je često situacija veoma jasna i nedvosmislena, pogotovo kada su u pitanju jednostavne, tipske klase (na primer klasa za smeštaj podataka iz baze podataka, najčešće će imati ista polja kao i odgovarajuća tabela u bazi), ali, ako uzmemo u obzir komplikovanije primere, kao što su različite računarske simulacije (recimo, pomenuta simulacija reli trka), situacija nije ni malo jednostavna: pre tridesetak godina 'strelica na gore' je značila 'auto ide napred', dok je 'strelica na dole' značila da smo 'aktivirali kočnicu'.

Današnje simulacije (zapravo, tako je već duži niz godina), vode računa o broju obrtaja motora (koliko smo jako stisnuli papučicu za gas (a ne samo da li je stisnuta ili otpuštena), koliko jako smo stisnuli kočnicu, koliko smo (tačno) okrenuli volan ulevo ili udesno, da li su gume pohabane (što utiče na proklizavanje) i da li je motor oštećen time što smo zaboravili da prebacimo menjač u viši stepen prenosa onda kada je motor dostigao (pre)visok broj obrtaja ....

Razvoj hardvera dozvolio je da se i softver razvije, odnosno da simulacije postanu lepše / bolje / raskošnije, ali i dalje ostaje pitanje: šta treba simulirati, a šta ne? Da li ćemo simulirati i zategnutost / otpuštenost matica, stanje opruga u sedištu vozača, uticaj temperature na širenje i skupljanje metala i slično?

Možda zapravo i hoćemo, ali, u praksi se svakako negde mora "podvući crta". Neke stvari se ostavljaju za buduće verzije programa (kada hardver bude u stanju da sve "povuče"), a druge se (ipak) odbacuju, kao izrazito nepraktične.

Polje (field) - podaci klase

Pojam polja (klase) u objektno orijentisanom programiranju označava pojedinačni imenovani podatak koji može biti:

  • primitivni podatak koji pripada nekom od osnovnih tipova (kao što su int, float ili char)
  • kolekcija podataka bilo kog tipa (niz, lista, red, stek ...)
  • objekat klase (iste ili različite)
  • pokazivač / referenca na podatak, objekat ili kolekciju bilo kog od navedenih tipova

Svi ovi podaci zajedno, kao što smo već nagovestili ranije, opisuju stanje (budućih) objekata.

Primer klase koja definiše datum
Slika 4. - Primer klase koja definiše datum: polja klase su celobrojne vrednosti koje predstavljaju godinu, dan, mesec, sat, minut i sekund, kao i UNIX timestamp. Pored polja, prisutne su i metode koje omogućavaju ažuriranje datuma i vode računa o usklađenosti svih podataka.

Iako su podaci sami po sebi uvek tačni, upotreba podataka u određenom kontekstu može biti nekorektna i takođe, odnosi između podataka ne moraju obavezno biti ispravni.

Primer #1: brojevi -4.5 i 0 su sami po sebi 'tačni' ('ispravni'), ali takva dva broja ne mogu biti stranice pravougaonika (prvi je negativan, a drugi je nula).

Primer #2: brojevi 4.5 i 5.5 su (ponovo) sami po sebi tačni i takva dva broja (ovoga puta) mogu biti stranice pravougaonika, ali - ako zabeležimo da je obim pravougaonika 1400, između vrednosti stranica i obima postoji nesklad.

Takođe: prevodilac za C++ ili Javu (ili neki drugi OOP jezik), ne "zna" šta predstavljaju "stranice pravougaonika", odnosno (u opštijem smislu), šta (inače) zapravo predstavljaju polja klase i u kakvim odnosima bi trebalo da budu, a to znači da ne postoji ni sistem koji će o svemu voditi računa "automatski" (više o tome u nastavku).

Da bi upotreba podataka u određenom kontekstu bila ispravna i da bi veze između podataka klase bile korektne, potrebno je pravilno sprovesti postupak enkapsulacije.

U pitanju je postupak u kome se podaci čuvaju od neovlašćenog pristupa ("diskutabilnih" upisa i sl) i takođe (kao što je već navedeno), po potrebi dovode u odgovarajuće međusobne odnose.

Ovoj veoma bitnoj temi ćemo posvetiti mnogo više pažnje u nastavku (pošto se osvrnemo na preostale osnovne odlike klasa).

Metode klase

Ponašanje objekata se opisuje (odnosno, zapisuje) preko metoda, koje su direktno definisane unutar klase.

U pitanju je programski kod koji je gotovo istovetan pojmu funkcije u programskom jeziku C, s tim da je (kao što je već navedeno) celokupan programski kod svih metoda zapisan unutar tela klase.

Opšti prikaz načina funkcionisanja metoda unutar klase / objekta
Slika 5. - Opšta šema načina funkcionisanja metode unutar klase/objekta.

Metode mogu pristupati podacima klase, ali takođe i spoljnim podacima iz drugih delova programa.

Da bismo mogli da za prave razumemo kako se odvija interakcija između polja i objekata i pre svega - kako klasa dobija željene odlike (samim tim - i budući objekti), počećemo polako da definišemo jednostavnu klasu i proučavamo šta se sve dešava u različitim fazama razvoja klase ....

Primer jednostavne klase - osnovni oblik

Na početku smo se potrudili da potcrtamo da se OOP pristup koristi (samo) onda kada su u pitanju kompleksni podaci koji takav pristup zavređuju, ali će primer koji ćemo koristiti za početno 'uhodavanje' (kako i dolikuje) ipak biti mnogo jednostavniji: definisaćemo klasu koja opisuje pravougaonik.

Primer je naizgled jednostavan (i ne samo naizgled), ali je sasvim adekvatan i u praktičnom smislu. U najjednostavnijim programima za početnike (gde je, na primer, samo potrebno izračunati obim i površinu pravougaonika), ne postoji realna potreba za kreiranjem klase koja opisuje pravougaonike, ali je zato za bilo koji program u kome se pojavljuju kolekcije pravougaonika (pogotovo ako se pravougaonici na kraju iscrtavaju, što ovoga puta neće biti deo primera), kreiranje takve klase više nego preporučljivo (a u mnogim slučajevima, praktično i obavezno).

Klasa Pravougaonik sadržaće polja a, b, O i P (podatke o stranicama pravougaonika, obimu i površini), metode kojima se obim i površina ažuriraju pri promeni vrednosti stranica a i b, kao i pomoćnu metodu za ispis podataka o pravougaoniku.

Uz sve navedeno, potrebno je udesiti i da četiri polja uvek budu u međusobnoj vezi koja odgovara pravilima geometrije (a, da li će se to dešavati "samo od sebe" - videćemo u nastavku, mada, verujemo da već znate odgovor na to pitanje) ....

Definisaćemo prvo najosnovnije okvire klase (tj. dodati samo osnovne podatke), pa će klasa imati sledeći oblik:

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	public Double a, b, O, P;
}
		
	
    	
class Pravougaonik
{
	public:

	double a, b, O, P;
};
		
	
    	
public class Pravougaonik {
	public double a, b, O, P;
}
		
	
    	
class Pravougaonik:
	# ne moramo definisati polja
	# jeste čudno u odnosu na ostale
	# jezike, ali, videćemo u nastavku
	# kako sve funkcioniše

		
	
Slika 6. - Klasa pravougaonik - početak - klasa za sada sadrži samo polja za upis podataka.

Četiri polja klase Pravougaonik (četiri promenljive tipa Double) beleže stranice pravougaonika (a i b), kao i obim (O) i površinu (P).

Napomena: Osnovni jezik koji ćemo koristiti za sve primere u ovom članku, biće C#, budući da upravo ovaj jezik smatramo najprimerenijim za početno upoznavanje sa principima objektno orijentisanog programiranja, ali, budući da čitaocima mogu biti zanimljiviji drugi jezici, spremili smo primere i u popularnim programskim jezicima kao što su C++ i Java (koji se koriste na prvim godinama studija), a tu je i Python.

Da biste automatski prebacili sve primere na drugi jezik, kliknite na odgovarajuće dugme za izbor jezika iznad bilo kog bloka koda i automatski će biti promenjen jezik i u ostalim blokovima (primer: da biste izabrali Javu za jezik koji će se koristiti za primere u celom članku, kliknite na bilo koje "Java" dugme)

Ako želite da izbegnete da se pri prvom kliku promene svi primeri, kliknite prvo na bilo koje "C#"" dugme, a zatim na željeno dugme za izbor jezika.

Iako klasa koju smo videli ni iz daleka nije dovršena, niti zapravo spremna za eksploataciju (tek smo počeli), u najosnovnijem (tehničkom) smislu, već je možemo koristiti za zapis podataka - naravno, uz napomenu da će u svemu biti nepredviđenih okolnosti koje i te kako zahtevaju pažnju.

Ali, upravo time se i bavimo: učimo kako se definišu klase i objekti i na šta se sve mora obratiti pažnja, pa ćemo pokazati:

  • kako se kreiraju (instanciraju) objekti
  • zašto je neophodno pravilno uspostaviti veze međe objektima (enkapsulacija)

Kreiranje objekta (rezervisana reč new)

Pozivanjem sledećeg (jednostavnog) koda ....

C#
C++
Java
Python 3
		
Pravougaonik P1 = new Pravougaonik();
		
	
		
Pravougaonik P1;
		
	
		
Pravougaonik P1 = new Pravougaonik();
		
	
		
P1 = Pravougaonik()
			
    	
Slika 7. - Kreiranje objekta P1 (klase Pravougaonik).

.... kreirali smo objekat P1, posle čega možemo pristupiti njegovim poljima:

C#
C++
Java
Python 3
        	
P1.a = 12.55;
P1.b = 10.40;
Console.WriteLine(P1.a);
Console.WriteLine(P1.b);
		
	
    	
P1.a = 12.55;
P1.b = 10.40;
cout << P1.a;
cout << P1.b;
		
	
    	
P1.a = 12.55;
P1.b = 10.40;
System.out.printf("%f\n", P1.a);
System.out.printf("%f\n", P1.b);
		
	
    	
P1.a = 12.55
P1.b = 10.40
print(str(P1.a))
print(str(P1.b))
		
	
Slika 8. - Pristup poljima objekta P1 - zadavanje vrednosti.

Polja a i b sada sadrže vrednosti koje same po sebi imaju smisla, ali, između polja klase ne postoje veze koje odgovaraju pravilima geometrije i verujemo da se mnogi od vas pitaju zašto je tako?

Iz iskustva znamo da mnogi polaznici pri prvom susretu sa objektno orijentisanim programiranjem imaju zamisao da OOP pristup "sam od sebe", "nekako" uspostavlja sve potrebne veze između podataka i rešava usputne probleme automatski, međutim (kao što smo već nagovestili) - to nije slučaj.

Vidimo i da se rezervisana reč new (očigledno) ne koristi u svim jezicima koje u članku koristimo za primere, ali, opšti princip instanciranja objekata je svakako veoma sličan.

Klasa nije automatizovan šablon koji "magijskim putem"/"tek tako"/samim tim što smo klasi dali ime koje nama nešto znači (a računaru - baš ništa), definiše sve moguće objekte koji nam mogu pasti na pamet (geometrijska tela, vozila, uređaje i slično) pri čemu važe odnosi iz spoljnjeg sveta.

Klasa je samo mehanizam koji omogućava da sve podatke i metode budućih objekata stavimo "pod istu kapu" (na jedno mesto), zarad preglednosti i bolje čitljivosti, a za uspostavljanje odnosa među podacima - moramo se pobrinuti sami.

Naravno, ništa od navedenog nije ni izdaleka "strašno", ali, svakako moramo biti pažljivi i svesni situacije.

Da pomenemo usput i to da je uobičajeno da nazivi klasa počinju velikim slovom ("Pravougaonik"), što ostavlja mogućnost da objekat (klase) nazovemo istim imenom koje počinje malim slovom ("pravougaonik") i po početnom malom slovu ga u kodu prepoznajemo kao objekat (i time razlikujemo od klase).

Naravno, kao što vidimo iz primera, moguće je objektima davati i druge, proizvoljne nazive.

Šta se tačno dešava sa podacima koje nismo inicijalizovali sami?

Ukoliko se sami ne pobrinemo za inicijalizaciju, polja čiji je tip Double (isto važi i za Int32) biće inicijalizovana na vrednost 0 (nula), niske se inicijalizuju kao prazne niske (""), a reference imaju vrednost null.

U slučaju objekta P1, koji je kreiran preko klase Pravougaonik, ukoliko inicijalizujemo stranice pravougaonika - ali ne i obim i površinu - polja O i P će imati vrednost 0 (što svakako nije u skladu sa pravilima geometrije i našim prvobitnim očekivanjima).

Budući da smo u primeru koji koristimo (naknadno) inicijalizovali stranice, a da obim i površina nisu inicijalizovani na odgovarajući način (polja jesu inicijalizovana, ali po podrazumevanoj metodi koja im dodeljuje vrednost 0), objekat P1 ima sledeće stanje:

    	
P1.a = 12.55
P1.b = 10.40
P1.O = 0
P1.P = 0
		
	
Slika 9. - Stanje objekta P1 posle nepotpune inicijalizacije.

Ovakav objekat nema praktično značenje (suštinski, nije ništa bolji nego četiri međusobno nezavisne promenljive), pa ćemo preduzeti korake da to popravimo, uvođenjem metoda koje će kontrolisati stanje podataka (polja).

Prvo ćemo se pobrinuti (iako to neće rešiti sve probleme), da bar početno stanje objekta bude korektno.

Jedna od najbitnijih stvari vezanih za promenljive u proceduralnom programiranju je inicijalizacija, to jest, postupak kojim se promenljivama zadaje početno stanje (u našem primeru, polja a i b dobila su vrednosti tek naknadno, a poljima O i P nedostaje početno stanje koje odgovara upisanim stranicama), pa će inicijalizacija polja biti prvo na šta ćemo obratiti pažnju u nastavku.

Jasno je da za računanje (ažuriranje) obima i površine moramo imati odgovarajuće metode i jasno je da objekat ne možemo inicijalizovati prostom naredbom dodele (kao običnu promenljivu), već i to moramo obaviti pozivom posebne metode.

Takva metoda u objektno orijentisanom programiranju nosi naziv konstruktor.

Konstruktor - metoda za definisanje početnog stanja objekta

Konstruktor je specijalizovana metoda kojom se zadaje početno stanje objekta.

Polja se inicijalizuju prostim naredbama dodele (ukoliko je moguće), ili pozivanjem pomoćnih metoda za ažuriranje vrednosti polja (ukoliko vrednosti određenih polja zavise od drugih polja, kao što je u našem primeru slučaj sa obimom i površinom pravougaonika, koji zavise od vrednosti stranica).

U pisanju, konstruktor se (u većini C-olikih jezika) prepoznaje po tome što (za razliku od ostalih metoda) nema povratni tip i po tome što ima isti naziv kao i sama klasa.

Dodaćemo konstruktor klasi Pravougaonik ....

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	public Double a, b, O, P;

	public Pravougaonik(Double a, Double b)
	{
		this.a = a;
		this.b = b;
	}
}
		
    
    	
class Pravougaonik
{
	public:

	double a, b, O, P;

	Pravougaonik(double a, double b)
	{
		this.a = a;
		this.b = b;
	}
};
		
    
    	
public class Pravougaonik {
	public double a, b, O, P;

	public Pravougaonik(double a, double b) {
		this.a = a;
		this.b = b;
	}
}
		
    
    	
class Pravougaonik:
	a, b, O, P

def __init__(self, a, b):
		self.a = a;
		self.b = b;
		
    
Slika 10. - Konstruktor dodat klasi pravougaonik: konstruktor ima dva parametra čije će vrednosti, pri pozivu konstruktora, biti prosleđene poljima (budućeg) objekta.

.... posle čega možemo zadati početno stanje objekta na sledeći način:

C#
C++
Java
Python 3
    	
Pravougaonik P1 = new Pravougaonik(12.55, 10.40);
		
    
    	
Pravougaonik P1(12.55, 10.40);
		
    
    	
Pravougaonik P1 = new Pravougaonik(12.55, 10.40);
		
    
    	
P1 = new Pravougaonik(12.55, 10.40)
		
    
Slika 11. - Kreiranje novog objekta (P1) klase pravougaonik, pozivanjem novog konstruktora koji prima dve vrednosti i prosleđuje ih poljima a i b.

Preko rezervisane reči new (kao što smo već videli), poziva se konstruktor klase (pri čemu se zadaju konkretni argumenti, koji će biti prosleđeni odgovarajućim poljima).

Kao što smo takođe već videli, rezervisana reč new ne koristi se u svim jezicima, ali ćemo ostaviti našim čitaocima koji za proučavanje primera ne koriste C#, već neki od drugih jezika, da sami uvide određene očigledne razlike (na primer - to da se u Python-u konstruktorska funkcija prepoznaje po nazivu __init__) i od sada ćemo skretati pažnju samo na suštinski bitne razlike u pristupu koji se koristi u različitim OOP jezicima.

Međutim, sve je (skoro) isto kao malopre: polja a i b odmah dobijaju konkretne vrednosti (o rezervisanoj reči this koju primećujete u kodu, više ćemo govoriti u sledećem odeljku), ali, polja O i P i dalje imaju vrednost 0 (tako smo udesili, naravno, isključivo iz razloga da još jednom potcrtamo da se stvari u objektno orijentisanom programiranju ne dešavaju automatski).

Uvođenjem dve nove metode: za računanje obima i za računanje površine, koje ćemo potom pozvati preko konstruktora (ako se neko pita, ovo je moguće, kao što je i inače moguće da metode pozivaju jedna drugu) - rešićemo problem:

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	public Double a, b, O, P;

	public Pravougaonik(Double a, Double b)
	{
		this.a = a;
		this.b = b;
		RacunanjeObima();
		RacunanjePovrsine();
	}

	public void RacunanjeObima()
	{
		O = 2 * (a + b);
	}

	public void RacunanjePovrsine()
	{
		P = a * b;
	}
}
		
    
    	
class Pravougaonik
{
	public:

	double a, b, O, P;

	Pravougaonik(double a, double b)
	{
		this.a = a;
		this.b = b;
		RacunanjeObima();
		RacunanjePovrsine();
	}

	void RacunanjeObima()
	{
		O = 2 * (a + b);
	}

	void RacunanjePovrsine()
	{
		P = a * b;
	}
};
		
    
    	
public class Pravougaonik {
	public double a, b, O, P;

	public Pravougaonik(double a, double b) {
		this.a = a;
		this.b = b;
		RacunanjeObima();
		RacunanjePovrsine();
	}

	public void RacunanjeObima() {
		O = 2 * (a + b);
	}

	public void RacunanjePovrsine()	{
		P = a * b;
	}
}
		
    
    	
class Pravougaonik
	a, b, O, P

def __init__ (self, a, b):
		self.a = a
		self.b = b
		RacunanjeObima()
		RacunanjePovrsine()

def RacunanjeObima():
	O = 2 * (a + b)

def RacunanjePovrsine():
	P = a * b
		
    
Slika 12. - Klasa Pravougaonik dopunjena metodama za računanje obima i površine, koje se pozivaju u konstruktoru.

Sada će (konačno), pri sledećem pozivu ....

C#
C++
Java
Python 3
    	
Pravougaonik P = new Pravougaonik(12.55, 10.40);
		
    
    	
Pravougaonik P(12.55, 10.40);
		
    
    	
Pravougaonik P = new Pravougaonik(12.55, 10.40);
		
    
    	
P = new Pravougaonik(12.55, 10.40)
		
    
Slika 13. - Ponovni poziv konstruktora klase.

.... objekat biti korektno inicijalizovan.

Pri pozivu konstruktora, pozivaju se i metode za računanje obima i površine, te će stoga sva četiri polja dobiti korektne vrednosti.

Ako pozovemo sledeći kod ....

C#
C++
Java
Python 3
    	
Console.WriteLine("-Stranica a: " + P1.a.ToString());
Console.WriteLine("-Stranica b: " + P1.b.ToString());
Console.WriteLine("-Obim: "       + P1.O.ToString());
Console.WriteLine("-Površina: "   + P1.P.ToString());
		
    
    	
cout << "-Stranica a: " << P1.a;
cout << "-Stranica b: " << P1.b;
cout << "-Obim: "       << P1.O;
cout << "-Površina: "   << P1.P;
		
    
    	
System.out.printf("-Stranica a: %f", P1.a);
System.out.printf("-Stranica b: %f", P1.b);
System.out.printf("-Obim: %f",       P1.O);
System.out.printf("-Površina: %f",   P1.P);
		
    
    	
print("-Stranica a: " + str(P1.a.ToString))
print("-Stranica b: " + str(P1.b.ToString))
print("-Obim: "       + str(P1.O.ToString))
print("-Površina: "   + str(P1.P.ToString))
		
    
Slika 14. - Konzolne instrukcije za ispis vrednosti polja.

.... dobićemo sledeći ispis:

    	
-Stranica a: 12.55
-Stranica b: 10.40
-Obim: 45.90
-Površina: 130.52
		
    
Slika 15. - Rezultat izvršavanja prethodno navedenih instrukcija.

Konačno smo došli do toga da je (bar) početno stanje objekta pravilno zadato, a, ako nam se ne sviđa prethodni poziv, možemo kreirati zasebnu metodu za ispis vrednosti polja ....

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	public Double a, b, O, P;

	public Pravougaonik(Double a, Double b)
	{
		this.a = a;
		this.b = b;
		RacunanjeObima();
		RacunanjePovrsine();
	}

	public void RacunanjeObima()
	{
		O = 2 * (a + b);
	}

	public void RacunanjePovrsine()
	{
		P = a * b;
	}

	public void KonzolniIspis()
	{
		Console.WriteLine("-Stranica a: " + this.a.ToString());
		Console.WriteLine("-Stranica b: " + this.b.ToString());
		Console.WriteLine("-Obim: "       + this.O.ToString());
		Console.WriteLine("-Površina: "   + this.P.ToString());
	}
}
    	
    
    	
class Pravougaonik
{
	public:

	double a, b, O, P;

	Pravougaonik(double a, double b)
	{
		this.a = a;
		this.b = b;
		RacunanjeObima();
		RacunanjePovrsine();
	}

	void RacunanjeObima()
	{
		O = 2 * (a + b);
	}

	void RacunanjePovrsine()
	{
		P = a * b;
	}

	void KonzolniIspis()
	{
		cout << "-Stranica a: " << this.a << endl;
		cout << "-Stranica b: " << this.b << endl;
		cout << "-Obim: "       << this.O << endl;
		cout << "-Površina: "   << this.P << endl;
	}
};
		
    
    	
public class Pravougaonik {
	public double a, b, O, P;

	public Pravougaonik(double a, double b)	{
		this.a = a;
		this.b = b;
		RacunanjeObima();
		RacunanjePovrsine();
	}

	public void RacunanjeObima() {
		O = 2 * (a + b);
	}

	public void RacunanjePovrsine()	{
		P = a * b;
	}

	public void KonzolniIspis()	{
		System.out.printf("-Stranica a: %f", this.a);
		System.out.printf("-Stranica b: %f", this.b);
		System.out.printf("-Obim: %f",       this.O);
		System.out.printf("-Površina: %f",   this.P);
	}
}
		
    
    	
class Pravougaonik
	a, b, O, P

	def __init__(self, a, b):
		self.a = a
		self.b = b
		RacunanjeObima()
		RacunanjePovrsine()

def RacunanjeObima():
	O = 2 * (a + b)	

def RacunanjePovrsine():
	P = a * b

def KonzolniIspis():
	print("-Stranica a: " + str(self.a))
	print("-Stranica b: " + str(self.b))
	print("-Obim: "       + str(self.O))
	print("-Površina: "   + str(self.P))
		
    
Slika 16. - Klasa Pravougaonik sa dodatom metodom za konzolni ispis vrednosti polja.

.... i pozvati je na sledeći način (koji je svakako nešto elegantniji):

C#
C++
Java
Python 3
    	
P1.KonzolniIspis();
		
    
    	
P1.KonzolniIspis();
		
    
    	
P1.KonzolniIspis();
		
    
    	
P1.KonzolniIspis()
		
    
Slika 17. - Pozivanje metode za konzolni ispis.

Programski kod sada jeste mnogo pregledniji, ali, definicija klase još uvek nije potpuna ....

Iako dosadašnji zapis omogućava korektnu inicijalizaciju objekta, ne postoji (za sada) mehanizam koji omogućava automatsko ažuriranje obima i površine pri promeni vrednosti stranica, a ne postoje ni mehanizmi koji onemogućavaju unošenje pogrešnih vrednosti stranica (recimo, negativnih brojeva i nule).

Skup tehnika OOP-a koje omogućavaju ovakve "sigurnosne provere" skupno nose naziv enkapsulacija i upravo će enkapsulacija polja biti glavna tema sledećeg poglavlja, ali ćemo se, pre nego što se posvetimo enkapsulaciji, osvrnuti ukratko na rezervisanu reč this koju smo prethodno koristili unutar konstruktora.

Rezervisana reč "this"

Svrha rezervisane reči this je razrešavanje nedoumica koje mogu nastati u zapisu programskog koda, kada parametri konstruktora imaju iste nazive kao polja klase.

Krajnje je uobičajeno da parametri konstruktora (koji se praktično koriste samo pri inicijalizaciji objekata), imaju iste nazive kao polja klase.

Da pojasnimo ....

Kada je objekat kreiran, njegovim poljima se pristupa navođenjem identifikatora objekta i polja, koji su spojeni operatorom pristupa (. u C#-u i Javi ili -> u C++ - u)

Na primer: P1.a - kao način pristupa stranici a objekta P (koji predstavlja pravougaonik).

Takav pristup je moguć kada (ako) je objekat već kreiran, ali, u samoj klasi Pravougaonik nije moguće koristiti poziv "Pravougaonik.a"!

Rezervisana reč this se koristi u situacijama kada, pri definisanju klase, želimo da se obratimo "budućem objektu" (koji tek treba da nastane instanciranjem klase).

U primeru konstruktora koji smo koristili....

C#
C++
Java
Python 3
    	
public Pravougaonik(Double a, Double b)
{
	this.a = a;
	this.b = b;
	RacunanjeObima();
	RacunanjePovrsine();
}
		
    
    	
public:

Pravougaonik(double a, double b)
{
	this.a = a;
	this.b = b;
	RacunanjeObima();
	RacunanjePovrsine();
};
		
    
    	
public Pravougaonik(double a, double b) {
	this.a = a;
	this.b = b;
	RacunanjeObima();
	RacunanjePovrsine();
}
		
    
    	
def __init__(self, a, b):
	self.a = a
	self.b = b
	RacunanjeObima()
	RacunanjePovrsine()
		
    
Slika 18. - Preko rezervisane reči "this" obraćamo se poljima klase na mestima gde može doći do zabune.

.... upotrebom rezervisane reči this (u Python-u se umesto rezervisane reči this, koristi se rezervisana reč self), pravimo razliku između polja klase a - this.a i parametra konstruktora a.

Klase za smeštaj podataka

Ukratko ćemo se na ovom mestu osvrnuti i na to da povremeno postoji potreba za klasama koje će se koristiti samo za smeštaj podataka (u stranoj literaturi se za ovakve klase koristi odrednica "data class") i takve klase se mogu kreirati preko do sada prikazane sintakse.

Praktično je u pitanju struktura podataka nalik na slogove (struct), kakvu možemo koristiti onda kada ne postoji potreba za proverom i međusobnim usklađivanjem podataka.

Na primer, ukoliko uvozimo podatke iz baze podataka, pri čemu podaci ne moraju biti međusobno usklađeni (i pri tom niske mogu biti prazne i sl), možemo definisati klasu po sledećem obrascu ....

			
public class Osoba
{
	public Uint64 Id; 
	public string Ime, Prezime, Email;

	public Osoba(Uint64 Id, string Ime, string Prezime, string Email)
	{
		this.Id      = Id;
		this.Ime     = Ime;
		this.Prezime = Prezime;
		this.Email   = Email;
	}
}
			
		
Slika 19. - Primer klase za smeštaj podataka.

.... i takva klasa će sasvim dobro poslužiti navedenoj svrsi.

Naravno, opisani pristup možemo koristiti samo ponekad (onda kada zaista nije potrebno proveravati sadržaj polja i njihove međusobne veze).

Budući da klasa Pravougaonik ne spada u "baš skroz jednostavne" klase (u kojima "možemo proći" bez provere podataka) i budući da pri tom (i pre svega) nije još uvek završena, vraćamo se na glavni posao ....

Enkapsulacija

Enkapsulacija (kao što smo već nagovestili) predstavlja skup tehnika kojima se:

  • definišu pravila za pristup poljima i metodama klase
  • uspostavljaju prikladne veza između podataka u poljima

Međutim, u još osnovnijem smislu, enkapsulacija u objektno orijentisanom programiranju predstavlja opštu ideju koja nalaže da pristup podacima treba da bude regulisan tako da "unutrašnji mehanizmi" klase budu skriveni od krajnjih korisnika, ali da je (pri tom) korisnicima klase omogućeno neometano korišćenje svih neophodnih funkcionalnosti.

Da pojasnimo preko poznatog primera iz spoljnjeg sveta: vozač automobila (u prenesenom značenju - krajnji korisnik klase), na raspolaganju ima volan, menjač i papučice (gas, kvačilo i kočnicu), čime je u mogućnosti da upravlja automobilom, ali, nema uticaja na dizajn karburatora, prenosnog mehanizma, vešanja, klipova i drugih delova, već je to posao autoinženjera (u prenesenom značenju - programera koji su zaduženi za dizajn/održavanje klase).

Sa blagodetima enkapsulacije (koliko god to čudno delovalo) najbolje ćemo se upoznati dodatnim posmatranjem klase koju smo već koristili, koja (još uvek) ne koristi enkapsulaciju ....

Šta se dešava ukoliko ne koristimo enkapsulaciju?

Ako se setimo naredbe P1.a = 12.55; kojom smo zadali vrednost 12.55 polju a objekta P1, može nam pasti na pamet da napravimo i sledeće pozive ....

C#
C++
Java
Python 3
    	
P1.O = 4114.14;
P1.P = -19.25;
		
    
    	
P1.O = 4114.14;
P1.P = -19.25;
		
    
    	
P1.O = 4114.14;
P1.P = -19.25;
		
    
    	
P1.O = 4114.14
P1.P = -19.25
		
    
Slika 20. - Zadavanje nelogičnih vrednosti poljima O i P (vrednosti su, same po sebi, kao podaci, tačne, ali nisu u skladu sa vrednostima polja a i b).

Iako je objekat prethodno bio korektno inicijalizovan, pozivi kakve smo videli će (ponovo) poremetiti objekat: obim je mnogo veći nego što bi trebalo, a površina je negativan broj..

Međutim, u ovakvoj situaciji se pre svega moramo zapitati - da li uopšte treba da postoji mogućnost direktnog zadavanja vrednosti obima i površine?!

(Pitanje je jednostavno, a odgovor je - ne treba.)

Dovoljno je da postoje samo dve moguće kombinacije vrednosti polja a i b posle promene obima ili površine, pa da ceo objekat postane vrlo neodređen, a kombinacija svakako ima "više od dve" (po matematici - beskonačno mnogo, a u praktičnoj implementaciji na računaru, iako broj nije beskonačan, kombinacija ima izrazito mnogo).

Ako se nekim slučajem dvoumite na šta mislimo, recimo da površini 40 odgovaraju stranice 5 i 8 - ali isto tako i 4 i 10, 2 i 20, kao i mnoge druge kombinacije (pošto je poljima a i b pripisan tip podatka Double, kombinacije se broje stotinama miliona).

Potrebno je dakle da postignemo dve stvari:

  • da se obim i površina automatski ažuriraju pri promeni stranica a i b
  • da mogućnost ručnog upisivanja vrednosti obima i površine bude ukinuta - ali da ostane mogućnost da se navedene vrednosti čitaju

U tehničkom smislu (što ćemo videti i u slučaju klase Pravougaonik), enkapsulacija se postiže kritičkom upotrebom specifikatora pristupa u kombinaciji sa javnim metodama koje regulišu pristup privatnim poljima (ili upotrebom akcesora set i get, u jezicima koji to podržavaju, kao što je C#).

Upravo smo izneli podosta novih/nepoznatih pojmova, pa ćemo se u nastavku potruditi da ih sve detaljno razjasnimo i naravno, iako će nam novopomenute tehnikalije u prvom trenutku pomalo "zakomplikovati život", takođe će uneti i red kao što je neophodno (pogotovo u dugoročnom smislu), pa ćemo konačno "doterati" klasu Pravougaonik kako dolikuje.

Specifikatori pristupa (private, public, protected)

Specifikatori pristupa su rezervisane reči koje stoje uz deklaracije polja i metoda i određuju mogućnost pristupa elementima klase:

  • private - poljima/metodama moguće je pristupati samo unutar klase i nije moguće pristupati im iz spoljnih delova koda
  • public - poljima/metodama moguće je neposredno pristupati iz spoljnih delova koda (i naravno unutar klase)
  • protected - poljima/metodama moguće je pristupati unutar osnovne klase i nasleđenih klasa (o nasleđivanju ćemo govoriti u nastavku), ali, nije moguće pristupati im iz spoljnih delova koda

Najvažniji specifikatori pristupa su private i public, pa ćemo im posvetiti najviše pažnje.

Primetili smo da je, do sada, uz sva polja (promenljive) klase Pravougaonik stajala odrednica public i ova rezervisana reč sugeriše spoljnjim delovima programskog koda da mogu neposredno pristupati poljima uz koje odrednica stoji.

Dobra strana ovakve "otvorenosti" je to što možemo neposredno pristupati poljima, a loša, to što (time što bilo kom polju koje ima specifikator pristupa public možemo neposredno pristupiti), možemo dovesti u pitanje korektnost međusobne povezanosti polja i (samim tim) funkcionalnost objekta.

(Kao recimo malopre, kada smo upisali da je površina pravougaonika -19.25, što nije matematički moguće.)

U Python-u se, umesto eksplicitnog navođenja specifikatora pristupa, koriste konvencije pri imenovanju polja, koje podrazumevaju sledeće:

  • public - identifikator nema donju crtu na početku (primer: a, b)
  • protected - identifikator ima jednu donju crtu na početku (primer: _a, _b)
  • private - identifikator ima dve donje crte na početku (primer: __a, __b)

Prvi korak u enkapsulaciji (tipično) podrazumeva da, preko specifikatora pristupa private, onemogućimo neposredan pristup određenom polju.

U slučaju klase Pravougaonik, izvešćemo to na sledeći način:

C#
C++
Java
Python 3
    	
private Double a, b, O, P;
		
    
    	
private:

double a, b, O, P;
		
    
    	
private double a, b, O, P;
		
    
    	
__a, __b, __O, __P
		
    
Slika 21. - Korišćenjem specifikatora pristupa private, skrivamo polja klase. U sledećem koraku kreiraćemo metode kojima se kontroliše pristup datim vrednostima.

Međutim, kada to uradimo, spoljni delovi koda više ne mogu pristupati poljima a, b, O i P.

U opštem smislu, nemogućnost pristupa može, ali i ne mora, predstavljati problem i ako nemogućnost pristupa poljima ne predstavlja problem, ne moramo preduzimati dalje korake.

U slučaju implementacije klase Pravougaonik (naravno, izabrali smo tipičan slučaj) - nemogućnost pristupa poljima predstavlja problem (razumno je pretpostaviti da će bar povremeno postojati potreba za ažuriranjem stranica pravougaonika i pogotovo - za neposrednim čitanjem vrednosti pojedinačnih polja), pa ćemo preduzeti korake da omogućimo pristup poljima na prikladan način.

Za početak, pogledaćemo u sledećem odeljku na koji način možemo privatnim poljima pristupati preko javnih metoda.

Postupak enkapsulacije koji prikazujemo jeste najopštiji (ne želimo da sakrivamo polja, ali, bezuslovni upis može napraviti probleme - i zato preduzimamo dodatne korake).

Međutim, u određenim okolnostima se može desiti da enkapsulacija podrazumeva i:

  • totalno "zatvaranje" (koristimo samo private polja - zato što je to u datim okolnostima optimalan pristup)
  • totalno "otvaranje" (upis proizvoljnih vrednosti u public polja, neće stvoriti probleme).

Pristup privatnim poljima preko javnih metoda

Javna metoda za čitanje vrednosti privatnog polja ima sledeći oblik:

C#
C++
Java
Python 3
    	
public Double Citanje_a()
{
	return this.a;
}
		
    
    	
public:

double Citanje_a()
{
	return this.a;
};
		
    
    	
public double Citanje_a() {
	return this.a;
}
		
    
    	
def Citanje_a():
	return self.a
		
    
Slika 22. - Javno dostupna (publici) metoda Citanje_a vraća vrednost polja a.

Specifikator pristupa public omogućava pristup metodi iz spoljnih delova koda, a preko povratnog tipa Double omogućava se direktno čitanje vrednosti polja a u izvornom obliku (a ne samo tekstualni ispis).

Za upis (ažuriranje), vrednosti polja a, koristićemo sledeću javnu metodu:

C#
C++
Java
Python 3
    	
public void Upis_a(Double a)
{
	// Radi preglednosti nećemo ovde
	// pisati deo koda sa porukom o grešci,
	// izuzecima i sl.

	if (a <= 0) return;  

	// Ako je uneta vrednost pozitivan broj,
	// ažuriraćemo obim i površinu

	this.a = a;          
	RacunanjeObima();
	RacunanjePovrsine();
}
		
    
    	
public:

void Upis_a(double a)
{
	// Radi preglednosti nećemo ovde
	// pisati deo koda sa porukom o grešci,
	// izuzecima i sl.

	if (a <= 0) return;  

	// Ako je uneta vrednost pozitivan broj,
	// ažuriraćemo obim i površinu

	this.a = a;          
	RacunanjeObima();
	RacunanjePovrsine();
};
		
    
    	
public void Upis_a(double a) {
	// Radi preglednosti nećemo ovde
	// pisati deo koda sa porukom o grešci,
	// izuzecima i sl.

	if (a <= 0) return;  

	// Ako je uneta vrednost pozitivan broj,
	// ažuriraćemo obim i površinu

	this.a = a;          
	RacunanjeObima();
	RacunanjePovrsine();
}
		
    
    	
def Upis_a(a)
	# Radi preglednosti nećemo ovde
	# pisati deo koda sa porukom o grešci,
	# izuzecima i sl.

	if a <= 0:
		return  

	# Ako je uneta vrednost pozitivan broj,
	# ažuriraćemo obim i površinu

	self.a = a
	RacunanjeObima()
	RacunanjePovrsine()
		
    
Slika 23. - Javno dostupna (public) metoda Upis_a upisuje predatu vrednost u polje a (ali, samo ako je data vrednost pozitivna) i potom ažurira obim i površinu.

Upotrebom navedenog pristupa rešavamo dva problema:

  • proveru ulaznog podatka (ukoliko je unet negativan broj ili nula, vrednost polja a se ne ažurira, a metode za ažuriranje obima i površine se ne pozivaju)
  • ažuriranje obima i površine pri zadavanju nove vrednosti stranice

Klasa je sada konačno spremna za upotrebu, pa možemo pogledati šematske prikaze klase koju smo napisali i objekta koji nastaje instanciranjem klase.

Klasa:

Šematski prikaz klase Pravougaonik
Slika 24. - Šematski prikaz klase pravougaonik (konstruktor je izostavljen zarad preglednosti)

Objekat:

Šematski prikaz objekta P (klase Pravougaonik)
Slika 25. - Šematski prikaz objekta klase Pravougaonik: spolja gledano, objekat nudi samo nekoliko metoda i ne prikazuje unutrašnje mehanizme obrade podataka

Deluje da klasa sadrži više, međutim, tako je samo naizgled. Na objektu je takođe "sve tu", samo .... "ispod haube". :)

Slike koje smo videli takođe predstavljaju i svojevrstan šematski prikaz principa enkapsulacije u opštem smislu: "sve je tu", ali, direktno možemo pristupati samo određenim metodama.

Za kraj odeljka o enkapsulaciji, razmotrićemo i primer enkapsulacije u programskom jeziku C# preko tzv. svojstava, što je pristup kojim se postiže još veća preglednost (ali, nažalost nije dostupan u ostalim jezicima koje koristimo za primere).

Enkapsulacija preko akcesora u programskom jeziku C#

Videli smo da je pristup privatnim poljima moguć preko javnih (public) metoda, a da je razlog zašto uopšte skrivamo polja klase, to što u mnogim situacijama (gde su vrednosti određenih polja uslovljene vrednostima drugih polja), želimo da očuvamo veze između podataka.

Kao što smo već naveli, enkapsulacija i "skrivanje podataka" nisu obavezni uvek, ali, najčešće u iole ozbiljnijim i obimnijim klasama postoji bar poneki primer eksplicitno definisane enkapsulacije.

Pristup koji smo opisali u prethodnom odeljku, možemo koristiti u većini programskih jezika koji podržavaju OOP (praktično: u svim OOP jezicima koji su nama poznati), ali, neki jezici, kao što je C# (koji koristimo u ovom članku), nude i dodatne mogućnosti.

U C#-u je moguće definisati "svojstva" - blokove koda sa tzv. akcesorima, posebnim odeljcima u kojima su zapisana pravila za pristup skrivenim poljima (čitanje i upis).

Princip definisanja svojstava u C# se (suštinski) gotovo nimalo ne razlikuje od upotrebe javnih metoda za pristup privatnim poljima, ali - zapis je nešto elegantniji.

Da bismo se upoznali sa svojstvima, pogledaćemo jednostavan primer (pri čemu ćemo napraviti i izvesne male izmene i prikazati odmah kako upotreba svojstava obično izgleda u klasama):

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	private Double _a, _b, _O, _P;

	public Pravougaonik(Double a, Double b)
	{
		this.a = a;
		this.b = b;
		RacunanjeObima();
		RacunanjePovrsine();
	}

	public Double a // Ovo je "svojstvo" a (nije isto što i polje a,
	{               // koje smo koristili u prethodnom opisu klase)
		get
		{
			return _a;
		}
		set
		{
			if (a > 0)
			{
				this.a = value;
				RacunanjeObima();
				RacunanjePovrsine();
			}
		}

	}                
}
		
    
    	
// Opcija nije dostupna u programskom jeziku C++
		
    
    	
// Opcija nije dostupna u programskom jeziku Java
		
    
    	
# Opcija nije dostupna u programskom jeziku Python
		
    
Slika 26. - Svojstvo a, definisano preko dva akcesora (set i get). Svojstva su svojevrsne kombinacije polja i metoda: spolja gledano, izgledaju kao public polja, a, po potrebi, funkcionišu kao metode.

Da pojasnimo kako funkcioniše kod koji smo videli: deo koda ....

C#
C++
Java
Python 3
    	
public Double a
{

}
		
    
    	
// Opcija nije dostupna u programskom jeziku C++
		
    
    	
// Opcija nije dostupna u programskom jeziku Java
		
    
    	
# Opcija nije dostupna u programskom jeziku Python
		
    
Slika 27. - Osnovna definicija svojstva a.

.... definiše svojstvo a koje će biti dostupno spoljnim delovima koda (specifikator pristupa ovog svojstva je public).

Unutar zagrada, definisana su dva bloka. Prvi blok je get ....

C#
C++
Java
Python 3
    	
get
{
	return _a
}
		
    
    	
// Opcija nije dostupna u programskom jeziku C++
		
    
    	
// Opcija nije dostupna u programskom jeziku Java
		
    
    	
# Opcija nije dostupna u programskom jeziku Python
			
        
Slika 28. - Akcesor get koji vraća vrednost polja _a.

.... kojim je određeno da će programski kod koji zatraži vrednost svojstva a (zapravo) dobiti vrednost polja _a.

Na primer, preko sledećeg koda ....

C#
C++
Java
Python 3
    	
Pravougaonik P = new Pravougaonik(5.5, 10.5);
MessageBox.Show("Stranica a: " + P1.a.ToString());
		
    
    	
// Opcija nije dostupna u programskom jeziku C++
		
    
    	
// Opcija nije dostupna u programskom jeziku Java
		
    
    	
# Opcija nije dostupna u programskom jeziku Python
		
    
Slika 29. - Primer koda koji prikazuje upotrebu svojstva a (naizgled, veoma slično kao kada smo samo koristili javno polje a, ali - samo naizgled).

.... dobićemo ispis stranice a (pozivom P1.a dobijamo vrednost polja P1._a, a zatim se poziva opšta funkcija ToString() kojom se ispisuje vrednost).

Drugi blok, set ....

C#
C++
Java
Python 3
    	
set
{
	if (a > 0)
	{
		this.a = value;
		RacunanjeObima();
		RacunanjePovrsine();
	}
}
		
    
    	
// Opcija nije dostupna u programskom jeziku C++
		
    
    	
// Opcija nije dostupna u programskom jeziku Java
		
    
    	
# Opcija nije dostupna u programskom jeziku Python
			
        
Slika 30. - Upravo je akcesor set ono što čini svojstvo a suštinski različitim (i mnogo "sigurnijim" za korišćenje), u odnosu na javno polje a (koje smo koristili pre nego što smo počeli da upotrebljavamo svojstva). Ako sada pokušamo da unesemo vrednost stranice koja je manja od 0, program neće prihvatiti novu vrednost (i neće preračunavati bim i površinu).

.... određuje da će polje _a dobiti predatu vrednost (value), samo u slučaju ako je predata vrednost veća od nule.

Prvo što primećujemo u odeljku set je sličnost sa pristupom koji smo koristili u prethodnom odeljku (gde smo koristili metodu Upis_a), a drugo je rezervisana reč value (koja može delovati pomalo "zbunjujuće").

Posmatrajmo to ovako (postavimo sebi pitanje): pod kojim okolnostima određeno polje može dobiti novu vrednost? Odgovor je - preko naredbe dodele, a naredba dodele, sa jedne strane sadrži identifikator promenljive (može naravno biti i polje klase), a sa druge .... ništa drugo nego izračunatu vrednost.

C#
C++
Java
Python 3
    	
P1.a = 10;    // vrednost se zadaje direktno
P1.a = X;     // vrednost se dobija čitanjem vrednost druge promenljive
P1.a = X + Y; // vrednost se dobija računanjem vrednosti izraza
		
    
    	
P1.a = 10;    // vrednost se zadaje direktno
P1.a = X;     // vrednost se dobija čitanjem vrednost druge promenljive
P1.a = X + Y; // vrednost se dobija računanjem vrednosti izraza
		
    
    	
P1.a = 10;    // vrednost se zadaje direktno
P1.a = X;     // vrednost se dobija čitanjem vrednost druge promenljive
P1.a = X + Y; // vrednost se dobija računanjem vrednosti izraza
		
    
    	
P1.a = 10;    # vrednost se zadaje direktno
P1.a = X;     # vrednost se dobija čitanjem vrednost druge promenljive
P1.a = X + Y; # vrednost se dobija računanjem vrednosti izraza
		
    
Slika 31. - Primeri različitih naredbi dodele.

Upravo to je razlog zašto se (makar u programskom jeziku C#), koristi rezervisana reč value, u smislu: kakva god vrednost da se nađe sa desne strane naredbe dodele, biće prosleđena svojstvu a.

Na kraju, primetimo da su svojstva svojevrsne kombinacije polja i metoda: svojstvo, spolja gledano (kada svojstvu pristupamo iz spoljnih delova koda), izgleda kao polje, ali, "iznutra" funkcioniše kao metoda.

U našem slučaju (koji je tipičan primer upotrebe svojstava):

  • dodela nije bezuslovna
  • posle dodele pozivaju se metode za ažuriranje

.... a takođe, u opštem smislu:

  • jedan od dva bloka može se i izostaviti

Na primer:

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	....

	public Double O // Obim 
	{
		get
		{
			return _O;
		}
	}

	public Double P // Površina
	{
		get
		{
			return _P;
		}
	}
}
	
	
// Opcija nije dostupna u programskom jeziku C++
		
    
    	
// Opcija nije dostupna u programskom jeziku Java
		
    
    	
# Opcija nije dostupna u programskom jeziku Python
		
    
Slika 32. - Svojstva O i P omogućavaju korisnicima klase da očitavaju vrednosti za obim i površinu, ali ne i da zadaju ove vrednosti direktno (što je svakako dobra praksa).

Svojstva O i P su "read only" i moguće je samo čitati vrednosti obima i površine (znamo da ne možemo nikako odrediti stranice a i b na nedvosmislen način, ako unesemo vrednost za obim ili površinu).

Takođe, primetili smo i da ćemo sada, kada spoljnim delovima koda "otkrivamo" svojstva koja nose prethodne nazive polja (a, b, O i P), za sama polja koristiti nazive sa dodatkom podvučene crte (_a, _b, _O i _P).

U pitanju je konvencija (i uobičajena praksa) u programiranju, koju svakako treba uvažiti.

Pred kraj, pozabavićemo se temom nasleđivanja u objektno orijentisanom programiranju.

Nasleđivanje i polimorfizam

Naveli smo ranije da je (prava) svrha objektno orijentisanog programiranja - dobra organizacija podataka.

U tom smislu, da bismo razumeli svrhu nasleđivanja u OOP-u, zamislićemo sledeću situaciju (nećemo ovoga puta koristiti klasu Pravougaonik, već drugi primer):

  • potrebno je definisati klasu Osoba, sa nekoliko opštih polja kao što su Ime, Prezime, DatumRodjenja i Starost
  • potrebno je definisati klasu Radnik koja sadrži sva polja prethodne klase, a sadrži i polja koja su specifično vezana za radnike: RadnoMesto, PocetakRada i RadniStaz

Možemo definisati dve potpuno nezavisne klase, ali, možemo primeniti i bolji pristup koji omogućava određene pogodnosti.

Nasleđivanje

Prvo je potrebno definisati opštiju od dve klase (onu koja sadrži zajedničke podatke), što je u našem slučaju klasa Osoba:

C#
C++
Java
Python 3
    	
public class Osoba
{
	public  String   Ime, Prezime;
	public  DateTime DatumRodjenja;
	private UInt32   _Starost;

	public Osoba(String Ime, String Prezime, DateTime DatumRodjenja)
	{
		this.Ime           = Ime;
		this.Prezime       = Prezime;
		this.DatumRodjenja = DatumRodjenja;

		RacunanjeStarosti();
	}

	public void RacunanjeStarosti()
	{
		_Starost = DateTime.Now.Year - DatumRodjenja.Year;

		if (DateTime.Now.Month < DatumRodjenja.Month)
		{
			_Starost--;
		}

		if (DateTime.Now.Month == DatumRodjenja.Month &&
		   DateTime.Now.Day   <  DatumRodjenja.Day)
		{
			_Starost--;
		}

	}

	public UInt32 Starost
	{
		return _Starost;
	}
}
		
    
    	
struct Datum {
	int godina, mesec, dan;
};

class Osoba
{
	private:

	int _starost;

	public:

	string ime, prezime;
	Datum  datumRodjenja;

	Osoba(string ime, string prezime, Datum datumRodjenja)
	{
		this->ime           = ime;
		this->prezime       = prezime;
		this->datumRodjenja = datumRodjenja;
		racunanjeStarosti();
	}

	racunanjeStarosti()
	{
		time_t d     = time(NULL);
		tm*    datum = localtime(&d);

		int godina   = datum->tm_year + 1900;
		int mesec    = datum->tm_mon  + 1;
		int dan      = datum->tm_mday;

		_starost = godina - datumRodjenja.godina;

		if (mesec < datumRodjenja.mesec)
		{
			_starost--;
		}

		if (mesec == datumRodjenja.mesec &&
		   dan   <  datumRodjenja.dan)
		{
			_starost--;
		}
	}

	int citanjeStarosti() {
		return _starost;
	}
};
		
    
    	
public class Osoba {
	public  String    Ime, Prezime;
	public  LocalDate DatumRodjenja;
	private int       _Starost;

	public Osoba(String Ime, String Prezime, LocalDate DatumRodjenja) {
		this.Ime           = Ime;
		this.Prezime       = Prezime;
		this.DatumRodjenja = DatumRodjenja;

		RacunanjeStarosti();
	}

	public void RacunanjeStarosti()	{
		LocalDate datum = LocalDate.now();
		_Starost = datum.getYear() - DatumRodjenja.getYear();

		if (datum.getMonthValue() < DatumRodjenja.getMonthValue()) {
			_Starost--;
		}

		if (datum.getMonthValue() == DatumRodjenja.getMonthValue() &&
		   datum.getDayOfMonth() <  DatumRodjenja.getDayOfMonth()) {
			_Starost--;
		}

	}

	public int citanjeStarosti() {
		return _Starost;
	}
}
		
    
    	
import datetime.datetime

class Osoba:

	def __init__(self, ime, prezime, datumRodjenja):
		self.ime           = ime
		self.prezime       = prezime
		self.datumRodjenja = datumRodjenja
		self.racunanjeStarosti()

	def racunanjeStarosti(self):
		datum         = datetime.datetime.now()
		self.__starost = datum.year - self.datumRodjenja.year

		if datum.month < self.datumRodjenja.month:
			self.__starost = self.__starost - 1

		if datum.month == self.datumRodjenja.month and
		   datum.day  <  self.datumRodjenja.day:
			self.__starost = self.__starost - 1

	def citanjeStarosti(self):
		return self.__starost
		
    
Slika 33. - Osnovna klasa Osoba, iz koje će u nastavku biti izvedena klasa Radnik.

Vidimo da klasa nema baš previše podataka, ali, čak i na ovakvom veoma jednostavnom primeru primećujemo da definisanje klase Radnik - koje bi podrazumevalo bukvalno "prepisivanje" prethodnog koda - ne deluje kao najpraktičnije rešenje.

Umesto "prepisivanja", klasa Radnik naslediće klasu Osoba i moći ćemo da koristimo polja i metode klase Osoba (u C#-u takođe i svojstva),

U svemu ćemo naravno biti u mogućnosti da klasi Radnik dodamo i nova polja i metode (i svojstva u C#-u).

C#
C++
Java
Python 3
		
public class Radnik : Osoba
{
	private String   _RadnoMesto;
	private DateTime _PocetakRada;
	private UInt32   _RadniStaz;

	public Radnik(String Ime, String Prezime,
	              String DatumRodjenja, String RadnoMesto,
	              DateTime PocetakRada) : base(Ime, Prezime, DatumRodjenja)
	{
		this._RadnoMesto    = RadnoMesto;
		this._DatumRodjenja = DatumRodjenja;
		this._PocetakRada   = PocetakRada;

		RacunanjeStaza();
	}

	public void RacunajeStaza()
	{
		_RadniStaz = DateTime.Now.Year - PocetakRada.Year;

		if (DateTime.Now.Month < PocetakRada.Month)
		{
			_RadniStaz--;
		}

		if (DateTime.Now.Month == PocetakRada.Month &&
		   DateTime.Now.Day   <  PocetakRada.Day)
		{
			_RadniStaz--;
		}
	}

	public UInt32 RadnoMesto
	{
		get
		{
			return _RadnoMesto;
		}
	}

	public UInt32 RadniStaz
	{
		get
		{
			return _RadniStaz;
		}
	}
}
		
	
		
class Radnik : public Osoba {

	private:

	Datum  _pocetakRada;
	string _pozicija; 
	int    _staz;

	public:

	Radnik(string ime, string prezime, Datum datumRodjenja, string pozicija, Datum pocetakRada) : Osoba(ime, prezime, datumRodjenja)
	{
		this->_pozicija    = pozicija;
		this->_pocetakRada = pocetakRada;
		racunanjeStaza();
	}

	racunanjeStaza()
	{
		time_t d     = time(NULL);
		tm*    datum = localtime(&d);

		int godina   = datum->tm_year + 1900;
		int mesec    = datum->tm_mon  + 1;
		int dan      = datum->tm_mday;

		_staz = godina - _pocetakRada.godina;

		if (mesec < _pocetakRada.mesec)
		{
			_staz--;
		}

		if (mesec == _pocetakRada.mesec &&
		   dan   <  _pocetakRada.dan)
		{
			_staz--;
		}
	}

	int citanjeStaza()
	{
		return _staz;
	}

	string citanjePozicije()
	{
		return _pozicija;
	}
};
		
	
		
public class Radnik extends Osoba {

	private  String    Pozicija;
	private  int       _Staz;
	public   LocalDate PocetakRada;

	public Radnik(String Ime, String Prezime, LocalDate DatumRodjenja, String Pozicija, LocalDate PocetakRada) {
		super(Ime, Prezime, DatumRodjenja);
		this.PocetakRada = PocetakRada;
		this.Pozicija    = Pozicija;
		RacunanjeStaza();
	}

	public void RacunanjeStaza( ) {
		LocalDate datum = LocalDate.now();
		_Staz = datum.getYear() - PocetakRada.getYear();

		if (datum.getMonthValue() < PocetakRada.getMonthValue()) {
			_Staz--;
		}

		if (datum.getMonthValue() == PocetakRada.getMonthValue() &&
		   datum.getDayOfMonth() <  PocetakRada.getDayOfMonth()) {
			_Staz--;
		}
	}

	public int citanjeStaza() {
		return _Staz;
	}

	public int citanjePozicije() {
		return _Pozicija;
	}
}
		
	
		
class Radnik(Osoba):

	def __init__(self, ime, prezime, datumRodjenja, radnoMesto, pocetakRada):
		self.radnoMesto  = radnoMesto
		self.pocetakRada = pocetakRada
		Osoba.__init__(self, ime, prezime, datumRodjenja)
		self.racunanjeRadnogStaza()

	def racunanjeRadnogStaza(self):
		datum            = datetime.datetime.now()
		self.__radniStaz = datum.year - self.pocetakRada.year

		if datum.month < self.pocetakRada.month:
			self.__radniStaz = self.__radniStaz - 1

		if datum.month == self.pocetakRada.month and
		   datum.day  <  self.pocetakRada.day:
			self.__radniStaz = self.__radniStaz - 1

	def citanjeStaza(self):
		return self.__radniStaz
		
    
Slika 34. - Klasa Radnik, koja nasleđuje klasu Osoba, to jest, preuzima njena polja, metode i svojstva, uz (naravno) mogućnost dodavanja novih.

Da pojasnimo (i ponovimo): kada napišemo ....

C#
C++
Java
Python 3
    	
public class Radnik : Osoba
{

}
		
    
    	
class Radnik : public Osoba
{

};
		
    
    	
public class Radnik extends Osoba {

}
		
    
    	
class Radnik(Osoba):
		
    
Slika 35. - Osnovni programski kod kojim se definiše nasleđivanje klase.

.... definisali smo klasu Radnik koja nasleđuje klasu Osoba, što znači da klasa Radnik ima sva polja, metode (i svojstva) koje ima klasa Osoba, pri čemu je moguće dodavati nova polja i nove metode, svojstvene (samo) klasi Radnik, kao na primer u konstruktoru ....

C#
C++
Java
Python 3
    	
public Radnik(String Ime, String Prezime, String DatumRodjenja,
              DateTime PocetakRada) : base(Ime, Prezime, DatumRodjenja)
{
	this._RadnoMesto    = RadnoMesto;
	this._DatumRodjenja = DatumRodjenja;
	this._PocetakRada   = PocetakRada;

	RacunanjeStaza();
}
		
    
    	
Radnik(string ime, string prezime, Datum datumRodjenja, string pozicija,
       Datum pocetakRada) : Osoba(ime, prezime, datumRodjenja)
{
	this->_pozicija    = pozicija;
	this->_pocetakRada = pocetakRada;
	racunanjeStaza();
}
		
    
    	
public Radnik(String Ime, String Prezime, String DatumRodjenja,
              String RadnoMesto, DateTime PocetakRada) {
	super(Ime, Prezime, DatumRodjenja);

	this._RadnoMesto    = RadnoMesto;
	this._PocetakRada   = PocetakRada;

	RacunanjeStaza();
}
		
    
    	
def __init__(self, ime, prezime, datumRodjenja, radnoMesto, pocetakRada):
	self.radnoMesto  = radnoMesto
	self.pocetakRada = pocetakRada
	Osoba.__init__(self, ime, prezime, datumRodjenja)
	self.racunanjeRadnogStaza()
		
    
Slika 36. - Konstruktor klase Radnik koji poziva konstruktor klase Osoba i takođe koristi sopstvena polja i pozive naredbi.

.... gde preko koda .....

C#
C++
Java
Python 3
    	
: base(Ime, Prezime, DatumRodjenja)
		
    
    	
: Osoba(Ime, Prezime, DatumRodjenja)
		
    
    	
super(Ime, Prezime, DatumRodjenja);
		
    
    	
Osoba.__init__(self, ime, prezime, datumRodjenja)
		
    
Slika 37. - Deo programskog koda kojim se konstruktor klase Radnik spaja sa konstruktorom klase Osoba.s

.... praktično pozivamo konstruktor klase Osoba, što podrazumeva inicijalizaciju tri navedena polja i pozivanje metode za računanje starosti, ali, budući da konstruktor klase Radnik sadrži i druge parametre i poziv metode za računanje radnog staža, vidimo da je povezivanje podataka koje smo ostvarili na ovaj način krajnje optimalno i praktično.

Polimorfizam

Sama reč polimorfizam (koja u prevodu označava "višeobličje"), u objektno orijentisanom programiranju označava da se osnovne klase i njihove izvedene klase u određenim okolnostima mogu koristiti ravnopravno, odnosno "istovremeno".

U određenim okolnostima, ali - ne i uvek.

Razmotrićemo jedan praktičan primer (koji koristi klase koje smo već definisali).

Recimo, smatraćemo da klase Osoba i Radnik koristimo za organizaciju podataka o radnicima sopstvene firme i članova njihove bliže familije:

  • podatke o radnicima ćemo beležiti preko klase Radnik
  • podatke o ostalim osobama ćemo beležiti preko klase Osoba

Sledeći zahtev je sastavljanje jedinstvene lista svih osoba, bez obzira na to da li je osoba radnik ili nije (smatramo da je, u konkretnoj implementaciji, najbolje da postoji samo jedna takva lista, zarad dobre organizacije i uštede prostora), pri čemu bismo takav spisak (na primer) mogli koristiti da u određenim okolnostima pronađemo:

  • decu mlađu od 7 godina (uoči Novogodišnjih praznika)
  • sve osobe starije od 55 godina (zarad organizacije karaoke večeri za sve seniore, bez obzira da li su radnici ili nisu)
  • sve žene (uoči 8. marta, radi dodele poklona, bez obzira da li su radnici ili nisu)

(Naslućujete sad da je to sve moguće izvesti upotrebom tehnika OOP-a i da će nam pomenuti polimorfizam u tome "nekako" već pomoći.)

Ako napravimo listu čiji su elementi objekti osnovne klase (Osoba) ....

C#
C++
Java
Python 3
    	
List<Osoba> SpisakOsoba = new List<Osoba>();
		
    
    	
list<Osoba> SpisakOsoba;
		
    
    	
List<Osoba> SpisakOsoba = new ArrayList<Osoba>();
		
    
    	
spisakOsoba = []
		
    
Slika 38. - Lista objekata klase Osoba. U ovakvu listu moguće je stavljati ne samo objekte klase Osoba, već i objekte izvedenih klasa.

.... takva lista moći će da prima objekte klase Osoba, ali i objekte klase Radnik.

Na primer, sledeći kod ....

C#
C++
Java
Python 3
    	
Osoba osoba   = new Osoba("Dejana", "Marković", "1970-05-27");
SpisakOsoba.Add(osoba);

Radnik radnik = new Radnik("Petar", "Marković", "1969-11-21",
                           "Upravnik magacina", "1997-01-05");
SpisakOsoba.Add(radnik);
		
    
    	
Datum datumRodjenja;
Datum pocetakRada;

datumRodjenja.godina = 1970;
datumRodjenja.mesec  = 5;
datumRodjenja.dan    = 27;

Osoba osoba = Osoba("Dejana", "Marković", datumRodjenja);
SpisakOsoba.push_back(osoba);

datumRodjenja.godina = 1969;
datumRodjenja.mesec  = 11;
datumRodjenja.dan    = 21;

pocetakRada.godina = 1997;
pocetakRada.mesec  = 1;
pocetakRada.dan    = 5;

Radnik radnik = new Radnik("Petar", "Marković", datumRodjenja,
                           "Upravnik magacina", pocetakRada);
SpisakOsoba.push_back(radnik);
		
    
    	
LocalDate datumRodjenja1 = LocalDate.of(1970, 5, 27);
Osoba osoba              = new Osoba("Dejana", "Marković", datumRodjenja1);
SpisakOsoba.Add(osoba);

LocalDate datumRodjenja2 = LocalDate.of(1969, 11, 21);
LocalDate pocetakRada2   = LocalDate.of(1997, 1, 5);
Radnik radnik = new Radnik("Petar", "Marković", datumRodjenja2,
                           "Upravnik magacina", pocetakRada2);
SpisakOsoba.Add(radnik);
		
    
    	
datumRodjenja = datetime.datetime(1970, 5, 27)
osoba = Osoba("Dejana", "Marković", datumRodjenja)
spisakOsoba.append(osoba)


datumRodjenja = datetime.datetime(1969, 11, 21)
pocetakRada   = datetime.datetime(1997, 1, 5)
radnik        = Radnik("Petar", "Marković", datumRodjenja,
                     "Upravnik magacina", pocetakRada)
spisakOsoba.append(radnik)
		
    
Slika 39. - Primer dodavanja objekata osnovne klase (Osoba) i izvedene klase (Radnik) u listu.

.... izvršavaće se bez problema.

Završićemo uz napomenu da je i "obrnuti" pristup moguć (kreiranje liste objekata klase Radnik na koju bismo dodavali i objekte klase Osoba), ali - u tom slučaju moramo biti vrlo pažljivi (ako već uopšte moramo da koristimo takav pristup).

Recimo, sledeći kod ....

C#
C++
Java
Python 3
    	
Osoba O = SpisakOsoba.Peek();
		
    
    	
Osoba O = SpisakOsoba.front();
		
    
    	
Osoba O = SpisakOsoba.peek();
		
    
    	
O = spisakOsoba[len(spisakOsoba) - 1]
		
    
Slika 40. - Preuzimanje reference na objekat iz liste: budući da se preuzeti objekat tretira kao objekat klase Osoba, nad njim je moguće primenjivati sve operacije.

.... daje referencu na objekat sa kojim možemo postupati na bilo koji (mogući) način, u tom smislu da objekat koji referenciramo garantovano ima sva polja, metode i svojstva koje bi nam moglo pasti na pamet da koristimo.

U "obrnutim" okolnostima (smatraćemo da smo kreirali listu objekata klase Radnik) ....

C#
C++
Java
Python 3
    	
Radnik R = SpisakRadnika.Peek();
		
    
    	
Radnik R = SpisakRadnika.front();
		
    
    	
Radnik R = SpisakRadnika.peek();
		
    
    	
R = spisakRadnika[len(spisakRadnika) - 1]
		
    
Slika 41. - Preuzimanje reference na objekat iz liste: budući da se preuzeti objekat tretira kao objekat klase Radnik, moramo biti jako pažljivi i (zapravo) ne smemo pozivati metode koje nisu svojstvene klasi Osoba - da ne bi dolazilo do grešaka.

.... može doći do problema ukoliko preko reference R referenciramo objekat klase Osoba i pri tom (recimo) pozivamo neko od svojstava klase Radnik (na primer,RadniStaz).

Ukoliko se ograničimo na polja/metode/svojstva koja su zajednička za obe klase (ili, za sve izvedene klase, u komplikovanijim slučajevima nasleđivanja), stvari će funkcionisati bez "ispada".

Primer za kraj ....

Iako deluje pomalo nepotrebno posle svega što smo naveli, vratićemo se još jednom na pravougaonike i pogledati primer jednostavne procedure preko koje želimo da ustanovimo koji od dva pravougaonika ima veću površinu: u prvom slučaju, preko klase Pravougaonik i objekata, a u drugom slučaju, uz korišćenje proceduralnog pristupa (gde se pravougaonici definišu preko zasebnih promenljivih):

Prvo, OOP pristup:

C#
C++
Java
Python 3
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
		
    
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
		
    
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
		
    
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3]
		
    
Slika 42. - Kreiranje niza objekata klase pravougaonik.

Ako je potrebno da pronađemo veći među prva dva pravougaonika, ceo kod ima sledeći oblik:

C#
C++
Java
Python 3
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonici[0] = new Pravougaonik(10.5, 12.4);
Pravougaonici[1] = new Pravougaonik(21.3, 7.2);
Pravougaonici[2] = new Pravougaonik(4.5,  22.6);

if (Pravougaonici[0].P > Pravougaonici[1].P)
{
	Console.WriteLine("Prvi pravougaonik ima veću površinu.");
}
else
{
	Console.WriteLine("Drugi pravougaonik ima veću površinu.");
}
		
    
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonici[0] = new Pravougaonik(10.5, 12.4);
Pravougaonici[1] = new Pravougaonik(21.3, 7.2);
Pravougaonici[2] = new Pravougaonik(4.5,  22.6);

if (Pravougaonici[0].P > Pravougaonici[1].P)
{
	cout << "Prvi pravougaonik ima veću površinu." << endl;
}
else
{
	cout << "Drugi pravougaonik ima veću površinu." << endl;
}
		
    
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonici[0] = new Pravougaonik(10.5, 12.4);
Pravougaonici[1] = new Pravougaonik(21.3, 7.2);
Pravougaonici[2] = new Pravougaonik(4.5,  22.6);

if (Pravougaonici[0].P > Pravougaonici[1].P) {
	System.out.printf("Prvi pravougaonik ima veću površinu.");
}
else {
	System.out.printf("Drugi pravougaonik ima veću površinu.");
}
		
    
    	
Pravougaonici = [3];
Pravougaonici.append(Pravougaonik(10.5, 12.4))
Pravougaonici.append(Pravougaonik(21.3, 7.2))
Pravougaonici.append(Pravougaonik(4.5,  22.6))

if Pravougaonici[0].P > Pravougaonici[1].P:
	print("Prvi pravougaonik ima veću površinu.")
else:
	print("Drugi pravougaonik ima veću površinu.")
		
    
Slika 43. - Zadatak pronalaženja većeg od prva dva pravougaonika rešen metodom OOP-a.

Za poređenje, isti program, bez klasa i objekata:

C#
C++
Java
Python 3
    	
Double P1_a = 10.5, P1_b = 12.4;
Double P2_a = 21.3, P2_b = 7.2;
Double P3_a = 4.5,  P3_b = 22.6;

Double P1_O = 2 * (P1_a + P1_b);
Double P2_O = 2 * (P2_a + P2_b);
Double P3_O = 2 * (P3_a + P3_b);

Double P1_P = P1_a * P1_b;
Double P2_P = P2_a * P2_b;
Double P3_P = P3_a * P3_b;

if (P1_P > P2_P)
{
	Console.WriteLine("Prvi pravougaonik ima veću površinu.");
}
else
{
	Console.WriteLine("Drugi pravougaonik ima veću površinu.");
}
		
    
    	
Double P1_a = 10.5, P1_b = 12.4;
Double P2_a = 21.3, P2_b = 7.2;
Double P3_a = 4.5,  P3_b = 22.6;

Double P1_O = 2 * (P1_a + P1_b);
Double P2_O = 2 * (P2_a + P2_b);
Double P3_O = 2 * (P3_a + P3_b);

Double P1_P = P1_a * P1_b;
Double P2_P = P2_a * P2_b;
Double P3_P = P3_a * P3_b;

if (P1_P > P2_P)
{
	cout << "Prvi pravougaonik ima veću površinu.") << endl;
}
else
{
	cout << "Drugi pravougaonik ima veću površinu.") << endl;
}
		
    
    	
double P1_a = 10.5, P1_b = 12.4;
double P2_a = 21.3, P2_b = 7.2;
double P3_a = 4.5,  P3_b = 22.6;

double P1_O = 2 * (P1_a + P1_b);
double P2_O = 2 * (P2_a + P2_b);
double P3_O = 2 * (P3_a + P3_b);

double P1_P = P1_a * P1_b;
double P2_P = P2_a * P2_b;
double P3_P = P3_a * P3_b;

if (P1_P > P2_P) {
	System.out.printf("Prvi pravougaonik ima veću površinu.");
}
else {
	System.out.printf("Drugi pravougaonik ima veću površinu.");
}
		
    
    	
P1_a = 10.5
P1_b = 12.4

P2_a = 21.3
P2_b = 7.2

P3_a = 4.5
P3_b = 22.6

P1_O = 2 * (P1_a + P1_b)
P2_O = 2 * (P2_a + P2_b)
P3_O = 2 * (P3_a + P3_b)

P1_P = P1_a * P1_b
P2_P = P2_a * P2_b
P3_P = P3_a * P3_b

if P1_P > P2_P:
	print("Prvi pravougaonik ima veću površinu.")
else:
	print("Drugi pravougaonik ima veću površinu.")
		
    
Slika 44. - Zadatak pronalaženja većeg od prva dva pravougaonika rešen upotrebom zasebnih promenljivih.

Razlika nije drastična na ovako malom i jednostavnom primeru (a veći i kompleksniji ipak nismo želeli da pišemo, zarad očuvanja preglednosti), ali, nije teško uvideti koliko je programski kod u OOP primeru pregledniji.

Ali, setimo se ovde i primedbe iz uvodnog pasusa o tome da OOP pristup ne treba koristiti uvek i bez potrebe i da je proceduralno programiranje (u nekim drugim okolnostima), takođe krajnje dobar i primeren način rešavanja problema (neretko i bolji).

Jednostavno, kao što u spoljnoj stvarnosti možemo od Beograda do Novog Sada putovati biciklom ili automobilom, dok bismo do Australije ipak radije putovali avionom, tako i u programiranju nećemo OOP pristup koristiti "za svaku sitnicu", već samo onda kada zatreba - a sada znamo i kako.

Autor članka Nikola Vukićević Za web portal www.codeblog.rs
Napomena: Tekstovi, slike, web aplikacije i svi ostali sadržaji na sajtu www.codeblog.rs (osim u slučajevima gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta www.codeblog.rs i zabranjeno je njihovo korišćenje na drugim sajtovima i štampanim medijima, kao i bilo kakvo drugo korišćenje u komercijalne svrhe, bez eksplicitnog pismenog odobrenja autora.
© 2020-2023. Sva prava zadržana.
Viber
početna Početna > Članci > Uvod u objektno orijentisano programiranje

Info & povezani članci Info o članku - dugme

Info

trejler_sat Datum objave: 04.03.2020.

trejler_sat Poslednja izmena: ----

trejler_dokument Jezici: C#
C++
Java
Python

trejler_teg_crveni Težina: 8/10

Povezani članci

Dinamičko programiranje Strukture podataka Postfiksna notacija - kako računari računaju? Binarno stablo pretrage AVL Stablo - Implementacija BFS i DFS - Pronalaženje putanje kroz lavirint Visinski balansirano (AVL) stablo Klase složenosti algoritama - (O notacija) Uvod u relacione baze podataka i SQL Shunting Yard - Implementacija Ostali članci
In C++ it’s harder to shoot yourself in the foot, but when you do, you blow off your whole leg.
Bjarne Stroustrup
codeBlog codeBlog
Projekat posvećen popularizaciji kulture i veštine programiranja.
Napomena: Tekstovi i slike na sajtu www.codeblog.rs (osim u slučajevima, gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta www.codeblog.rs i zabranjeno je njihovo korišćenje na drugim sajtovima i štampanim medijima, kao i bilo kakvo drugo korišćenje u komercijalne svrhe, bez eksplicitnog odobrenja autora.
© 2020-2023. Sva prava zadržana.
Facebook - logo
Instagram - logo
LinkedIn - logo
Twitter - logo
E-mail
Naslovna
   •
Uslovi korišćenja
   •
Obaveštenja
   •
FAQ
   •
Kontakt