Uvod
Objektno orijentisano programiranje je jedna od paradigmi programiranja, to jest, jedan od pristupa u projektovanju računarskih programa, koji podrazumeva da se problemi rešavaju kreiranjem i međusobnom interakcijom "objekata":
- objekti se mogu shvatiti kao funkcionalne 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)
- klasa predstavlja programski kod preko koga se definišu moguće odlike budućih objekata
Može se reći da simulacije objekata iz spoljnjeg sveta u računarskim sistemima (razne simulacije vožnje, letenja i sl), predstavljaju verovatno najočigledniji i najrazumljiviji primer objektno orijentisanog pristupa, ali, svakako ne i jedini.
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, i stoga ćemo se potruditi da u nastavku predočimo sve što je neophodno za početno razumevanje.
Još nekoliko uvodnih napomena ....
Za uspešan početak bavljenja 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, pre nego što pređemo na osnovne pojmove objektno orijentisanog programiranja i primere, prvo ćemo - "za svaki slučaj" - prodiskutovati o razlikama između proceduralnog i objektno orijentisanog pristupa, a iznećemo i kraći istorijat OOP-a (uz osvrt na to, koje bi jezike trebalo koristiti, a koje izbegavati, pri početnom upoznavanju sa metodologijom objektno orijentisanog programiranja) ....
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" (eng. top-down), koji podrazumeva da se problem prvo sagledava u celosti, a zatim (po potrebi), razbija na potprobleme.
Navedeni pristup (čitaocima poznat od ranije), 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).
U takvim situacijama, umesto top-down pristupa, tipično se koristi 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 za međusobno povezivanje.
Na kraju, celokupan sistem nastaje spajanjem i interakcijom pojedinačnih elemenata, i u pitanju je takozvani pristup "odozdo-nagore" (eng. 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 odgovaraju poznatim zakonima fizike iz spoljnjeg sveta (gravitacija, inercija, trenje, detekcija sudara i sl), čime se na kraju kreira funkcionalan sistem.
Ako bismo pokušali da navedeni problem simulacije sagledamo "odjednom" (da primenimo pristup 'odozgo-nadole') .... verovatno bismo izazvali "blago" preopterećenje sopstvenog aparata za razmišljanje. :)
Pretpostavljamo da je jasno, da pravilno i 'glatko' funkcionisanje prethodno opisanog sistema, u praktičnom smislu (veoma (!)) zavisi i od raspoloživih računarskih resursa (od toga da li ima dovoljno memorije za smeštaj podataka, da li procesor može da pokreće simulaciju dovoljnom brzinom i sl), međutim, slični zahtevi (naravno) važe i za "ne-OOP" programe (hardverski zahtevi postoje uvek i, blago uprošćeno, može se reći da ne zavise od izbora metodologije za projektovanje softvera).
Kada koristiti proceduralni pristup, a kada objektno orijentisani
U simulaciji vožnje, na prirodan način smo prepoznavali računarske objekte koji nastaju po uzoru na objekte iz spoljnjeg sveta, ali, sa nekim drugim algoritamskim problemima, to možda 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" (naravno, važi i obrnuto), već je samo u određenoj situaciji jedan od dva navedena pristupa primereniji, i oba pristupa - 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, pri čemu je potrebno voditi računa o dobrom dizajnu klasa, čitljivosti koda i održivosti na duže staze.
Naravno, ima i projekata manjeg obima čija je struktura kompleksna i za koje je primereno koristiti OOP pristup, međutim (da ponovimo), za mnoge manje programe jednostavne strukture (kao i pomoćne skripte i sličan softver), objektno orijentisani pristup nije obavezan, i proceduralni pristup će poslužiti sasvim dobro.
Ali, da bismo dodatno 'zakomplikovali priču', osvrnućemo se na još nekoliko situacija ....
Sa jedne strane, u praksi postoji i veći broj proceduralnih programa 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, vrlo bitni u određenom informacionom sistemu, ali - ipak ne zahtevaju OOP pristup.
Sa druge strane, mnogi naoko obimni programi napisani preko OOP tehnika, zapravo su relativno jednostavni i "rutinski" (na primer, mnoge aplikacije koje se povezuju sa bazama podataka, zarad pregleda prometa u maloprodaji i sl), pri čemu naravno, nema ničeg 'pogrešnog' u tome što je 'tako kako smo naveli'.
Situacija zaista ima raznih. :)
U svakom slučaju, u iole razumnim okolnostima (i pošto steknete dovoljno iskustva), bićete u stanju da razaznate zašto neko "jeste ili nije" izabrao OOP ili proceduralni pristup i takođe, 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 zapišu na pregledniji način, i učine jednostavnijim za održavanje.
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 koji je još na početku (prva verzija pojavila se 1961), imao većinu odlika današnjih objektno orijentisanih programskih jezika.
Ipak, skromni kapaciteti računara iz šezdesetih i sedamdesetih godina, pomalo su kočili pravi napredak OOP pristupa (zapravo, više nego "pomalo"), 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, 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, a u opštem smislu (kao što smo, na neki način, već nagovestili), Java se može smatrati "najočiglednijim" primerom jezika koji je nedvosmisleno usmeren na objektno orijentisani pristup, i stoga ćemo osobenostima programskog jezika Java posvetiti još koji red.
Ako C++ (na primer), dozvoljava da se proceduralni pristup koristi samostalno ili u kombinaciji sa OOP pristupom (a ima i drugih jezika koji nude slične 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 takođe i dugački, deskriptivni nazivi klasa i pripadajućih 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 se prethodni kod uporedi 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. :)
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 drugih jezika koji takođe 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 (kao što je već pomenuto), tokom vremena - a pogotovo uporedo sa ekspanzijom Jave i pod uticajem Jave, OOP pristup je (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 glavni jezik koji će u članku biti korišćen za primere - izabrali smo C#, s tim da pružamo čitaocima mogućnost da lako promene jezik po želji (opcije su: C#, C++, Java i Python).
C# je jezik koji je, poput Jave, usmeren na klase, ali, rekli bismo ne baš u istoj meri, a osim toga, C# je ponešto fleksibilniji, opšti je utisak da sintaksa C#-a deluje preglednije od sintakse drugih popularnih OOP jezika - i upravo zato smatramo da je C# najprikladniji jezik za početno upoznavanje sa OOP pristupom.
Python je svakako malo "ležerniji" po pitanju implementacije OOP tehnika, ali ipak će biti prikazani i primeri iz Python-a ("da pomenemo još koji jezik"), dok je JavaScript znatno ležerniji, i stoga ćemo primere iz JS-a 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 osobinama OOP-a i sa time kako je i zašto OOP 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 tehnike o kojima pišemo, funkcionišu u praksi.
Osnovni pojmovi na kojima se temelji objektno orijentisano programiranje su:
- klasa
- objekat
- apstrakcija podataka
- enkapsulacija
- nasleđivanje
- polimorfizam
Svakom od navedenih pojmova, biće posvećen zaseban odeljak u nastavku.
Klase, objekti i apstrakcija podataka
U idejnom smislu, klasa u objektno orijentisanom programiranju predstavlja obrazac po kome se kreiraju (budući) objekti.
U praktičnom smislu, u pitanju je programski kod koji je tipično, ali ne i obavezno (videti napomenu ispod), zapisan u zasebnoj datoteci, i sadrži sve elemente koji opisuju moguća stanja i moguće oblike ponašanja 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.
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 na samo one odlike koje su bitne za izvršavanje programa.
Na primer, u programu za poslovnu administraciju, možemo zanemariti većinu prethodno navedenih svojstava i ostaviti samo: datum rođenja, adresu stanovanja i podatke o učinku na radnom mestu.
Ukratko, pri dizajniranju klase, potrebno je uskladiti prethodno navedene smernice (kao i mnoge druge detalje o kojima će tek biti reči), to jest, potrebno je izabrati i opisati:
- koji podaci će biti predstavljeni
- kako će biti predstavljeni
- kako će podaci biti međusobno povezani
- mehanizme za obradu podataka
Odnos između klase i objekta
Da bismo ilustrovali sve što smo do sad naveli (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 pominjali, ali, sasvim ilustrativan):

U smislu apstrakcije podataka, izabrali smo samo ono što je neophodno: opštim matematičkim svojstvima pravilnih mnogouglova, dodali smo svojstva koja se koriste za prikaz 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
Pojam polja (klase) u objektno orijentisanom programiranju, označava pojedinačni imenovani podatak koji je zapisan unutar klase, i takav podatak 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 navedeni podaci zajedno, kao što smo već nagovestili ranije, opisuju stanje (budućih) objekata.

U opštem smislu, 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 broj 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 kojim 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 posvetićemo mnogo više pažnje u nastavku (pošto se osvrnemo na preostale osnovne odlike klasa).
Metode - obrada podataka
Ponašanje objekata definiše se preko metoda - funkcija koje su zapisane direktno unutar tela klase.

Po svojim spoljnim odlikama, metode se gotovo u potpunosti poklapaju sa funkcijama sa kakvima ste se mogli susresti u C-u (ili nekom sličnom jeziku): pri pozivu funkcije/metode, predaju se argumenti, funkcije/metode imaju (ili nemaju) povratnu vrednost i sl.
Osim navedenih odlika, bitno je napomenuti da metode klase mogu direktno pristupati poljima klase.
Da bismo mogli da za prave razumemo kako se odvija interakcija između polja i metoda (i pre svega, da bismo što bolje mogli da sagledamo kako klasa dobija željene odlike, a 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, međutim, primer koji ćemo koristiti za početno 'uhodavanje', biće ipak (kako i dolikuje), mnogo jednostavniji: definisaćemo klasu koja opisuje pravougaonik(e).
Primer je naizgled jednostavan (i ne samo naizgled), ali je sasvim adekvatan i u praktičnom smislu.
Klasa Pravougaonik
sadržaće polja a
, b
, O
i P
(podatke o stranicama pravougaonika, obimu i površini), metode preko kojih se obim i površina računaju i 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đusobnim vezama koje odgovaraju pravilima geometrije (a da li će se to dešavati "samo od sebe" - videćemo u nastavku, mada, verujemo da već znate odgovor) ....
Definisaćemo prvo najosnovnije okvire klase (tj. dodaćemo samo osnovne podatke), pa stoga klasa na početku ima 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
Preko četiri polja klase Pravougaonik
(četiri promenljive tipa Double
), beleže se stranice pravougaonika (a
i b
), kao i obim (O
) i površina (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, klasa se već može 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 objekti kreiraju (tj. 'instanciraju')
- zašto je neophodno da veze među poljima budu pravilno uspostavljene (kroz postupak 'enkapsulacije')
Kreiranje objekta (rezervisana reč new)
Pozivanjem sledećeg (vrlo jednostavnog) koda ....
Pravougaonik P1 = new Pravougaonik();
Pravougaonik P1 = Pravougaonik();
// Može i (samo):
// Pravougaonik P1;
// ali, u tom slučaju, polja neće (obavezno)
// biti inicijalizovana nulama
Pravougaonik P1 = new Pravougaonik();
P1 = Pravougaonik()
.... kreira se objekat P1
, posle čega se može pristupati poljima objekta:
P1.a = 12.55;
P1.b = 10.40;
Console.WriteLine(P1.a);
Console.WriteLine(P1.b);
P1.a = 12.55;
P1.b = 10.40;
std::cout << P1.a;
std::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 "strašno" (ni iz daleka), 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
i druge brojčane tipove), 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 se inicijalizuju stranice pravougaonika - ali ne i obim i površina - 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 su, u primeru koji koristimo, stranice inicijalizovane naknadno, a 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 "popravimo stanje", uvođenjem metoda koje će regulisati stanje različitih polja.
Prvo ćemo se pobrinuti (iako to neće rešiti sve probleme), da bar početno stanje objekta bude korektno.
U kontekstu proceduralnog programiranja, inicijalizacija promenljivih je jedna od najbitnijih operacija i (kao što znamo), u pitanju je postupak preko koga se promenljivama zadaje početna (tj. "inicijalna") vrednost. 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 - i stoga će inicijalizacija polja biti prvo na šta ćemo obratiti pažnju u nastavku.
Jasno je da, za računanje (tj. ažuriranje) obima i površine, moramo imati na raspolaganju odgovarajuće metode i jasno je da se objekat ne može inicijalizovati prostom naredbom dodele (kao obična promenljiva), već pozivanjem posebne metode.
Konstruktor - metoda za definisanje početnog stanja objekta
Konstruktor je specijalizovana metoda preko koje 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 (u većini C-olikih jezika), konstruktor se 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 se početno stanje objekta može zadati na sledeći način:
Pravougaonik P1 = new Pravougaonik(12.55, 10.40);
Pravougaonik P1 = Pravougaonik(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, sa objektom je sve (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 "same od sebe").
Uvođenjem dve nove metode (za računanje obima i za računanje površine), koje će potom biti pozvane iz konstruktora - problem će biti rešen.
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 = Pravougaonik(12.55, 10.40);
Pravougaonik P = new Pravougaonik(12.55, 10.40);
P = new Pravougaonik(12.55, 10.40)
.... objekat biti korektno inicijalizovan.
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());
std::cout << "-Stranica a: " << P1.a;
std::cout << "-Stranica b: " << P1.b;
std::cout << "-Obim: " << P1.O;
std::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()
{
std::cout << "-Stranica a: " << this.a << std::endl;
std::cout << "-Stranica b: " << this.b << std::endl;
std::cout << "-Obim: " << this.O << std::endl;
std::cout << "-Površina: " << this.P << std::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 od navođenja zasebnih instrukcija za ispis):
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 OOP tehnika za obavljanje "sigurnosnih provera" preko kojih se rešavaju prethodno navedeni problemi, nosi naziv enkapsulacija, i upravo će enkapsulacija biti glavna tema sledećeg poglavlja, ali - pre nego što se posvetimo enkapsulaciji - napravićemo i kratak osvrt 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, u situacijama kada parametri konstruktora imaju iste nazive kao polja klase.
Da pojasnimo ....
Kada je objekat (već) kreiran, poljima se pristupa uz navođenje identifikatora objekta i polja, pri čemu su identifikatori spojeni preko operatora .
(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
, koristi se 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 rezervisana reč self), pravi se razlika između polja klase (this.a
), i parametra konstruktora (a
).
Enkapsulacija
Enkapsulacija (kao što smo već nagovestili), predstavlja skup tehnika preko kojih se:
- definišu pravila za pristup poljima i metodama klase
- uspostavljaju prikladne veze 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 (kvačilo, gas 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, već smo se upoznali u velikoj meri - na posredan način - time što smo posmatrali delove koda koji ne koriste enkapsulaciju (što je bila prilika da bar naslutimo "kako bi trebalo da bude").
U nastavku, sledi zvanično upoznavanje sa postupkom enkapsulacije, a počinjemo upravo od primera sa kakvima smo se već sretali ....
Šta se dešava ukoliko ne koristimo enkapsulaciju?
Ako se setimo naredbe P1.a = 12.55;
kojom smo (naknadno) 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, naredbe kakve smo videli (ako ih izvršimo), ponovo će poremetiti objekat: obim postaje mnogo veći nego što bi trebalo, a površina je negativan broj.
Međutim, u prethodnoj situaciji, pre svega se moramo zapitati - da li uopšte treba da postoji mogućnost direktnog zadavanja vrednosti obima i površine?!
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" (prema matematici - beskonačno mnogo, a u praktičnoj implementaciji na računaru, iako broj nije beskonačan, kombinacija ima izrazito mnogo).
Preko postupaka koji će biti prikazani u nastavku, rešavaju se sledeća dva problema:
- potrebno je postići da se obim i površina automatski ažuriraju pri promeni stranica
a
ib
- potrebno je da mogućnost ručnog upisivanja vrednosti obima i površine bude ukinuta - ali da pri tom 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 potruditi da ih u nastavku 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), i stoga ćemo konačno biti u stanju da "doteramo" 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, ali i preko nasleđenih klasa (o nasleđivanju ćemo govoriti u nastavku), međutim, nije moguće pristupati im iz spoljnih delova koda
Najviše pažnje, biće posvećeno specifikatorima pristupa private
i public
(sa kojima se programeri najčešće i sreću).
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 takve "otvorenosti" je to što se poljima može pristupati neposredno, a loša strana, to što mogućnost direktne izmene polja koja imaju specifikator pristupa public
- ostavlja prostora za naredbe dodele koje mogu, bar u najgorem slučaju, dovesti u pitanje korektnost samog objekta.
(Kao recimo malopre, kada smo upisali da je površina pravougaonika -19.25, što nije matematički moguće, pa smo takvom naredbom dodele praktično obesmislili ceo objekat.)
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, pošto smo polja a
, b
, O
i P
proglasili privatnim elementima klase, spoljni delovi koda više im ne mogu pristupati.
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
, 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).
Budući da smo za primer izabrali tipičan slučaj projektovanja klase, * preduzećemo korake da omogućimo pristup poljima a
, b
, O
i P
- na adekvatan način.
Za početak, pogledaćemo u sledećem odeljku, na koji način se privatnim poljima može pristupati preko javnih metoda.
Pristup privatnim poljima preko javnih metoda
Javna metoda za čitanje vrednosti privatnog polja (u primeru koji koristimo), može se definisati na sledeći način:
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 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 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 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 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()
Uz pristup koji smo izabrali, efikasno smo rešili dva problema:
- ulazni podatak se ne prihvata bezuslovno - već se proverava (na primer, 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 - obavlja se automatski
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 (objekat takođe sadrži sve metode koje sadrži i klasa).
Na slici je (preko konkretnog primera), prikazan opšti princip o kome smo ranije pisali: objekat "ispod haube" sadrži 'sve što treba', ali se direktno može 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 je, nažalost, u pitanju pristup koji nije dostupan u ostalim jezicima koje koristimo za primere).
Enkapsulacija preko akcesora u programskom jeziku C# (get i set)
Videli smo da je pristup privatnim poljima moguć preko javnih (public
) metoda, i da je glavni razlog zašto uopšte skrivamo polja klase, to što postoje situacije u kojima su vrednosti određenih polja uslovljene vrednostima drugih polja.
U opštem smislu:
- u određenim situacijama je potrebno očuvati veze između podataka (obim i površina pravougaonika zavise od vrednosti stranica)
- u drugim situacijama, potrebno je 'zaštititi' sadržaj samih polja (vrednost stranice pravougaonika ne sme biti negativan broj ili nula)
Pristup koji smo opisali u prethodnom odeljku može se koristiti u većini programskih jezika koji podržavaju OOP (praktično: u svim OOP jezicima koji su nam 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 ("pristupačima"), posebnim odeljcima u kojima su zapisana pravila za pristup skrivenim poljima (u smislu mogućnosti čitanja i upisa).
Princip definisanja svojstava u C#, u suštinskom smislu se gotovo nimalo ne razlikuje od upotrebe javnih metoda za pristup privatnim poljima, ali - zapis je nešto elegantniji.
Za upoznavanje sa svojstvima, koristićemo dosadašnji primer (pri čemu ćemo napraviti i izvesne (manje) izmene, i prikazaćemo 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 // Novo "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: sledeći 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 svojstva a
je public
), i unutar zagrada su definisana dva bloka.
Preko prvog bloka, 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
.... definisano je da će programski kod koji zatraži vrednost svojstva a
, dobiti (zapravo) 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
(pozivanjem svojstva P1.a
dobijamo vrednost polja P1._a
, a zatim se poziva opšta funkcija ToString()
preko koje se ispisuje vrednost).
Preko drugog bloka, 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đeno je da će polje _a
dobiti predatu vrednost (value
), samo u slučaju ako je predata vrednost veća od nule.
Pre svega, primećuje se sličnost sa pristupom iz prethodnog odeljka (gde smo koristili metodu Upis_a
), a verujemo da pažnju čitalaca najviše privlači rezervisana reč value
(koja može delovati pomalo "zbunjujuće").
Posmatrajmo to ovako (postavimo sebi pitanje): pod kojim okolnostima se 'sadržaj' određenog polja može promeniti?
Odgovor je - preko naredbe dodele, a naredba dodele sadrži: sa jedne strane, 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, može se primetiti da je svojstvo (u idejnom smislu), svojevrsna kombinacija polja i metode: spolja gledano (kada mu se pristupa iz spoljnih delova koda), svojstvo 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 to, da sada - kada se spoljnim delovima koda "otkrivaju" svojstva koja nose prethodne nazive polja (a
, b
, O
i P
) - za sama polja se koriste nazivi sa prefiksom: _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, kao i preopterećivanjem metoda i preklapanjem operatora.
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: *
- 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 racionalniji 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 previše podataka, ali, čak i na ovakvom 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
, čime će praktično biti omogućeno korišćenje polja i metoda klase Osoba
, u okviru klase Radnik
(a u C#-u, takođe i svojstava),
Naravno, postoji i mogućnost da klasi Radnik
budu dodata 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 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
.
Da pojasnimo dodatno: u konstruktoru klase Radnik
....
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()
.... preko sledećeg koda ....
: base(Ime, Prezime, DatumRodjenja)
: Osoba(Ime, Prezime, DatumRodjenja)
super(Ime, Prezime, DatumRodjenja);
Osoba.__init__(self, ime, prezime, datumRodjenja)
.... praktično se poziva 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 prikazani 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 mogu, u određenim okolnostima, koristiti ravnopravno (odnosno, "istovremeno").
Razmotrićemo jedan praktičan primer (u kome se koriste klase koje smo već definisali).
Recimo, smatraćemo da smo klase Osoba
i Radnik
osmislili zarad organizacije podataka o radnicima sopstvene firme i članova njihove bliže familije:
- podaci o radnicima beleže se preko klase
Radnik
- podaci o ostalim osobama beleže se preko klase
Osoba
Sledeći zahtev je: kreiranje i popunjavanje jedinstvene liste svih osoba (bez obzira na to da li je osoba radnik ili nije), * pri čemu bi se takva lista mogla koristiti (na primer), 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 na to da li su radnici ili nisu)
- sve žene (uoči 8. marta, radi dodele poklona, bez obzira na to da li su radnici ili nisu)
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
u 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 se može postupati na bilo koji ("realno mogući") način, u tom smislu da objekat koji referenciramo garantovano ima sva polja, metode i svojstva (naravno, u kontekstu toga šta objekat ikako može "imati"/"biti", u programu o kome diskutujemo).
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 se preko reference R
referencira objekat klase Osoba
- i pri tom se poziva 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".
Preopterećivanje metoda
U odeljku koji je posvećen konstruktorima, nagovestili smo da postoji mogućnost * da se u okviru klase pojavi više metoda istog naziva (bilo da je u pitanju konstruktor, ili neka od metoda opšteg tipa), pa ćemo objasniti kako takav pristup funkcioniše u praksi.
Uzmimo za primer klasu koja definiše automobil (prikazaćemo vrlo uprošćen primer takve klase, u kome je akcenat na preopterećivanju metoda), i uzmimo da su unutar klase definisana dva konstruktora:
- konstruktor koji zahteva unos marke i modela (automobila)
- konstruktor koji omogućava da se unese i komentar
public class Automobil
{
private String proizvodjac, model, komentar;
public Automobil(String proizvodjac, String model)
{
this.proizvodjac = proizvodjac;
this.model = model;
this.komentar = "";
}
public Automobil(String proizvodjac, String model, String komentar)
{
this.proizvodjac = proizvodjac;
this.model = model;
this.komentar = komentar;
}
}
class Automobil
{
private:
string proizvodjac, model, komentar;
public:
Automobil(string proizvodjac, string model)
{
this->proizvodjac = proizvodjac;
this->model = model;
this->komentar = "";
}
Automobil(string proizvodjac, string model, string komentar)
{
this->proizvodjac = proizvodjac;
this->model = model;
this->komentar = komentar;
}
}
public class Automobil
{
private String proizvodjac, model, komentar;
public Automobil(String proizvodjac, String model)
{
this.proizvodjac = proizvodjac;
this.model = model;
this.komentar = "";
}
public Automobil(String proizvodjac, String model, String komentar)
{
this.proizvodjac = proizvodjac;
this.model = model;
this.komentar = komentar;
}
}
# Direktno preopterećivanje metoda u Pythonu - nije podržano
# (a na 'zaobilazne metode' ćemo se osvrnuti drugom prilikom)
Uz preopterećivanje konstruktora, inicijalizacija se može obaviti na različite načine:
Automobil automobil_1 = new Automobil("Audi", "A4");
Automobil automobil_2 = new Automobil("VW", "Golf II", "Veoma očuvan primerak iz 1990.")
Automobil automobil_1 = Automobil("Audi", "A4");
Automobil automobil_2 = Automobil("VW", "Golf II", "Veoma očuvan primerak iz 1990.")
Automobil automobil_1 = new Automobil("Audi", "A4");
Automobil automobil_2 = new Automobil("VW", "Golf II", "Veoma očuvan primerak iz 1990.")
# Direktno preopterećivanje metoda u Pythonu - nije podržano
# (a na 'zaobilazne metode' ćemo se osvrnuti drugom prilikom)
U opštem smislu, preopterećivanje metoda moguće je izvesti onda kada je zadovoljen jedan od sledeća dva uslova:
- metode istog naziva imaju različit broj parametara
- metode istog naziva imaju isti broj parametara, ali - parametri su različitog tipa
Navedene principe ilustrovaćemo na primeru metode suma
(koja pripada klasi Kalkulator
):
public class Kalkulator
{
// polja i konstruktor su izostavljeni zarad preglednosti
Int64 suma(Int64 a, Int64 b)
{
return a + b;
}
// Isti broj parametara, ali - različit tip:
Double suma(Double a, Double b)
{
return a + b;
}
// Različit broj parametara:
Int64 suma(Int64[] niz)
{
Int64 s = 0;
for(Int64 i = 0; i < niz.Length; ++i) {
s += niz[i];
}
return s;
}
}
class Kalkulator
{
// polja i konstruktor su izostavljeni zarad preglednosti
long long suma(long long a, long long b)
{
return a + b;
}
// Isti broj parametara, ali - različit tip:
double suma(double a, double b)
{
return a + b;
}
// Različit broj parametara:
long long suma(long long niz[])
{
long long s = 0;
for(long long i = 0; i < sizeof(niz); ++i) {
s += niz[i];
}
return s;
}
}
public class Kalkulator
{
// polja i konstruktor su izostavljeni zarad preglednosti
long suma(long a, long b)
{
return a + b;
}
// Isti broj parametara, ali - različit tip:
double suma(double a, double b)
{
return a + b;
}
// Različit broj parametara:
long suma(long[] niz)
{
long s = 0;
for(long i = 0; i < niz.length; ++i) {
s += niz[i];
}
return s;
}
}
# Direktno preopterećivanje metoda u Pythonu - nije podržano
# (a na 'zaobilazne metode' ćemo se osvrnuti drugom prilikom)
Preklapanje operatora
Preklapanje operatora, * predstavlja način da se standardni unarni i binarni operatori (kao što su na primer operatori sabiranja, oduzimanja i sl), koriste za operacije nad objektima (određene klase), nalik tome kako se standardni operatori koriste za operacije sa osnovnim tipovima podataka.
U najosnovnijem tehničkom smislu, iza svega stoji ideja da se, za određeni operator (koji se "preklapa"), napiše metoda koja definiše način funkcionisanja operatora, ili (što će biti slučaj u primeru koji ćemo prikazati), da se operator poveže sa nekom od postojećih metoda unutar klase.
Zamislimo (na primer), da klasa VelikiBroj
omogućava smeštaj celobrojnih vrednosti koje su veće od smeštajnog kapaciteta 64-bitnih int
promenljivih (tako da se broj zapisuje kao niz cifara) ** i zamislimo da je unutar klase definisana metoda za sabiranje brojeva ....
public class VelikiBroj
{
private const Int32 MAX_BROJ_CIFARA = 100;
private Int8[] cifre;
public VelikiBroj(string cifre)
{
this.cifre = new Int8[MAX_BROJ_CIFARA];
popunjavanjeNizaCifara(cifre);
}
// Prepustićemo vam da sami osmislite metodu
// popunjavanjeNizaCifara, koja niz cifara
// učitava iz niske koja predstavlja broj
public VelikiBroj sabiranje(VelikiBroj b)
{
Int8 prenos = 0;
VelikiBroj rez = new VelikiBroj();
for(int i = MAX_BROJ_CIFARA - 1; i >= 0; --i) {
Int8 p = (this.cifre[i] + b.cifre[i] + prenos);
rez.cifre[i] = p % 10;
prenos = p / 10;
}
return rez;
}
}
#define MAX_BROJ_CIFARA 100
class VelikiBroj
{
private:
short cifre[];
public:
VelikiBroj(string cifre)
{
popunjavanjeNizaCifara(cifre);
}
// Prepustićemo vam da sami osmislite metodu
// popunjavanjeNizaCifara, koja niz cifara
// učitava iz niske koja predstavlja broj
VelikiBroj sabiranje(VelikiBroj b)
{
short prenos = 0;
VelikiBroj rez = VelikiBroj();
for (int i = MAX_BROJ_CIFARA - 1; i >= 0; --i) {
short p = this->cifre[i] + b.cifre[i] + prenos;
rez.cifre[i] = p % 10;
prenos = p / 10;
}
return rez;
}
}
// Programski jezik Java ne podržava preklapanje operatora
MAX_BROJ_CIFARA = 100
class VelikiBroj:
cifre = [ ]
def __init__(self, cifre):
self.popunjavanjeNizaCifara(cifre)
# Prepustićemo vam da sami osmislite metodu
# popunjavanjeNizaCifara, koja niz cifara
# učitava iz niske koja predstavlja broj
def sabiranje(self, b):
prenos = 0
rez = VelikiBroj("0")
for i in range(MAX_BROJ_CIFARA - 1, 0, -1):
p = self.cifre[i] + b.cifre[i] + prenos
rez.cifre[i] = int(p) % 10
prenos = int(p) / 10
return rez
Pozivanje metode sabiranje
(uz prethodno instanciranje dva objekta klase VelikiBroj
) ....
VelikiBroj a = new VelikiBroj("1250000000");
VelikiBroj b = new VelikiBroj("7500000000");
VelikiBroj c;
c = a.sabiranje(b);
VelikiBroj a = VelikiBroj("1250000000");
VelikiBroj b = VelikiBroj("7500000000");
VelikiBroj c;
c = a.sabiranje(b);
// Programski jezik Java ne podržava preklapanje operatora
a = VelikiBroj("1250000000")
b = VelikiBroj("7500000000")
c = a.sabiranje(b)
.... ni u kom slučaju ne deluje neintuitivno i "problematično", ali, mogućnost da velike brojeve (koji su definisani preko klase VelikiBroj
), sabiramo onako kako bismo inače sabirali obične celobrojne vrednosti:
Int32 a, b, c;
a = 12;
b = 125;
c = a + b;
int a, b, c;
a = 12;
b = 125;
c = a + b;
// Programski jezik Java ne podržava preklapanje operatora
a = 12
b = 125
c = a + b
.... svakako deluje još zanimljivije i prirodnije.
Operator sabiranja, koji je vezan za klasu VelikiBroj
, možemo "preklopiti" preko sledećeg koda (time što ćemo ga povezati sa metodom sabiranje
):
public class VelikiBroj
{
....
public static VelikiBroj operator + (VelikiBroj b)
{
return this.sabiranje(b);
}
}
class VelikiBroj
{
....
public:
VelikiBroj operator + (VelikiBroj b)
{
return this.sabiranje(b);
}
}
// Programski jezik Java ne podržava preklapanje operatora
class VelikiBroj
....
def __add__(self, b)
return self.sabiranje(b);
.... posle čega i velike brojeve možemo sabirati na prirodan i intuitivan način:
VelikiBroj a = new VelikiBroj("1250000000");
VelikiBroj b = new VelikiBroj("7500000000");
VelikiBroj c;
c = a + b;
VelikiBroj a = VelikiBroj("1250000000");
VelikiBroj b = VelikiBroj("7500000000");
VelikiBroj c;
c = a + b;
// Programski jezik Java ne podržava preklapanje operatora
a = VelikiBroj("1250000000")
b = VelikiBroj("7500000000")
c = a + b;
Preklapanje operatora predstavlja svojevrstan uvod u naprednije tehnike objektno orijentisanog programiranja (apstraktne klase, delegati i sl), ali, ovoga puta se navedenim temama nećemo baviti, već ćemo to ostaviti za drugu priliku (uobličićemo članak "OOP 2", u kome ćemo se osvrnuti na sve ono na šta smo hteli da se osvrnemo već na početku, ali smo negde ipak "podvukli crtu", budući da je uvodni članak već dovoljno obiman sam po sebi).
Primer za kraj ....
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 može se zapisati na sledeći način:
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] = Pravougaonik(10.5, 12.4);
Pravougaonici[1] = Pravougaonik(21.3, 7.2);
Pravougaonici[2] = Pravougaonik(4.5, 22.6);
if (Pravougaonici[0].P > Pravougaonici[1].P)
{
std::cout << "Prvi pravougaonik ima veću površinu." << std::endl;
}
else
{
std::cout << "Drugi pravougaonik ima veću površinu." << std::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)
{
std::cout << "Prvi pravougaonik ima veću površinu.") << std::endl;
}
else
{
std::cout << "Drugi pravougaonik ima veću površinu.") << std::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 primer ipak nismo želeli da pišemo, zarad očuvanja preglednosti), ali, nije teško uvideti koliko je programski kod u OOP primeru pregledniji.
Međutim, setimo se na ovom mestu i primedbe iz uvodnih 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.