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

Uvod u objektno orijentisano programiranje

Facebook LinkedIn Twitter Viber WhatsApp E-mail
zoom_plus zoom_minus bookmark

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":

  • objekte možemo 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žemo reći da simulacije objekata iz spoljnjeg sveta u računarskim sistemima (razne simulacije vožnje, letenja i sl), verovatno predstavljaju 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, pa ć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 daljem tekstu i budućim člancima, za objektno orijentisano programiranje ćemo takođe povremeno koristiti i (veoma) uobičajenu skraćenicu OOP.

** Na izvestan način, svakako je potrebno biti svestan i toga šta OOP pristup ne podrazumeva, to jest, potrebno je biti svestan toga da se određene stvari neće dešavati 'same od sebe', samo zato što koristimo objekte i klase.

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 i primere, prodiskutovati prvo 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.

.... koji se, u praktičnom smislu, tipično rešavaju preko funkcija (koje po pravilu ne bi trebalo da sadrže više od tridesetak linija koda).

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

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.

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.

* U navedenim objektima, prepoznaju se i "podobjekti" koje je potrebno simulirati: pogonski mehanizam automobila (motor), prenosni mehanizam, vešanje i sl.

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žemo reći da ne zavise od izbora metodologije za projektovanje softvera).

Kad smo se već dotakli teme automobilizma, možda bi jedno poređenje sa auto industrijom moglo da bude od koristi i u razumevanju toga kakvo mesto OOP pristup zauzima u računarskoj industriji.

Kao što je motor sa unutrašnjim sagorevanjem (još uvek), najuobičajeniji vid pogona motornih vozila, iako nije i jedini, tako je i OOP uobičajen pristup u razvoju većine softvera: nije jedini, ali - jeste najuobičajeniji.

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.

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

Problem (sasvim jednostavno), rešava petlja koja prolazi kroz sadržaj svih datoteka, pri čemu se za svaku datoteku poziva funkcija koja analizira i prepravlja sadržaj datoteke, tako da se na izlaz šalje samo prvi prazan red (dok se ostali susedni prazni redovi zanemaruju).

OOP pristup nije "bolji" od proceduralnog, niti "gori" (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, 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 skripti i 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, jako 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 preglednije i učine jednostavnijim za održavanje.

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

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

Kratak istorijat OOP-a

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

U to vreme, dvojica norveških programera, Ole-Johan Dal (Ole-Johan Dahl) i Kristen Najgard (Kristen Nygaard), razvili su programski jezik SIMULA (prva verzija pojavila se 1961), koji je još na početku imao 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.

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

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

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

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

Sredinom devedesetih godina, uporedo sa vrhuncem ekspanzije C++-a, 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), Javu možemo smatrati "najočiglednijim" primerom jezika koji je nedvosmisleno 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 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! :)");
  }
}
        
    
Slika 1. - "Hello world" program u Javi.

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

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

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

Međutim, pojasnili smo već da je prava svrha OOP pristupa - bolja organizacija većih projekata, a ne rešavanje ovakvih jednostavnih zadataka.

Doduše, na kursu Jave na početnim godinama studija, prost "hello world" program, pisaćete 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 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 smo već nagovestili), 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. **

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

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

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

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

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

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

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

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

Osnovni pojmovi u objektno orijentisanom programiranju

Pošto smo se ukratko upoznali sa najopštijim odrednicama OOP-a i sa time kako je i zašto 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, posvetićemo zaseban odeljak u nastavku.

.... a osvrnućemo se ukratko (pred kraj) i na nekoliko naprednijih opcija koje su prisutne u OOP jezicima.

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

Kao što smo već pomenuli, u nekim programskim jezicima (kao što je na primer Java), doslovno je obavezno da svaka klasa bude zapisana u zasebnoj datoteci, ali, većina OOP programskih jezika je ipak ponešto fleksibilnija u smislu strukture projekta, i dozvoljeno je definisati više klasa unutar jedne datoteke (jedan od jezika koji to dozvoljava je i C#).

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

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

Pri dizajniranju klase, potrebno je dakle, izabrati i opisati:

  • koji će podaci biti predstavljeni
  • kako će biti predstavljeni
  • kako će podaci 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 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.

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):

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

U smislu apstrakcije podataka, izabrali smo samo ono što je neophodno: opštim matematičkim svojstvima pravilnih mnogouglova, dodali smo ona svojstva koja se koriste 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.

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

