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 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.
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. :)
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.
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.
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.
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! :)");
}
}
.... 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! :)")
(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. **
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.
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)
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):

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.
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.

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.
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.

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:
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
Četiri polja klase Pravougaonik
(četiri promenljive tipa Double
) beleže stranice pravougaonika (a
i b
), kao i obim (O
) i površinu (P
).
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 ....
Pravougaonik P1 = new Pravougaonik();
Pravougaonik P1;
Pravougaonik P1 = new Pravougaonik();
P1 = Pravougaonik()
.... kreirali smo objekat P1
, posle čega možemo pristupiti njegovim poljima:
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))
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.
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.
Š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
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
....
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;
.... posle čega možemo zadati početno stanje objekta na sledeći način:
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)
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).
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:
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
Sada će (konačno), pri sledećem pozivu ....
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)
.... 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 ....
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))
.... dobićemo sledeći ispis:
-Stranica a: 12.55
-Stranica b: 10.40
-Obim: 45.90
-Površina: 130.52
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 ....
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))
.... i pozvati je na sledeći način (koji je svakako nešto elegantniji):
P1.KonzolniIspis();
P1.KonzolniIspis();
P1.KonzolniIspis();
P1.KonzolniIspis()
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.
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)
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....
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()
.... 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
.
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 ....
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
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).
Potrebno je dakle da postignemo dve stvari:
- da se obim i površina automatski ažuriraju pri promeni stranica
a
ib
- 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 kodapublic
- 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.)
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:
private Double a, b, O, P;
private:
double a, b, O, P;
private double a, b, O, P;
__a, __b, __O, __P
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.
Pristup privatnim poljima preko javnih metoda
Javna metoda za čitanje vrednosti privatnog polja ima sledeći oblik:
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
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:
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()
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:

Objekat:

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.
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):
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
Da pojasnimo kako funkcioniše kod koji smo videli: deo koda ....
public Double a
{
}
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
.... 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
....
get
{
return _a
}
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
.... 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 ....
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
.... 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
....
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
.... 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.
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
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:
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
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 suIme
,Prezime
,DatumRodjenja
iStarost
- 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
iRadniStaz
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
:
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
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).
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
Da pojasnimo (i ponovimo): kada napišemo ....
public class Radnik : Osoba
{
}
class Radnik : public Osoba
{
};
public class Radnik extends Osoba {
}
class Radnik(Osoba):
.... 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 ....
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()
.... gde preko koda .....
: base(Ime, Prezime, DatumRodjenja)
: Osoba(Ime, Prezime, DatumRodjenja)
super(Ime, Prezime, DatumRodjenja);
Osoba.__init__(self, ime, prezime, datumRodjenja)
.... 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".
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
) ....
List<Osoba> SpisakOsoba = new List<Osoba>();
list<Osoba> SpisakOsoba;
List<Osoba> SpisakOsoba = new ArrayList<Osoba>();
spisakOsoba = []
.... takva lista moći će da prima objekte klase Osoba
, ali i objekte klase Radnik
.
Na primer, sledeći kod ....
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)
.... 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 ....
Osoba O = SpisakOsoba.Peek();
Osoba O = SpisakOsoba.front();
Osoba O = SpisakOsoba.peek();
O = spisakOsoba[len(spisakOsoba) - 1]
.... 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
) ....
Radnik R = SpisakRadnika.Peek();
Radnik R = SpisakRadnika.front();
Radnik R = SpisakRadnika.peek();
R = spisakRadnika[len(spisakRadnika) - 1]
.... 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:
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonik[] Pravougaonici = new Pravougaonik[3]
Ako je potrebno da pronađemo veći među prva dva pravougaonika, ceo kod ima sledeći oblik:
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.")
Za poređenje, isti program, bez klasa i objekata:
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.")
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.