Naravno da je često situacija veoma jasna i nedvosmislena, pogotovo kada su u pitanju jednostavne, tipske klase (na primer, klasa za smeštaj podataka koji su preuzeti iz baze podataka, najčešće će imati ista polja kao i odgovarajuća tabela u bazi), ali, ako uzmemo u obzir komplikovanije primere, kao što su različite računarske simulacije (recimo, pomenuta simulacija reli trka), situacija nije nimalo jednostavna.

Pre tridesetak godina 'strelica na gore' je značila 'auto ide napred', dok je 'strelica na dole' značila da smo 'aktivirali kočnicu'.

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

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

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

Polje (field) - podaci

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.

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

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.

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

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

U pitanju je postupak 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 ćemo posvetiti mnogo više pažnje u nastavku (pošto se osvrnemo na preostale osnovne odlike klasa).

Metode - obrada podataka

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

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

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

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

Da bismo mogli da za prave razumemo kako se odvija interakcija između polja i objekata i, pre svega, kako klasa dobija željene odlike (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', ipak će (kako i dolikuje), biti 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.

U najjednostavnijim programima za početnike (gde je, na primer, samo potrebno izračunati obim i površinu jednog pravougaonika), ne postoji realna potreba za kreiranjem klase koja opisuje pravougaonike, ali 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 klase koja definiše pravougaonike - više je 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) ....

Definisaćemo prvo najosnovnije okvire klase (tj. dodaćemo samo osnovne podatke), pa će klasa na početku imati sledeći oblik:

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

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

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

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

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

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

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

Iako klasa koju smo videli ni iz daleka nije dovršena, niti zapravo spremna za eksploataciju (tek smo počeli), u najosnovnijem (tehničkom) smislu, klasu već 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 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 (jednostavnog) koda ....

C#
C++
Java
Python 3
		
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()
			
    	
Slika 7. - Kreiranje objekta P1 (klase Pravougaonik).

.... kreira se objekat P1, posle čega možemo pristupiti poljima objekta:

C#
C++
Java
Python 3
        	
P1.a = 12.55;
P1.b = 10.40;
Console.WriteLine(P1.a);
Console.WriteLine(P1.b);
		
	
    	
P1.a = 12.55;
P1.b = 10.40;
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))
		
	
Slika 8. - Pristup poljima objekta P1 - zadavanje vrednosti.

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

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

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

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

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

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

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

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

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

Ukoliko se sami ne pobrinemo za inicijalizaciju, polja čiji je tip Double (isto važi i za Int32 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 inicijalizujemo stranice pravougaonika - ali ne i obim i površinu - polja O i P će imati vrednost 0 (što svakako nije u skladu sa pravilima geometrije i našim prvobitnim očekivanjima).

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

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

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

* Napomenimo usput da, iako "implicitna inicijalizacija" polja funkcioniše onako kako smo pokazali, ne mora "baš uvek" biti tako ....

Recimo, u programskom jeziku C++ (kao što smo naveli u komentarima), postoje i drugačiji načini za inicijalizuju objekata (u odnosu na način koji smo prikazali u članku), i u takvim situacijama, brojčana polja unutar objekata ne moraju biti inicijalizovana vrednošću 0, međutim, zarad preglednosti članka, ostavićemo čitaocima koji proučavaju C++, da se samostalno upoznaju sa specifičnostima ovog jezika (i vraćamo se na uređivanje klase Pravougaonik).

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 (tj. 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ć pozivanjem posebne metode.

Takva metoda u objektno orijentisanom programiranju nosi naziv konstruktor.

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, konstruktor se (u većini C-olikih jezika), prepoznaje po tome što (za razliku od ostalih metoda), nema povratni tip i po tome što ima isti naziv kao i sama klasa.

Dodaćemo konstruktor klasi Pravougaonik ....

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

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

	double a, b, O, P;

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

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

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

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

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

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

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

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

Uvođenjem dve nove metode, za računanje obima i za računanje površine, koje ćemo potom pozvati preko konstruktora - rešićemo problem.

Usput, ako se neko pita, recimo da je sasvim moguće da konstruktor poziva metode klase, kao što je i inače moguće da metode pozivaju jedna drugu.

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

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

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

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

	double a, b, O, P;

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

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

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

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

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

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

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

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

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

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

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

.... objekat biti korektno inicijalizovan.

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

Ako pozovemo sledeći kod ....

C#
C++
Java
Python 3
    	
Console.WriteLine("-Stranica a: " + P1.a.ToString());
Console.WriteLine("-Stranica b: " + P1.b.ToString());
Console.WriteLine("-Obim: "       + P1.O.ToString());
Console.WriteLine("-Površina: "   + P1.P.ToString());
		
    
    	
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))
		
    
Slika 14. - Konzolne instrukcije za ispis vrednosti polja.

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

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

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

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

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

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

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

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

	double a, b, O, P;

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

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

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

	void KonzolniIspis()
	{
		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))
		
    
Slika 16. - Klasa Pravougaonik sa dodatom metodom za konzolni ispis vrednosti polja.

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

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

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

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

Skup tehnika OOP-a 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 ćemo se, pre nego što se posvetimo enkapsulaciji, osvrnuti ukratko na rezervisanu reč this koju smo prethodno koristili unutar konstruktora.

Pomenućemo na ovom mestu da je, u okviru jedne klase, moguće definisati više konstruktora, ali o tome nećemo pisati direktno u nastavku, već ćemo tzv. preopterećivanju metoda, posvetiti zaseban odeljak pred kraj.

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.

.... što je prilično uobičajeno u mnogim konstruktorima sa kojima ćete se sretati.

Da pojasnimo ....

Kada je objekat (već) kreiran, poljima se pristupa uz navođenje identifikatora objekta i polja, a identifikatori su spojeni operatorom pristupa.

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

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

Rezervisana reč this, 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....

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

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

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

Klase za smeštaj podataka

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

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

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

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

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

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

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

Budući da klasa Pravougaonik ne spada u "baš skroz jednostavne" klase, u kojima nema potrebe za proverom podataka, i budući da pri tom (i pre svega) nije još uvek završena, vraćamo se na glavni posao ....

Enkapsulacija

Enkapsulacija (kao što smo već nagovestili), predstavlja skup tehnika 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 ....

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

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

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

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

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

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

Potrebno je dakle da postignemo dve stvari:

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

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

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

Specifikatori pristupa (private, public, protected)

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

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

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

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

Dobra strana takve "otvorenosti", je to što možemo neposredno pristupati poljima, 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.)

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

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

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

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

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

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

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

Za primer smo izabrali tipičan slučaj, * pa ćemo preduzeti 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 možemo privatnim poljima pristupati preko javnih metoda.

* Najčešće se pri dizajniranju klasa (baš kao u primeru koji smo izabrali), srećemo sa poljima koja sama po sebi (u praktičnom smislu), moraju biti privatna, ** ali - u svemu (takođe) mora postojati i način da se pristupi podacima.

Međutim, postoje i okolnosti (u drugim primerima), kada pravilno izvedena enkapsulacija podrazumeva potpuno "otvaranje", ali, tako nešto je moguće (uglavnom u manjim klasama), samo onda kada smo potpuno sigurni da upis proizvoljnih vrednosti u public polja, ne stvara probleme.

Korišćenje potpuno "zatvorenih" klasa je moguće (pod uslovom da je konstruktor definisan kao javna metoda; što je najtipičniji slučaj), međutim, reklo bi se ipak da takav pristup nema prednosti u odnosu na "standardnu"/"tipičnu" enkapsulaciju, kakvu opisujemo u glavnom toku članka.

** U suprotnom, ako polja definišemo preko specifikatora pristupa public, nastaje 'zbrka' koja je slična situaciji koju smo na početku opisali (mogućnost unosa negativnih vrednosti stranica pravougaonika, posle čega se obim i površina ne ažuriraju automatski i sl).

Pristup privatnim poljima preko javnih metoda

Javna metoda za čitanje vrednosti privatnog polja (u primeru koji koristimo), ima sledeći oblik:

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

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

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

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

C#
C++
Java
Python 3
    	
public void Upis_a(Double a)
{
	// Radi preglednosti nećemo 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()
		
    
Slika 23. - Javno dostupna (public) metoda Upis_a upisuje predatu vrednost u polje a (ali, samo ako je data vrednost pozitivna) i potom ažurira obim i površinu.

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 - poziva 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:

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

Objekat:

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

Deluje da klasa sadrži više, međutim, tako je samo naizgled (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, a da je 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), dok je u nekim drugim situacijama potrebno 'zaštititi' sadržaj samih polja (vrednost stranice pravougaonika ne sme biti negativan broj ili nula).

Kao što smo takođe već videli, enkapsulacija koja se izvodi "skrivanjem podataka" nije obavezna ("uvek"), ali, najčešće, u iole ozbiljnijim i obimnijim klasama, postoji bar poneki primer eksplicitno definisane enkapsulacije.

Pristup koji smo opisali u prethodnom odeljku mož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# se (suštinski) 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):

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

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

	public Double a  // 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
		
    
Slika 26. - Svojstvo a, definisano preko dva akcesora (set i get). Svojstva su svojevrsne kombinacije polja i metoda: spolja gledano, izgledaju kao public polja, a, po potrebi, funkcionišu kao metode.

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

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

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

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

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

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

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

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

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

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

Drugi blok, set ....

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

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

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

Posmatrajmo to ovako (postavimo sebi pitanje): pod kojim okolnostima 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.

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

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

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

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

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

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

  • jedan od dva bloka može se i izostaviti

Na primer:

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

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

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

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

Takođe, primetili smo i to da sada, kada spoljnim delovima koda "otkrivamo" svojstva koja nose prethodne nazive polja (a, b, O i P), za sama polja koristimo nazive 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 temama nasleđivanja u objektno orijentisanom programiranju, 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 (nećemo ovoga puta koristiti klasu Pravougaonik, već drugi, jednostavniji primer):

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

Možemo definisati dve potpuno nezavisne klase, ali, možemo primeniti i 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:

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

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

		RacunanjeStarosti();
	}

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

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

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

	}

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

class Osoba
{
	private:

	int _starost;

	public:

	string ime, prezime;
	Datum  datumRodjenja;

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

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

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

		_starost = godina - datumRodjenja.godina;

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

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

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

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

		RacunanjeStarosti();
	}

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

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

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

	}

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

class Osoba:

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

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

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

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

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

Vidimo da klasa nema baš previše podataka, ali, čak i na ovakvom 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 (a u C#-u, takođe i svojstva),

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

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

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

		RacunanjeStaza();
	}

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

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

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

	public UInt32 RadnoMesto
	{
		get
		{
			return _RadnoMesto;
		}
	}

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

	private:

	Datum  _pocetakRada;
	string _pozicija; 
	int    _staz;

	public:

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

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

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

		_staz = godina - _pocetakRada.godina;

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

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

	int citanjeStaza()
	{
		return _staz;
	}

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

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

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

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

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

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

	public int citanjeStaza() {
		return _Staz;
	}

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

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

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

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

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

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

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

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

}
		
    
    	
class Radnik : public Osoba
{

};
		
    
    	
public class Radnik extends Osoba {

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

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

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

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

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

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

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

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

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

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

Razmotrićemo jedan praktičan primer (u kome se koriste 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: kreiranje i popunjavanje jedinstvene liste svih osoba, bez obzira na to da li je osoba radnik ili nije (smatramo da je, u konkretnoj implementaciji, najbolje da postoji samo jedna takva lista, zarad dobre organizacije i uštede prostora), pri čemu bismo takav spisak (na primer), mogli koristiti da u određenim okolnostima pronađemo:

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

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

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

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

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

Na primer, sledeći kod ....

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

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

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

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

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

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

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

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


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

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

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

Recimo, sledeći kod ....

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

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

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

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

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

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

Preopterećivanje metoda

Nagovestili smo, u odeljku koji je posvećen konstruktorima, 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.

* Naravno - "preopterećivanje" metoda podrazumeva uvažavanje određenih pravila, koja ćemo navesti u nastavku.

Što se tiče samog termina preopterećivanje, u pitanju je (ne baš previše intuitivan), direktan prevod originalnog engleskog termina "overload" (ali, više o terminologiji - na kraju odeljka).

Takođe, napomenimo da Python ne podržava direktno preopterećivanje metoda (kao drugi jezici koje koristimo u članku), već samo postoje (manje ili više elegantni) 'zaobilazni' načini da se 'simulira' preopterećivanje metoda (čime ćemo se baviti nekom drugom prigodnom prilikom).

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:

  • jedan koji zahteva unos marke i modela (automobila)
  • drugi, koji omogućava da se unese i komentar
C#
C++
Java
Python 3
    	
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)
		
    
Slika 42. - Klasa Automobil (preko koje se beleže samo najosnovniji podaci o automobilima), koja prikazuje primer preopterećivanja konstruktora.

Uz preopterećivanje konstruktora, inicijalizaciju možemo obaviti na različite načine:

C#
C++
Java
Python 3
    	
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)
		
    
Slika 43. - Dva različita načina inicijalizacije klase Automobil (uz korišćenje dva različita konstruktora).

U opštem smislu, preopterećivanje metoda moguće je izvesti onda kada je zadovoljen bar 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):

C#
C++
Java
Python 3
    	
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)
		
    
Slika 44. - Primeri preopterećivanja metode za računanje sume brojeva (u okviru klase Kalkulator).

U domaćoj literaturi se, za termine iz engleskog jezika "method overload(ing)" i "operator overload(ing)", tipično koriste prevodi "preopterećivanje metoda" i "preklapanje operatora".

Ponegde se koristi i prevod "preklapanje metoda", ali, bez obzira što takav prevod smatramo elegantnijim, moramo primetiti da je prevod "preopterećivanje metoda" (iako donekle rogobatan sam po sebi), ipak tačniji i prikladniji, budući da nije u pitanju poništavanje funkcionalnosti jedne metode - uvođenjem druge metode (kao pri preklapanju operatora).

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

* u originalu, takođe "overload"

** Da bi primer bio jednostavniji, klasu ćemo projektovati tako da nizovi cifara mogu imati maksimalno 100 elemenata.

Što se tiče upoznavanja sa (prilično jednostavnim, makar kada su u pitanju sabiranje i oduzimanje), idejama koje stoje iza operacija nad brojevima koji su predstavljeni kao nizovi cifara, možete posetiti sledeći link.

Takođe, napomenimo da, kao što smo se u prošlom odeljku susreli sa situacijom da Python ne omogućava preopterećivanje metoda (onako kako to omogućavaju ostali jezici koje koristimo u članku), u ovom odeljku se susrećemo sa time da Java ne dozvoljava preklapanje operatora.

Ali, da se vratimo na velike brojeve ....

C#
C++
Java
Python 3
    	
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
		
    
Slika 45. - Klasa VelikiBroj, preko koje se veliki brojevi predstavljaju kao nizovi cifara.

Pozivanje metode sabiranje (uz prethodno instanciranje dva objekta klase VelikiBroj) ....

C#
C++
Java
Python 3
    	
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)
		
    
Slika 46. - Pozivanje metode sabiranje (uz prethodno instanciranje objekata).

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

C#
C++
Java
Python 3
    	
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
		
    
Slika 47. - Primer sabiranja celobrojnih promenljivih.

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

C#
C++
Java
Python 3
    	
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);
		
    
Slika 48. - Preklapanje operatora sabiranja u okviru klase VelikiBroj.

.... posle čega i velike brojeve možemo sabirati na prirodan i intuitivan način:

C#
C++
Java
Python 3
    	
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;
		
    
Slika 49. - Primer upotrebe "preklopljenog" operatora sabiranja, za rad sa objektima klase VelikiBroj.

Preklapanje operatora predstavlja svojevrstan uvod u naprednije tehnike objektno orijentisanog programiranja (apstraktne klase, delegate 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:

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

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

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

if (Pravougaonici[0].P > Pravougaonici[1].P)
{
	Console.WriteLine("Prvi pravougaonik ima veću površinu.");
}
else
{
	Console.WriteLine("Drugi pravougaonik ima veću površinu.");
}
		
    
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonici[0] = 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.")
		
    
Slika 51. - Zadatak pronalaženja većeg od prva dva pravougaonika rešen metodom OOP-a.

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

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

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

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

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

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

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

if (P1_P > P2_P)
{
	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.")
		
    
Slika 52. - Zadatak pronalaženja većeg od prva dva pravougaonika rešen upotrebom zasebnih promenljivih.

Razlika nije drastična na ovako malom i jednostavnom primeru (a veći i kompleksniji 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.

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

Info & povezani članci Info o članku - dugme

Info

trejler_sat Datum objave: 04.03.2020.

trejler_olovka Poslednja izmena: 24.03.2020.

trejler_dokument Jezici: C#
C++
Java
Python

trejler_teg_narandzasti Težina: 8/10

Povezani članci

Povezani članci

Svi članci
So if you want to go fast, if you want to get done quickly, if you want your code to be easy to write, make it easy to read.
Robert C. Martin
codeBlog codeBlog
Sajt posvećen popularizaciji kulture i veštine programiranja.
Napomena: Tekstovi i slike na sajtu www.codeblog.rs (osim u slučajevima, gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta www.codeblog.rs i zabranjeno je njihovo korišćenje na drugim sajtovima i štampanim medijima, kao i bilo kakvo drugo korišćenje u komercijalne svrhe, bez eksplicitnog odobrenja autora.
© 2020-2023. Sva prava zadržana.
Facebook - logo
Instagram - logo
LinkedIn - logo
Twitter - logo
E-mail
Naslovna
   •
Uslovi korišćenja
   •
Obaveštenja
   •
FAQ
   •
Kontakt