nav_dugme codeBlog codeBlog
  • početna Početna stranica
  • Sačuvani članci Sačuvani članci
  • Članci
     (spisak)
  • Kontakt
Povratak na vrh stranice

Info & povezani članci Info o članku - dugme

Info

trejler_sat Datum objave: 04.03.2020.

trejler_olovka Poslednja izmena: 16.10.2024.

trejler_dokument Jezici: C#
C++
Java
Python

trejler_teg_narandzasti Težina: 8/10

C++
C#
Java
Python
objektno orijentisano programiranje
oop
enkapsulacija
strukture podataka
operatori
teorija

Povezani članci

Uvod u dinamičko programiranjeStrukture podatakaUvod u relacione baze podataka i SQLPostfiksna notacija - kako računari računaju?Klase složenosti algoritama (O notacija)Operacije sa bitovima u programskom jeziku COperacije sa tekstualnim datotekama u programskim jezicima C i PythonIzbor prvog programskog jezikaAsinhrono programiranje u JavaScriptuASCII, Unicode i UTF - Predstavljanje znakova na računarimaGNU/Linux - 1. deo - Uvod
Svi članci
A computer is like a mischevious genie. It will give you exactly what you ask for, but not always what you want
Joe Sondow

Uvod u objektno orijentisano programiranje

Facebook LinkedIn Twitter Viber WhatsApp E-mail
zoom_plus zoom_minus bookmark
početna > Članci > Teorija

Uvod

Objektno orijentisano programiranje je jedna od paradigmi u programiranju, odnosno, jedan od pristupa u projektovanju računarskih programa, koji podrazumeva da se problemi rešavaju upotrebom "objekata":

  • u idejnom smislu, pojedinačni objekat predstavlja određeni aspekt konkretnog problema
  • u tehničkom smislu, objekat se može shvatiti kao funkcionalna celina u programskom kodu koju 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

Programi u kojima se objekti iz spoljnjeg sveta simuliraju u okviru računarskih sistema (razne simulacije vožnje, letenja, elektronskih kola, industrijskih procesa i sl), verovatno su 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 i ikone na desktopu, a postoje i brojni drugi objekti koji se ne vide, već 'samo' podržavaju strukturu različitih programa koji se sreću u svakodnevnoj upotrebi.

Tematika objektno orijentisanog programiranja je složena, potrebno je pristupiti joj na ozbiljan način i uložiti odgovarajući trud da bi se postigli pravi rezultati, i stoga ćemo se potruditi da predočimo u nastavku 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 pojam "objektno orijentisano programiranje" uopšte podrazumeva. *

Ukoliko su zadovoljena prva dva uslova, u slučaju većine čitalaca i drugih pojedinaca koji započinju upoznavanje sa objektno orijentisanim programiranjem tipično postoji i (bar osnovna) predstava o smislu i značaju OOP ** pristupa (vrlo često i o ponekim 'tehnikalijama'), ali, pre nego što pređemo na osnovne pojmove objektno orijentisanog programiranja i primere, prvo ćemo ("za svaki slučaj") prodiskutovati o razlikama između proceduralnog i objektno orijentisanog programiranja, 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) ....

* Na izvestan način, svakako je potrebno biti svestan i toga šta objektno orijentisano programiranje ne podrazumeva, to jest, potrebno je biti svestan toga da se određene stvari neće dešavati 'same od sebe', samo zato što određeni program koristi "objekte i klase" (više o tome, kasnije u članku).

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

Osnovne postavke, razlike u odnosu na proceduralni pristup

U proceduralnom programiranju (što, da se podsetimo, predstavlja zvanični naziv za "obično programiranje" sa kakvim već imate iskustva), primenjuje se tzv. pristup "odozgo-nadole" (eng. top-down), koji podrazumeva da se problem prvo sagledava u celosti, i zatim po potrebi 'razbija' na potprobleme.

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

Na primer, ako je zadatak: među pozitivnim celobrojnim vrednostima koje se unose redom, pronaći i ispisati vrednost sa najvećim brojem cifara (ulaz: 9, 1771, 165; izlaz: 1771), problem se može rešiti po sledećoj proceduri:

  • definisati funkciju koja, za unetu celobrojnu vrednost, vraća pozitivnu celobrojnu vrednost koja odgovara broju cifara unetog broja
  • u glavnom programu, učitati prvu vrednost i inicijalizovati pomoćnu promenljivu br_cif vrednošću koja odgovara broju cifara prvog učitanog broja, a pomoćnu promenljivu izlaz inicijalizovati vrednošću prvog učitanog broja
  • učitavati redom ostale vrednosti i, svaki put kada se pronađe učitana vrednost koja ima veći broj cifara (od dotadašnje maksimuma), dodeliti promenljivoj br_cif vrednost koja odgovara broju cifara trenutno učitane vrednosti, a promenljivoj izlaz dodeliti vrednost koja odgovara trenutno učitanom broju
  • na kraju - ispisati vrednost promenljive izlaz

Opisani način rešavanja problema (čitaocima poznat od ranije), primereno je koristiti u mnogim situacijama, ali - ne uvek.

U velikim projektima, složenost celokupnog posla tipično je tolika da ne dozvoljava da se problem sagleda odjednom (i zatim podeli na manje celine), i stoga se u pomenutim situacijama - umesto top-down pristupa - tipično koristi pristup koji podrazumeva da se prvo detaljno definišu: osnovni delovi sistema ('objekti'), pojedinačni način funkcionisanja svakog od delova, kao i načini za međusobno povezivanje.

Na kraju, celokupan sistem nastaje spajanjem i interakcijom pojedinačnih elemenata, i u pitanju je pristup koji je poznat kao "odozdo-nagore" (eng. bottom-up).

Ako uzmemo dobro poznati primer simulacija vožnje na računaru, lako možemo razumeti da se kao objekti prirodno javljaju automobili i staze, * što znači da je prvo potrebno detaljno definisati pravila po kojima svaki od objekata funkcioniše, a potom i međusobne odnose koji odgovaraju poznatim zakonitostima fizike (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 "odjednom" sagledamo navedeni problem simulacije (tj. da rešimo problem metodom 'odozgo-nadole') .... verovatno bismo izazvali "blago" preopterećenje sopstvenog aparata za razmišljanje. :)

Takođe (pored pojedinosti koje smo do sada naveli), pretpostavljamo da je jasno da pravilno i 'glatko' funkcionisanje prethodno opisanog sistema, u praktičnom smislu zavisi (veoma nedvosmisleno (!)), i od raspoloživih računarskih resursa (tj. 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 važe (naravno) i za "ne-OOP" programe (drugim rečima: hardverski zahtevi postoje uvek i, blago uprošćeno, može se 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 biti od koristi i u razumevanju toga kakvo mesto objektno orijentisano programiranje 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čajeni 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 tehnika objektno orijentisanog programiranja.

Ako bismo kao (dodatni) 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 lako može sagledati u celosti i nema potrebe za 'objektima i klasama'.

Problem se rešava (sasvim jednostavno), upotrebom petlje 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 metodologija nije "bolja ili gora" od proceduralne, već je samo u određenoj situaciji jedan od dva navedena pristupa primereniji, i oba pristupa (i te kako) imaju mesto u informacionim tehnologijama.

Objektno orijentisano programiranje je uobičajen način rešavanja problema u slučaju većih projekata 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, što znači da je za takve projekte primereno koristiti OOP pristup, međutim (da ponovimo), za mnoge manje programe jednostavne strukture (kao i pomoćne skripte i sličan softver), nije obavezno koristiti tehnike OOP-a, to jest, proceduralno programiranje može poslužiti sasvim dobro.

Ali, da bismo dodatno 'zakomplikovali priču', osvrnućemo se na još nekoliko situacija ....

Sa jedne strane, u praksi postoji i veći broj proceduralnih programa koji "nisu mali" (recimo, imaju preko 500, 1000, pa i više linija koda), veoma su kompleksni i zahtevni za razumevanje (čak i ukoliko su dobro optimizovani), mogu biti vrlo značajni u određenom informacionom sistemu, ali - ipak ne zahtevaju OOP pristup.

Sa druge strane, mnogi naoko obimni programi napisani uz korišćenje tehnika OOP-a, 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 se neko oslonio na OOP ili proceduralno programiranje pri rešavanju određenog problema, i (takođe, kao što smo već naglasili): i jedan i drugi pristup ima svrhu, a poenta OOP-a (naravno) nije da se mali programi učine komplikovani(ji)m, već, da se veliki programi zapišu na pregledniji način, i učine jednostavnijim za održavanje.

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 (tema sledećeg poglavlja), i 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

Među programerima postoji svojevrstan "Mandela efekat" po pitanju vremena pojave prvih implementacija tehnika objektno orijentisanog programiranja, odnosno, mnogi smatraju da istorija OOP-a počinje sredinom 80-ih godina dvadesetog veka, međutim, prva implementacija prvi put se pojavila već početkom šezdesetih godina, kada su dvojica norveških programera, Ole-Johan Dal (Ole-Johan Dahl) i Kristen Najgard (Kristen Nygaard), razvili programski jezik Simula, 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") i - u praktičnom smislu - može se 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 orijentisano programiranje počelo da doživljava pravu ekspanziju.

* Prva verzija jezika Simula pojavila se 1961.

** Osamdesete su takođe i vreme kada su softverski paketi već uveliko počeli da liče na današnje (u smislu izgleda i funkcionalnosti).

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, a ako biste se vratili u osamdesete, verovatno ne bi bilo 'začuđenosti'.

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" (a vredi pomenuti i da mnogi poznati softverski paketi (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 metodologiji objektno orijentisanog programiranja.

Druga navedena odlika imala je veći (neposredan ili posredan) uticaj i na mnoge programske jezike koji OOP metodologiji do tada nisu pridavali previše značaja, a u opštem smislu (kao što smo na neki način već nagovestili), Java se može smatrati "najočiglednijim" primerom jezika koji je nedvosmisleno usmeren na objektno orijentisani pristup, i stoga ćemo izvesnim osobenostima programskog jezika Java posvetiti još koji red.

Ako (na primer) C++, omogućava da se tehnike proceduralnog programiranja koriste samostalno ili u kombinaciji sa tehnikama OOP-a (pri čemu ima i drugih jezika koji nude slične mogućnosti), sa Javom to nije slučaj: u Javi sve mora biti definisano preko klasa (pa makar ceo projekat u praktičnom smislu bio najobičniji proceduralni program od nekoliko instrukcija), svaka javna 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, zapisan u programskom jeziku Java.

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

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

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

Međutim, pojasnili smo već da je prava svrha objektno orijentisanog programiranja - dobra organizacija većih projekata (a ne rešavanje 'skroz 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 nisu toliko "isključivi" (kao na primer C++), a postoji i veći broj drugih jezika koji takođe nisu (na očigledan način) 'unapred usmereni' na proceduralno programiranje ili OOP, ali svakako podržavaju upotrebu klasa i objekata.

U svakom slučaju (kao što je već pomenuto), tokom vremena, a pogotovo uporedo sa ekspanzijom Jave i pod uticajem Jave, objektno orijentisani pristup (bilo kao "glavna atrakcija" ili samo kao "jedna od mogućnosti"), našao je 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".

(Usput: Python podržava objektno orijentisano programiranje.)

** Programski jezik C je svakako jedan od najpoznatijih i 'najočiglednijih' izuzetaka (da ne kažemo: najpoznatiji i najočigledniji :)), ali, s obzirom na prilično veliki značaj u današnjoj računarskoj industriji i ogroman istorijski značaj, mogli bismo (uz nešto "pesničke slobode"), jezik C poistovetiti sa uvaženim starijim članom porodice koji je porodici obezbedio ugled i imetak.

Iako je dobar deo posla vremenom prešao na potomke, 'pater familias' - i dalje - i te kako ima uticaja na važne odluke (i niko ne dovodi u pitanje zašto šarmantni prosedi gospodin u belom odelu: i dalje zapisuje brojeve telefona u stari notes - i dalje živi u istoj kući i vozi isti auto kao i pre pedeset godina, a ponajmanje - zašto i dalje ne želi da usvoji principe OOP-a .... zaslužio je).

Kada je u pitanju glavni jezik koji će u članku biti korišćen za primere - izabrali smo C#, s tim da pružamo čitaocima mogućnost da lako promene jezik po želji (opcije su: C#, C++, Java i Python).

C# je jezik koji je usmeren na klase - poput Jave, ali, rekli bismo ne baš u istoj meri, a osim toga: C# je ponešto fleksibilniji, opšti je utisak da sintaksa C#-a deluje preglednije od sintakse drugih popularnih OOP jezika - i upravo zato smatramo da je C# najprikladniji jezik za početno upoznavanje sa objektno orijentisanim programiranjem. *

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

* Iako, u opštem smislu, ne smatramo da je C# "najoptimalniji" OOP jezik, smatramo da sintaksa C#-a pogoduje upoznavanju sa OOP tehnikama u većoj meri nego sintaksa ostalih jezika.

** Python ne preporučujemo za početno upoznavanje sa tematikom OOP-a, ali, ipak smo prikazali i primere iz Python-a, zarad opšteg obrazovanja i zarad "razbijanja monotonije" (budući da su primeri iz C#-a, C++-a i Jave, 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 osnove OOP-a kroz C++, C# ili Javu.

Osnovni pojmovi u objektno orijentisanom programiranju

Pošto smo se ukratko upoznali sa najopštijim osobinama OOP-a i sa time kako je i zašto takav pristup našao mesto u računarskoj industriji, vreme je da se vratimo na 'tehnikalije' i da se upoznamo sa najvažnijim konceptima objektno orijentisanog programiranja.

Teoriju objektno orijentisanog programiranja dodatno ćemo ilustrovati kroz jednostavan primer (sasvim prikladan za sam početak), koji ćemo razvijati (tj. dopunjavati) kroz poglavlja članka, što će biti dobra prilika da proučimo kako tehnike o kojima pišemo funkcionišu u praksi.

Objektno orijentisano programiranje temelji se na sledećim osnovnim pojmovima:

  • klasa
  • objekat
  • apstrakcija podataka
  • enkapsulacija
  • nasleđivanje
  • polimorfizam

Svakom od navedenih pojmova, biće posvećen 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

U idejnom smislu, klasa u objektno orijentisanom programiranju predstavlja obrazac za kreiranje (budućih) objekata.

U praktičnom smislu, u pitanju je programski kod koji je tipično ali ne i obavezno (videti napomenu na kraju odeljka), 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 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 sopstvenim podacima, ili sa drugim podacima u programu)

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

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

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

Ukratko, pri dizajniranju klase, potrebno je uskladiti prethodno navedene smernice (kao i druge detalje o kojima će tek biti reči), to jest, potrebno je izabrati i opisati:

  • koji podaci će biti predstavljeni
  • kako će podaci biti predstavljeni
  • kako će podaci biti međusobno povezani

.... i (naravno), potrebno je definisati mehanizme za obradu podataka.

Kao što smo već pomenuli, u nekim programskim jezicima (kao što je na primer Java), javne klase * se obavezno zapisuju preko zasebnih datoteka, po principu 'jedna klasa - jedna datoteka' (i pri tom je uobičajeno da se i privatne klase zapisuju preko zasebnih datoteka), ali, većina OOP programskih jezika je ipak ponešto fleksibilnija po pitanju strukture projekta, tj. dozvoljeno je da se klase proizvoljno rasporede po datotekama (jedan od jezika koji to dozvoljava je i C#).

Međutim, savetujemo da se ne 'opuštate' previše ako koristite jezike kao što je C#, već, da koristite prethodno pomenutu mogućnost zapisivanja više klasa u jednoj datoteci, samo ako je potrebno "usput" definisati neku manju/pomoćnu klasu, a sve iole veće i ozbiljnije klase - i dalje zapisujte preko zasebnih, prigodno imenovanih datoteka (što je u praksi stvar konvencije, bez obzira na to da li je i pitanje obaveze).

Naravno, detaljnija diskusija o 'javnim klasama' i 'privatnim klasama' tek predstoji (nismo očekivali da čitaoci treba da razumeju navedene pojmove 'neposredno'/bez pravog uvoda). :)

U kasnijem toku članka (u poglavlju o 'enkapsulaciji'), prodiskutovaćemo detaljno o razlici između javnih i privatnih klasa, ali, za sada se vraćamo na 'osnovnije' teme ....

Odnos između klase i objekta

Da bismo ilustrovali sve što je do sada navedeno (i da bismo prikazali odnos između klase i objekta, odnosno - objekata (u množini)), za primer možemo uzeti klasu preko koje se definišu pravilni mnogouglovi 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, dodata su svojstva koja se koriste za prikaz na ekranu.

Ako bismo zapravo iscrtavali figure sa gornje slike, bilo bi potrebno da navedemo i koordinate temena (to smo ovoga puta ipak izostavili, da bi primeri bili lakši za praćenje).

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

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

Klasa čija je šema prikazana na slici #3 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 (tj. mnogouglovi), ispoljavaju određenu kombinaciju konkretnih svojstava.

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

Naravno, često je 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 i sl), 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 (a zapravo je tako već duži niz godina), vode računa i o broju obrtaja motora, o tome koliko je jako stisnuta papučica za gas (ne samo o tome da li je uopšte stisnuta ili otpuštena), koliko je jako stisnuta kočnica, koliko je (tačno) volan okrenut 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, međutim, i dalje ostaje pitanje: šta treba simulirati, a šta ne? Da li ćemo simulirati i zategnutost 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

Termin 'polje klase' označava pojedinačni imenovani podatak koji je zapisan (direktno) unutar klase, pri čemu 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č ili referenca - na podatak, objekat ili kolekciju bilo kog od prethodno 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 vođenje 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' (tj. '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 nastaje 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 "automatski" voditi računa o tome da se smisao upotrebljenih polja poklopi sa nazivima (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 preko koga se podaci:

  • čuvaju od neovlašćenog pristupa ("diskutabilnih" upisa i sl)
  • po potrebi dovode u odgovarajuće međusobne odnose

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

Metode - obrada podataka

U objektno orijentisanom programiranju, termin "metoda" označava:

  • u idejnom smislu - proceduru za obavljanje operacija nad poljima klase (i, po potrebi, sa spoljašnjim podacima)
  • u najpraktičnijem smislu - funkciju koja je zapisana unutar tela klase
Opšti prikaz načina funkcionisanja metoda unutar klase / objekta
Slika 5. - Opšta šema izvršavanja metode unutar klase (tj. objekta).

Dakle (kao što smo već nagovestili), 'osnovne tehničke karakteristike' metoda koje su zapisane unutar klase, gotovo u potpunosti se poklapaju sa opštim karakteristikama funkcija sa kakvima ste se već sretali * u C-u ili nekom sličnom jeziku, ** a bitno je napomenuti i to da metode klase mogu direktno pristupati poljima klase *** (u primeru na slici #5, metoda PromenaPoluprecinika pristupa direktno polju r).

* Još na početku smo uveli pretpostavku da su čitaoci (koji žele da se upoznaju sa tehnikama OOP-a), već upoznati sa proceduralnim programiranjem.

** Na primer: pri pozivu funkcije (tj. metode), predaju se argumenti; funkcije (tj. metode) imaju ili nemaju povratnu vrednost, i sl.

*** Kao što smo već nagovestili na početku odeljka, metodama klasa (tj. objekata), mogu se prosleđivati i podaci iz spoljnih delova koda, pod uslovom da je određena metoda definisana tako da kao argument(e) prima jedan ili više podataka odgovarajućeg tipa (prikazaćemo primere u narednim odeljcima).

Naravno, dizajn i upotreba metoda, takođe podleže i drugim 'pisanim i nepisanim pravilima', na koja ćemo se detaljnije osvrtati u kasnijem toku članka.

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

Struktura jednostavnog primera koji će biti korišćen u članku

Na početku smo se potrudili da potcrtamo da je tehnike OOP-a primereno koristiti (samo) onda kada su u pitanju kompleksni objekti koji takav pristup zavređuju, međutim, primer koji ćemo koristiti za početno 'uhodavanje', biće (kako i dolikuje), ipak mnogo jednostavniji: definisaćemo klasu koja opisuje pravougaonik(e).

Primer je naizgled jednostavan (i ne samo naizgled), ali, sasvim je adekvatan i u praktičnom smislu.

U najjednostavnijim programima za početnike (u kojima npr. samo treba izračunati obim i površinu jednog pravougaonika), ne postoji realna potreba za kreiranjem klase koja opisuje pravougaonike, ali, za bilo koji (bar ponešto kompleksniji) 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 (tj. u praktičnom smislu, neophodno).

Klasa Pravougaonik obuhvatiće sledeće elemente:

  • polja a, b, O i P (podatke o stranicama pravougaonika, obimu i površini)
  • metode preko kojih se obim i površina računaju i ažuriraju pri promeni vrednosti stranica a i b
  • pomoćnu metodu za ispis podataka o pravougaoniku

Uz sve navedeno, potrebno je udesiti i to da četiri polja uvek budu u međusobnim odnosima koji odgovaraju pravilima geometrije (a da li će takav poredak biti uspostavljen "sam od sebe" - videćemo u nastavku (mada verujemo da već znate odgovor)) ....

Definisaćemo prvo najosnovnije okvire klase (tj. navešćemo samo polja sa podacima), i stoga klasa na početku ima 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:
	# U Python-u, polja klase ne definišu se
	# eksplicitno, već preko tzv. konstruktorske
	# funkcije (što ćemo uskoro prikazati), s tim
	# da je bitno napomenuti da, ako se polja klase
	# navedu direktno unutar tela klase (npr. na mestu
	# komentara koje trenutno čitate, takva polja se
	# smatraju "statičkim" poljima (što je tematika
	# kojom ćemo se baviti nešto kasnije u članku)
		
	
Slika 6. - Klasa pravougaonik - početak (klasa za sada sadrži samo polja za upis podataka).

Preko četiri polja klase Pravougaonik (četiri promenljive tipa Double), beleže se stranice pravougaonika (a i b), kao i obim i površina (polja O i 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, s obzirom na to da čitaocima mogu biti zanimljiviji drugi jezici, spremili smo i primere iz popularnih OOP jezika kao što su C++ i Java (koji se koriste u prvim godinama studija), a tu je i Python (v3).

Da biste automatski prebacili sve primere na drugi jezik, kliknite na odgovarajuće dugme za izbor jezika iznad bilo kog bloka programskog koda, posle čega će automatski biti promenjen jezik i u ostalim blokovima (na primer, da biste izabrali Javu kao jezik koji će biti korišćen 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, klasa se već može koristiti za zapis podataka; naravno - uz napomenu da će u svemu biti nepredviđenih okolnosti koje i te kako zahtevaju pažnju. :)

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

  • kako se objekti kreiraju (tj. 'instanciraju')
  • zašto je neophodno da veze među poljima budu pravilno uspostavljene (kroz postupak 'enkapsulacije')

Kreiranje objekta (rezervisana reč new)

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

C#
C++
Java
Python 3
		
Pravougaonik P1 = new Pravougaonik();
		
	
		
Pravougaonik p1 = Pravougaonik();

/*
	Moguće je napraviti i
	sledeću deklaraciju ....

	Pravougaonik p1;

	.... bez naredbe dodele, 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 se može pristupati poljima objekta:

C#
C++
Java
Python 3
        	
// Dodela vrednosti:
P1.a = 12.55;
P1.b = 10.40;
// Ispis:
Console.WriteLine(P1.a);
Console.WriteLine(P1.b);
		
	
    	
// Dodela vrednosti:
p1.a = 12.55;
p1.b = 10.40;
// Ispis:
std::cout << p1.a;
std::cout << p1.b;
		
	
    	
// Dodela vrednosti:
p1.a = 12.55;
p1.b = 10.40;
// Ispis:
System.out.printf("%f\n", p1.a);
System.out.printf("%f\n", p1.b);
		
	
    	
# Dodela vrednosti:
p1.a = 12.55
p1.b = 10.40
# Ispis:
print(str(p1.a))
print(str(p1.b))
		
	
Slika 8. - Primer pristupa 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 takođe da se rezervisana reč new (očigledno) ne pojavljuje u svim jezicima koje u članku koristimo za primere, ali, opšti princip instanciranja objekata je veoma sličan.

Klasa nije automatizovan šablon koji rešava "sve" probleme "magijskim putem"/"tek tako", odnosno (drugim rečima): time što klasama dajemo imena koja nama nešto znače * (a računaru - "baš ništa"), ne dolazimo u priliku da "neposredno" ** definišemo sve moguće objekte koji nam padaju na pamet (geometrijska tela, vozila, uređaje i sl), 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, međutim, za pravilno uspostavljanje odnosa među podacima - moramo se pobrinuti sami.

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

* Da pojasnimo: klasama treba davati prigodna i prepoznatljiva imena (isto kao i objektima i 'običnim' promenljivama i drugim entitetima u računarskim kodovima), ali ....

** Zarad definisanja željenih karakteristika klase, nije dovoljno samo dati 'ime' klasi (već je potrebno da samostalno osmislimo i navedemo sve neophodne delove programskog koda).

Pomenimo usput i to da je uobičajeno da nazivi klasa počinju velikim slovom ("Pravougaonik"), što ostavlja mogućnost da se objekat (određene klase), nazove istim imenom - koje počinje malim slovom ("pravougaonik"), čime se pravi razlika između objekta i klase.

Naravno, kao što se vidi iz dosadašnjih primera, moguće je objektima davati i druge, proizvoljne nazive (ali, još jednom - uputno je da nazivi budu jasni i prikladni (tj. uputno je da naziv objekta jasno ukazuje na svrhu objekta)).

Šta se tačno dešava sa podacima koje nismo inicijalizovali sami ('ručno')?

U većini OOP jezika, * 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 vrednošću 0 (nula), niske se inicijalizuju kao prazne niske (""), a pokazivači imaju vrednost null.

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

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

    	
P1.a = 12.55
P1.b = 10.40
P1.O = 0
P1.P = 0
		
	
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), i stoga ćemo preduzeti dalje korake da "popravimo situaciju", uvođenjem metoda koje će regulisati stanje različitih polja.

* Još jedna usputna napomena: "implicitna inicijalizacija" polja tipično funkcioniše onako kako smo pokazali, ali - 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).

Iako postupak koji sledi u nastavku neće rešiti "sve probleme", prvo ćemo se pobrinuti da makar početno stanje objekta bude korektno.

U kontekstu proceduralnog programiranja, inicijalizacija promenljivih je jedna od najbitnijih operacija, i (kao što znamo), u pitanju je postupak preko koga se promenljivama zadaje početna (tj. "inicijalna") vrednost.

U primeru koji proučavamo, polja a i b dobila su vrednosti tek naknadno, a poljima O i P nedostaje početno stanje koje odgovara upisanim stranicama, i stoga će inicijalizacija polja biti prvo na šta ćemo obratiti pažnju u nastavku.

Jasno je da računanje (tj. ažuriranje) obima i površine podrazumeva korišćenje odgovarajućih metoda, i jasno je da se objekti (za razliku od 'običnih' promenljivih), ne mogu inicijalizovati prostom naredbom dodele, već je potrebno koristiti posebne metode.

Konstruktor - metoda za definisanje početnog stanja objekta

Konstruktor (klase) 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 da vrednosti polja Obim i Povrsina zavise od vrednosti stranica).

U pisanju (u većini C-olikih jezika), konstruktor se prepoznaje po tome što nema povratni tip (za razliku od ostalih metoda), 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:

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

.... posle čega se početno stanje objekta može zadati 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 ovoga puta predaju konkretni argumenti (koji će biti prosleđeni odgovarajućim poljima). *

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

* Kao što smo takođe već videli, rezervisana reč new ne koristi se u svim jezicima, ali, ostavićemo č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 u Python-u konstruktorska funkcija nosi naziv __init__ i sl), i od sada ćemo skretati pažnju samo na situacije koje mogu stvoriti zabunu.

Uvođenjem dve nove metode (za računanje obima i za računanje površine), koje će potom biti pozvane iz konstruktora - problem će biti rešen. *

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

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

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

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

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 13. - Ponovni poziv konstruktora klase.

.... objekat biti korektno inicijalizovan.

Pri pozivu konstruktora, pozivaju se i metode za računanje obima i površine, i stoga sva četiri polja dobijaju korektne početne 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 instrukcija sa slike #14.

Konačno smo došli do toga da je (bar) početno stanje objekta pravilno zadato, a ako nam prethodni poziv "nije po volji", možemo kreirati i 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

	def __init__(self, a, b):
		self.a = a
		self.b = b
		racunanje_obima()
		racunanje_povrsine()

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

	def racunanje_povrsine():
		P = a * b

	def konzolni_ispis():
		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 potom možemo pozvati novu metodu na sledeći način (što je svakako (bar ponešto) elegantnije od navođenja zasebnih instrukcija za ispis):

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

Programski kod sada jeste znatno 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).

Kao što smo još ranije nagovestili, skup OOP tehnika za obavljanje "sigurnosnih provera" preko kojih se rešavaju prethodno navedeni problemi, nosi naziv enkapsulacija, i upravo će enkapsulacija biti glavna tema sledećeg poglavlja, ali, pre toga ćemo napraviti i kratak osvrt na rezervisanu reč this (koju smo u gornjim primerima koristili u konstruktorima).

Napomena: u okviru jedne klase moguće je definisati više (od jednog) konstruktora, ali, nećemo takve kodove prikazivati direktno u nastavku, već, u zasebnom poglavlju pred kraj (kada ćemo tzv. preopterećivanju metoda posvetiti više pažnje).

Rezervisana reč "this"

U praktičnom smislu, rezervisana reč this koristi se za rešavanje nedoumica koje mogu nastati u zapisu programskog koda, u situacijama kada parametri konstruktora (kao i drugih metoda), imaju iste nazive kao polja klase.

.... što je prilično uobičajeno u mnogim konstruktorima sa kojima ćete se sretati (a nije neuobičajeno ni u ostalim metodama).

Da pojasnimo dodatno, preko jednostavnog primera ....

Kada je objekat (već) kreiran, poljima se pristupa uz navođenje identifikatora objekta i polja, pri čemu su identifikatori spojeni preko odgovarajućeg operatora . (na primer, P1.a - kao način pristupa stranici a objekta P (koji predstavlja pravougaonik)).

Navedeni način pristupanja poljima i metodama moguć je ako je objekat već kreiran, ali, u samoj klasi Pravougaonik, nije moguće (npr) koristiti poziv "Pravougaonik.a"!

Rezervisana reč this koristi se u situacijama kada je, pri definisanju klase, potrebno obratiti se "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
	racunanje_obima()
	racunanje_povrsine()
		
    
Slika 18. - Preko rezervisane reči "this", obraćamo se poljima klase koja imaju iste nazive kao parametri konstruktora i drugih metoda (što bi inače moglo stvoriti "zabunu").

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

Klase za smeštaj podataka

Na ovom mestu napravićemo malu digresiju i ukratko ćemo se osvrnuti na to da povremeno postoji potreba za klasama koje se koriste samo za smeštaj podataka (u stranoj literaturi, za ovakve klase koristi se odrednica "data class"), pri čemu se "klase za podatke" mogu kreirati preko sintakse koja je već prikazana u članku.

Praktično su u pitanju strukture podataka nalik na slogove (struct), i takvi "slogovi" se mogu koristiti onda kada ne postoji potreba za proverom i međusobnim usklađivanjem podataka.

Na primer, ukoliko se uvoze polja iz baze podataka, pri čemu podaci u poljima ne moraju biti međusobno usklađeni (i pri tom niske mogu biti prazne i sl), već sada 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že se koristiti samo onda kada zaista nije potrebno proveravati sadržaj različitih 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 (pre svega), budući da još uvek nije dovršena - vraćamo se na glavni posao ....

Enkapsulacija

Termin "enkapsulacija" označava skup tehnika koje za cilj imaju:

  • definisanje pravila za pristupanje poljima i metodama klase (i istovremeno)
  • uspostavljanje prikladnih veza između podataka u poljima

Međutim, u još osnovnijem smislu, enkapsulacija u objektno orijentisanom programiranju predstavlja opštu ideju, koja nalaže da pristup podacima treba da bude regulisan tako da "unutrašnji mehanizmi" klase budu skriveni od krajnjih korisnika, ali (istovremeno) - tako da korisnicima klase bude omogućeno da neometano koriste sve neophodne opcije.

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), što znači da 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 i održavanje klase).

Pojava koju smo opisali često se naziva "skrivanje kompleksnosti", pri čemu (u zavisnosti od konkretnih okolnosti), opisani način definisanja klasa može dovesti i do potencijalnih problema u razvoju softvera.

Sa jedne strane, osnovna ideja jeste usmeravanje na bitne aspekte datog problema uz zanemarivanje nebitnih detalja, ali (sa druge strane - i budući da zapravo ne postoje pravila o tome koji elementi klase "moraju" biti privatni ili javni), 'skrivanje kompleksnosti' potencijalno može dovesti i do zabune/različitih nedoslednosti i sl, ako autor određene klase (slučajno ili namerno) odluči da 'sakrije' određene delove implementacije od potencijalnih korisnika, pri čemu je prevideo neki bitan detalj i sl.

Sa blagodetima enkapsulacije već smo se upoznali u velikoj meri - na posredan način - tako što smo posmatrali delove koda koji ne koriste enkapsulaciju (što je zapravo bila prilika da "naslutimo kako bi trebalo da bude").

U nastavku, sledi zvanično upoznavanje sa postupkom enkapsulacije, a počećemo upravo od primera sa kakvima smo se već sretali ....

Šta se događa ukoliko se ne koristi enkapsulacija?

Ako se setimo naredbe P1.a = 12.55; preko koje 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 (ukratko: same po sebi, zadate vrednosti predstavljaju tačne podatke, ali, unete vrednosti polja O i P nisu u skladu sa prethodno zadatim vrednostima polja a i b).

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

Međutim, u prethodnoj 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'. :)

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

Ako se dvoumite oko pomenute 'računice', recimo da površini 40 odgovaraju stranice 5 i 8, ali - isto tako - i stranice 4 i 10, 2 i 20, 3.2 i 6.25, kao i mnoge druge kombinacije (pošto je poljima a i b pripisan tip podatka Double, kombinacije se broje stotinama biliona).

U nastavku, slede postupci preko kojih se rešavaju najvažniji problemi sa kojima smo se prethodno susreli:

  • pri unosu stranica pravougaonika, vrednost koju je predao korisnik ne sme se prihvatiti bezuslovno
  • potrebno je postići da obim i površina budu automatski ažurirani pri promeni stranica a i b
  • potrebno je ukinuti mogućnost ručnog upisivanja vrednosti obima i površine, ali, istovremeno je potrebno ostaviti mogućnost čitanja obima i površine

U tehničkom smislu (što ćemo videti i u slučaju klase Pravougaonik), enkapsulacija se obavlja kritičkom upotrebom specifikatora pristupa - u kombinaciji sa javnim metodama koje regulišu pristup privatnim poljima.

U C#-u se mogu definisati i tzv. svojstva, preko 'pristupnika' * get i set (kasnije ćemo pokazati primere).

* U originalu: (eng.) "accessor".

Upravo smo izneli 'podosta' novih/nepoznatih pojmova koje u nastavku treba detaljno razjasniti, i (naravno), iako će nam novopomenute tehnikalije u prvom trenutku pomalo "zakomplikovati život", takođe će uneti i red kao što je neophodno (pogotovo u dugoročnom smislu), i stoga ćemo konačno biti u stanju da "doteramo" klasu Pravougaonik kako dolikuje.

Specifikatori pristupa (private, public, protected)

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

  • private - poljima i metodama moguće je pristupati samo unutar klase, i nije moguće pristupati im iz spoljnih delova koda
  • public - poljima i metodama moguće je neposredno pristupati iz spoljnih delova koda (naravno, i unutar klase)
  • protected - poljima i metodama moguće je pristupati unutar osnovne klase, ali i preko nasleđenih klasa, *** međutim, nije moguće elementima pristupati iz spoljnih delova koda

Do sada je uz sva polja klase Pravougaonik stajala odrednica public, pri čemu je u pitanju rezervisana reč koja (u skladu sa prethodno navedenim pravilima), sugeriše spoljnjim delovima programskog koda da mogu neposredno pristupati svim poljima klase kojima je pripisan navedeni specifikator pristupa.

Dobra strana pomenute "otvorenosti" je to što se poljima može pristupati neposredno, a loša strana, to što mogućnost direktne izmene polja koja imaju specifikator pristupa public ostavlja prostora za naredbe dodele koje mogu (bar u najgorem slučaju), dovesti u pitanje korektnost celog 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, umesto eksplicitnog navođenja specifikatora pristupa, koriste se sledeće konvencije pri imenovanju polja:

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

** Kolokvijalno, 'mogućnost pristupa' određenom elementu (klase), može se shvatiti i kao 'vidljivost' (datog elementa).

*** O nasleđivanju ćemo diskutovati nešto kasnije u članku.

U svakom slučaju, najviše pažnje (u nastavku) biće posvećeno specifikatorima pristupa private i public (sa kojima se programeri najčešće i sreću).

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

U slučaju klase Pravougaonik, prepravićemo specifikator pristupa 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. - "Skrivanje" polja klase preko specifikatora pristupa private, tipično predstavlja prvi korak u procesu enkapsulacije (u sledećem koraku, kreiraćemo metode preko kojih se kontroliše pristup "skrivenim" vrednostima).

.... i sada, pošto su polja a, b, O i P proglašena 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, pa, ako nemogućnost pristupa poljima ne predstavlja problem, ne moraju se preduzimati dalji koraci, međutim, u slučaju klase Pravougaonik (koja simbolički predstavlja tipičan slučaj projektovanja klase), * 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), i stoga ćemo preduzeti korake da omogućimo pristup poljima a, b, O i P - na adekvatan način.

Za početak, razmotrićemo (u sledećem odeljku), na koji način se privatnim poljima može pristupati preko javnih metoda.

* U dizajnu različitih klasa, najčešće se srećemo (baš kao u primeru koji smo izabrali), sa poljima koja sama po sebi praktično 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", ili (ređe), potpuno "zatvaranje".

Potpuno otvaranje jeste moguće (uglavnom u manjim klasama), ali - samo onda kada smo potpuno sigurni da upis proizvoljnih vrednosti u public polja ne stvara probleme (kao što je bio slučaj u primeru klase za smeštaj podataka iz baze).

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

** U suprotnom, ako se polja definišu preko specifikatora pristupa public, nastaje 'zbrka' koja je slična situaciji koju smo opisali na početku (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), * može se definisati na sledeći način:

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 metoda Citanje_a (koja vraća vrednost polja a).

* Naravno, po istom principu se mogu definisati metode i u drugim klasama.

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 (ne samo tekstualni ispis).

Za upis (tj. 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,
	// obim i površina se ažuriraju

	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,
	// obim i površina se ažuriraju

	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,
	// obim i površina se ažuriraju

	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,
	# obim i površina se ažuriraju

	self.a = a
	racunanjeObima()
	racunanjePovrsine()
		
    
Slika 23. - Javno dostupna metoda Upis_a (metoda služi za upis predate vrednosti u polje a, ali - samo ako je predata vrednost pozitivna (nakon upisa, ažuriraju se obim i površina)).

Preko postupka koji smo prikazali, efikasno su rešeni 'najvažniji problemi enkapsulacije' (koje smo pominjali u ranijim odeljcima):

  • ulazni podatak ne prihvata se bezuslovno - već se proverava (na primer, ukoliko je unet negativan broj ili nula, ne ažurira se vrednost polja a, niti se pozivaju metode za ažuriranje obima i površine)
  • ažuriranje obima i površine pri zadavanju nove vrednosti stranice - obavlja se (praktično) automatski

Klasa je sada konačno spremna za upotrebu, i možemo se osvrnuti na šematske prikaze klase koju smo definisali 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 za obradu podataka (međutim, "iznutra", objekat sadrži "sve što treba").

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

Preko konkretnog primera (na gornjim slikama), prikazan je opšti princip koji je ranije opisan: objekat "ispod haube" sadrži sve što je potrebno, ali, direktno se može pristupati samo određenim metodama.

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

Enkapsulacija preko svojstava u programskom jeziku C# (pristupnici get i set)

Videli smo da je moguće pristupati privatnim poljima preko javnih metoda (tj. metoda koje imaju specifikator pristupa public), a videli smo i da je glavni razlog zašto se polja klase uopšte "skrivaju", 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 (npr. obim i površina pravougaonika zavise od vrednosti stranica)
  • u drugim situacijama, potrebno je 'zaštititi' sadržaj samih polja (npr. vrednost stranice pravougaonika ne sme biti negativan broj ili nula; vrednosti polja O i P ne smeju se upisivati 'ručno' i sl)

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 nama poznati), ali, neki jezici nude i dodatne mogućnosti.

U jeziku C#, moguće je definisati tzv. "svojstva" (eng. properties).

Gledano sa 'spoljne' strane, svojstva u C#-u podsećaju na polja, ali, u pitanju su specijalni elementi klasa koji takođe poštuju i enkapsulaciju (postoje jasna pravila u smislu mogućnosti čitanja i upisa - što ćemo u nastavku objasniti podrobnije).

Princip definisanja svojstava u C#-u, u suštinskom smislu je veoma sličan principu upotrebe javnih metoda zarad pristupa privatnim poljima, ali, zapis je poneš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 pristupnika (set i get). U opštem smislu, svojstvo je svojevrsna kombinacije polja i metode: spolja gledano, izgleda kao public polje, dok - po potrebi - može funkcionisati kao metoda.

Da pojasnimo šta znače dopune koje smo videli u primeru ....

U sledećem delu klase:

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.

.... definisano je svojstvo a koje će biti dostupno spoljnim delovima koda (specifikator pristupa svojstva je public), a unutar zagrada su definisana dva bloka.

Preko prvog bloka, 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. - Pristupnik get koji vraća vrednost polja _a.

.... određeno je da će programski kod koji zatraži vrednost svojstva a, dobiti (zapravo) 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 programskog koda koji prikazuje upotrebu svojstva a (naizgled, veoma slično prethodnoj situaciji, kada je postojalo javno polje a, ali - samo naizgled).

.... dobija se ispis stranice a.

Pozivanjem svojstva P1.a dobija se vrednost polja P1._a, i zatim se poziva opšta funkcija ToString() preko koje se ispisuje vrednost.

Preko drugog bloka, 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 pristupnik 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đeno je da će polje _a dobiti predatu vrednost (value), ali - samo u slučaju da je predata vrednost veća od nule.

Pre svega, primećuje se sličnost sa pristupom iz prethodnog odeljka (kada je korišćena metoda Upis_a), ali, verujemo da pažnju čitalaca privlači rezervisana reč value (koja može delovati pomalo "zbunjujuće").

Pojava rezervisane reči value najlakše se može razumeti ako odgovorimo na sledeće 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), i - sa druge strane - 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.

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

Summa summarum, svojstvo se (najpraktičnije) može shvatiti kao svojevrsna kombinacija polja i metode:

  • spolja gledano (tj. u situacijama kada mu se pristupa iz spoljnih delova koda), svojstvo izgleda kao polje, ali ....
  • "iznutra" funkcioniše kao metoda.

U primeru koji razmatramo (koji služi kao tipičan primer upotrebe svojstava):

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

.... i (takođe), u opštem smislu, jedan od dva bloka se može 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 omogućavaju zadavanje vrednosti direktno (što je svakako dobra praksa).

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

Takođe, primetili smo i to da sada - kada se spoljnim delovima koda "otkrivaju" svojstva koja nose prethodne nazive polja (a, b, O i P) - sama polja nose nazive sa prefiksom: _a, _b, _O i _P.

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

Statički elementi klase

Da bismo zaokružili (uvodnu) diskusiju o enkapsulaciji, osvrnućemo se još na tzv. "statičke" elemente klase koji ne pripadaju pojedinačnim instanciranim objektima već - samoj klasi, ali (naravno), pošto je u pitanju netrivijalna ideja kojoj je potreban 'prevod' (tj. razrada), ukratko ćemo se osvrnuti na najbitnije detalje.

Za početak, podsetićemo se na to da sva polja instanciranih objekata (npr. polja a, b, O i P objekata klase Pravougaonik), predstavljaju zasebne/nezavisne podatke - što je u većini situacija 'povoljna okolnost', ali, određeni problemi zahtevaju malo drugačiji pristup, sa čime ćemo se upoznati tako što ćemo pokušati da rešimo jedan od 'najtipičnijih' problema u objektno orijentisanom programiranju, a taj problem je - kako prebrojati instancirane objekte klase?

Načelno, "nestatički" elementi različitih objekata mogu deliti podatke (tj "informacije"), ali, nije teško uvideti da - ako bi svaki objekat klase Pravougaonik sadržao podatak o ukupnom broju kreiranih objekata - takav pristup bi samo doveo do različitih 'nelogičnosti i nedoslednosti', nepotrebnog 'rasipanja' memorijskog prostora i sl.

Tačnije, bez statičkih polja, problem se praktično ni ne može rešiti na iole jednostavan način: mnogim čitaocima može pasti na pamet ideja o uvođenju "običnog" polja brojInstanci čija bi se vrednost uvećavala pri pozivu konstruktora, ali, pitanja su: kako inicijalizovati takvo polje i kako uvećavati vrednost.

Verujemo da nije teško uvideti da se polje ne može inicijalizovati unutar klase (tako da zadatak bude zapravo rešen), a što se tiče inkrementacije polja brojInstanci preko konstruktora - tehnički je izvodljivo, ali, pretpostavljamo da je (ipak :)) jasno da bismo preko navedenog pristupa samo uspeli da kreiramo 'gomilu' nezavisnih polja brojInstanci koja pripadaju pojedinačnim objektima (i pri tom svako od takvih polja ima vrednost 1).

Problem se može rešiti uvođenjem posebnog polja koje ne pripada objektima (tj. svakom pojedinačnom objektu ponaosob), već 'pripada klasi' * i označava se preko rezervisane reči static.

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	....
	public static UInt32 brojInstanci;
}
	
	
class Pravougaonik {
public:
	static unsigned long brojInstanci;
	....
}
		
    
    	
public class Pravougaonik
{
	....
	public static int brojInstanci;
}
		
    
    	
class Pravougaonik:
	broj_instanci
# Kao što smo ranije nagovestili,
# polja koja se deklarišu direktno
# u okviru klase (tj. van metoda i sl),
# smatraju se statičkim elementima
		
    
Slika 33. - Primer deklarisanja statičkog polja.

* Jednostavno: za svako polje brojInstanci koje bi bilo definisano kao "nestatički" element klase, rezerviše se zaseban prostor u memoriji, dok je statičko polje brojInstanci jedinstven podatak u memoriji, koji je dostupan svim objektima klase.

Statičkim poljima (koja "ne pripadaju objektima") pristupa se preko klase, * i pri tom se statički članovi inicijalizuju eksplicitno, izvan klase:

C#
C++
Java
Python 3
    	
Pravougaonik.brojInstanci = 0;
		
	
Pravougaonik::brojInstanci = 0;
		
    
    	
Pravougaonik.brojInstanci = 0;
		
    
    	
Pravougaonik.broj_instanci = 0
		
    
Slika 34. - Primer inicijalizacije statičkog polja klase.

* Dakle: Pravougaonik.brojInstanci, umesto pravougaonik1.brojInstanci ili P1.brojInstanci i sl.

U konkretnom primeru, može se udesiti da se vrednost (statičkog) polja brojInstanci uveća pri svakom pozivu konstruktora ....

C#
C++
Java
Python 3
    	
public class Pravougaonik
{
	// konstruktor klase:
	public Pravougaonik(Double a, Double b)
	{
		// ostale naredbe
		// iz konstruktora ....
		brojInstanci++;
	}

}
	
	
class Pravougaonik {
	public:

	// konstruktor klase:
	Pravougaonik(double a, double b) {
		// ostale naredbe
		// iz konstruktora ....
		brojInstanci++;
	}

}
		
    
    	
public class Pravougaonik {
	// konstruktor klase:
	public Pravougaonik(double a, double b)
	{
		// ostale naredbe
		// iz konstruktora ....
		brojInstanci++;
	}

}
		
    
    	
class Pravougaonik:
	# konstruktor klase: 
	__init__(self, a, b):
		# ostale naredbe iz
		# konstruktora ....
		broj_instanci += 1
		
    
Slika 35. - Uvećanje vrednosti polja brojInstanci preko konstruktora klase.

.... i kasnije se takva vrednost može očitavati (kao i bilo koje drugo polje ili svojstvo): *

C#
C++
Java
Python 3
    	
Console.WriteLine(Pravougaonik.brojInstanci);
		
	
std::cout(Pravougaonik.brojInstanci);
		
    
    	
System.out.printf("%d\n", Pravougaonik.brojInstanci);
		
    
    	
print(Pravougaonik.broj_instanci)
		
    
Slika 36. - Očitavanje vrednosti polja brojInstanci.

* Na primer, ukoliko smo u međuvremenu kreirali tri pravougaonika, ispis će biti "3".

Pred kraj, pozabavićemo se temom nasleđivanja u objektno orijentisanom programiranju, kao i preopterećivanjem metoda i preklapanjem operatora.

Nasleđivanje i polimorfizam

Naveli smo ranije da je jedan od najvažnijih zadataka objektno orijentisanog programiranja (možda i glavni cilj) - dobra organizacija podataka.

Da bismo razumeli svrhu nasleđivanja u OOP-u (u prethodno navedenom kontekstu), zamislićemo sledeću situaciju:

  • 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, ali - sadrži i polja koja su specifično vezana za radnike (npr. RadnoMesto, PocetakRada, RadniStaz i sl.)

* Nećemo ovoga puta koristiti klasu Pravougaonik, već drugi primer (koji je jednostavniji).

Problem se može rešiti definisanjem dve potpuno nezavisne klase, međutim, možemo primeniti i racionalniji pristup koji omogućava određene pogodnosti.

Nasleđivanje

Nasleđivanje, kao tehnika OOP-a, podrazumeva (u praktičnom smislu), definisanje izvedenih klasa koje direktno preuzimaju strukturu postojećih klasa (polja i metode), pri čemu (naravno/očekivano) postoji mogućnost uvođenja novih polja i metoda.

Navedeni pristup koristi se u situacijama kada određene klase (dve ili više), dele zajedničke osobine, iz čega praktično proizilazi da bi višestruko navođenje istih polja i metoda (u dve ili više različitih klasa) - dovelo do nepotrebnog 'gomilanja' programskog koda.

.... a potencijalno i do pojave grešaka i/ili različitih nedoslednosti - vrlo lako. :)

Sve što smo naveli, bolje ćemo razumeti preko praktičnog primera ....

Prvo je potrebno definisati opštiju od dve klase (tj. klasu koja sadrži zajednička polja i/ili metode), što je u primeru koji razmatramo 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.datum_rodjenja = datumRodjenja
		self.racunanjeStarosti()

	def racunanje_starosti(self):
		datum          = datetime.datetime.now()
		self.__starost = datum.year - self.datum_rodjenja.year

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

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

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

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

Umesto "prepisivanja", klasa Radnik će naslediti klasu Osoba, čime se praktično omogućava korišćenje polja i metoda klase Osoba - u okviru klase Radnik.

U C#-u: moguće je koristiti i svojstava.

Naravno (kao što smo već naveli), postoji mogućnost da se klasi Radnik pridodaju i nova/zasebna 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, datum_rodjenja, radno_mesto, pocetak_rada):
		self.radno_mesto  = radno_mesto
		self.pocetak_rada = pocetak_rada
		Osoba.__init__(self, ime, prezime, datum_rodjenja)
		self.racunanje_radnog_staza()

	def racunanje_radnog_staza(self):
		datum             = datetime.datetime.now()
		self.__radni_staz = datum.year - self.pocetak_rada.year

		if datum.month < self.pocetak_rada.month:
			self.__radni_staz = self.__radni_staz - 1

		if datum.month == self.pocetak_rada.month and
		   datum.day  <  self.pocetak_rada.day:
			self.__radni_staz = self.__radni_staz - 1

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

Kratka analiza

Preko sledećeg programskog koda, definisano je nasleđivanje klase:

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

}
		
    
    	
class Radnik : public Osoba
{

};
		
    
    	
public class Radnik extends Osoba {

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

u C#-u i C++-u, znak : označava da "jedna klasa nasleđuje drugu", u Javi se koristi rezervisana reč extends, a u Python-u se osnovna klasa navodi unutar zagrade.

U konkretnom primeru, klasa Radnik nasleđuje klasu Osoba, što praktično znači da klasa Radnik ima sva polja, metode (i svojstva) koja odlikuju klasa Osoba - i pri tom je moguće dodavati nova polja (koja su svojstvena samo klasi Radnik) i nove metode (koje su takođe svojstvene samo klasi Radnik), kao i svojstva u C#-u.

Da pojasnimo dodatno: u konstruktoru klase Radnik ....

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, datum_rodjenja, radno_mesto, pocetak_rada):
	self.radno_mesto  = radno_mesto
	self.pocetak_rada = pocetak_rada
	Osoba.__init__(self, ime, prezime, datum_rodjenja)
	self.racunanje_radnog_staza()
		
    
Slika 40. - Konstruktor klase Radnik - koji poziva konstruktor klase Osoba (ali takođe koristi sopstvena polja i pozive naredbi).

.... preko sledećeg koda ....

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

.... praktično se poziva konstruktor klase Osoba, što podrazumeva inicijalizaciju tri navedena polja i pozivanje metode za računanje starosti, ali, budući da konstruktor klase Radnik sadrži i druge parametre i poziv metode za računanje radnog staža, vidimo da nije u pitanju klasa Osoba, već je u pitanju druga/zasebna klasa (i vidimo da je povezivanje podataka koje je ostvareno na prikazani način, optimalno i praktično).

Polimorfizam

Termin polimorfizam (u prevodu, "višeobličje" ili "višeobličnost"), u objektno orijentisanom programiranju tipično * se odnosi na sledeće pojave:

  • u okviru osnovne klase i izvedenih klasa (jedne ili više), moguće je implementirati metode istog naziva - na različite načine (u pitanju je tzv. "dinamički polimorfizam")
  • u okviru jedne klase, moguće je (uz poštovanje određenih pravila), definisati više metoda istog naziva ** (u pitanju je tzv. "statički polimorfizam")
  • u određenim okolnostima, tj. pod određenim uslovima, *** postoji mogućnost ravnopravnog (tj. "istovremenog") korišćenja osnove klase i izvedenih klasa, što u najpraktičnijem smislu znači da se objekti osnovne klase i izvedenih klasa mogu zajednički smeštati u kolekcije podataka, i takođe im se može pristupati preko istih metoda i sl. (u pitanju je tzv. "hijerarhijski polimorfizam")

Razmotrićemo primere u kojima se pojavljuju klase koje smo već koristili.

* Zarad preglednosti, u uvodnom članku ćemo se zadržati samo na najvažnijim vidovima polimorfizma (a ostalima, koji uglavnom predstavljaju podvrste i "varijacije na temu", više pažnje ćemo posvetiti drugom prilikom).

** Tzv. "preopterećivanje metoda" je pojava na koju smo se već osvrnuli kada smo diskutovali o konstruktorima, ali, budući da je takođe u pitanju pristup koji se često javlja i van konteksta nasleđivanja, preopterećivanje metoda smo izdvojili u zasebno poglavlje članka.

*** Kao što smo već nagovestili: u određenim okolnostima, ali - ne uvek!

(U nastavku, o svemu ćemo prodiskutovati detaljnije.)

Dinamički polimorfizam

Ukoliko je u okviru osnovne klase definisana određena metoda, objekti koji nastaju instanciranjem nasleđene klase mogu pozivati datu metodu, međutim, moguće je u okviru nasleđene klase implementirati i novu metodu sa istim nazivom - tako da implementacija bolje odgovara potrebama nasleđene klase.

U prvom primeru, ukoliko je u okviru klase Osoba definisana metoda IspisPodataka, na sledeći način ....

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

	// konstruktorska metoda i druge metode
	// koje smo prethodno prikazali, izostavljene
	// su zarad bolje preglednosti

	public void IspisPodataka() {
		Console.WriteLine("Ime: " + Ime + " " + Prezime);
		Console.WriteLine("Datum rođenja" + DatumRodjenja.ToString());
	}
}
		
    
    	
class Osoba {
	private:

	int starost;

	public:

	string  ime, prezime;
	Datum datumRodjenja;

	// konstruktorska metoda i druge metode
	// koje smo prethodno prikazali, izostavljene
	// su zarad bolje preglednosti

	ispisPodataka() {
		std:cout << "Ime: " << ime << " " << prezime;
		std:cout << "Datum rođenja: " << ispisDatuma(datumRodjenja);
	}
}
		
    
    	
public class Osoba {
    private String ime, prezime;
    public LocalDate datumRodjenja;

	// konstruktorska metoda i druge metode
	// koje smo prethodno prikazali, izostavljene
	// su zarad bolje preglednosti

	public void ispisPodataka() {
		System.out.printf("Ime: %s %s", ime, prezime);
		System.out.printf("Datum rođenja: %s", datumRodjenja.toString());
	}
}
		
    
    	
class Osoba:
	# konstruktorska metoda i druge metode
	# koje smo prethodno prikazali, izostavljene
	# su zarad bolje preglednosti

	def ispis_podataka(self):
		print("Ime: " + __ime + " " + __prezime);
		print("Datum rođenja: " + str(__datum_rodjenja);
	}
}
		
    
Slika 42. - Implementacija pomoćne metode za ispis podataka, u okviru osnovne klase Osoba.

.... u okviru klase Radnik moguće je definisati metodu IspisPodataka tako da se u ispisu pojave i podaci koji su svojstveni samo osobama čiji su podaci zapisani preko nasleđene klase:

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

	// konstruktorska metoda i druge metode
	// koje smo prethodno prikazali, izostavljene
	// su zarad bolje preglednosti

	public void IspisPodataka() {
		Console.WriteLine("Ime: " + Ime + " " + Prezime);
		Console.WriteLine("Datum rođenja:" + DatumRodjenja);
		Console.WriteLine("Pozicija:" + _RadnoMesto);
		Console.WriteLine("Aktivan od: " + _PocetakRada.ToString());
	}
}
		
    
    	
class Radnik : public Osoba {
	private:

	Datum  pocetakRada;
	string pozicija;
	int    RadniStaz;

	// konstruktorska metoda i druge metode
	// koje smo prethodno prikazali, izostavljene
	// su zarad bolje preglednosti

	public:

	ispisPodataka() {
		std:cout << "Ime: " << ime << " " << prezime;
		std:cout << "Datum rođenja: " << ispisDatuma(datumRodjenja);
		std:cout << "Pozicija: " << pozicija;
		std:cout << "Aktivan od: " << ispisDatuma(pocetakRada);
	}
}
		
    
    	
public class Radnik extends Osoba {
    private String pozicija;
    private int staz;
    public LocalDate pocetakRada;

	// konstruktorska metoda i druge metode
	// koje smo prethodno prikazali, izostavljene
	// su zarad bolje preglednosti

	public void ispisPodataka() {
		System.out.printf("Ime: %s %s", ime, prezime);
		System.out.printf("Datum rođenja: %s", datumRodjenja.toString());
		System.out.printf("Pozicija: %s", pozicija);
		System.out.printf("Aktivan od: %s", pocetakRada.toString());
	}
}
		
    
    	
class Radnik (Osoba):

	# konstruktorska metoda i druge metode
	# koje smo prethodno prikazali, izostavljene
	# su zarad bolje preglednosti

	def ispis_podataka(self):
		print("Ime: " + __ime + " " + __prezime);
		print("Datum rođenja: " + str(__datum_rodjenja);
		print("Pozicija: " + __pozicija);
		print("Aktivan od: " + str(__pocetak_rada));
	}
}
		
    
Slika 43. - Implementacija pomoćne metode za ispis podataka, u okviru nasleđene klase Radnik.

Hijerarhijski polimorfizam

Termin "hijerarhijski polimorfizam" odnosi se (kao što smo ranije nagovestili), na okolnosti u kojima se objekti osnovne klase i izvedenih klasa koriste "istovremeno" (ukoliko je takav pristup moguć).

Da bismo se bolje upoznali sa time šta hijerarhijski polimorfizam predstavlja u praktičnom smislu, poslužićemo se primerom u kome ćemo smatrati da klase Osoba i Radnik koristimo zarad organizacije podataka o radnicima sopstvene firme i članova njihove bliže familije:

  • podaci o radnicima beleže se preko klase Radnik
  • podaci o ostalim osobama beleže se preko klase Osoba

Sledeći zahtev je - kreiranje i popunjavanje jedinstvene liste svih osoba (bez obzira na to da li je osoba radnik ili nije), * pri čemu bi se takva lista mogla koristiti (na primer), da se u određenim okolnostima pronađu:

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

* U konkretnoj implementaciji, smatramo da optimalan pristup podrazumeva korišćenje objedinjene liste (zarad bolje organizacije i uštede prostora).

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

C#
C++
Java
Python 3
    	
List<Osoba> SpisakOsoba = new List<Osoba>();
		
    
    	
list<Osoba> spisakOsoba;
		
    
    	
List<Osoba> spisakOsoba = new ArrayList<Osoba>();
		
    
    	
spisak_osoba = []
		
    
Slika 44. - 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);
		
    
    	
datum_rodjenja = datetime.datetime(1970, 5, 27)
osoba = Osoba("Dejana", "Marković", datum_rodjenja)
spisak_osoba.append(osoba)


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

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

Završićemo uz napomenu da "obrnuti pristup" jeste moguć (kreiranje liste objekata klase Radnik u koju bi se dodavali i objekti klase Osoba), ali - u tom slučaju potrebno je postupati vrlo pažljivo (ako već uopšte moramo da koristimo navedeni pristup (koji je nezanemarljivo rizičan)).

Na primer, sledeći kod ....

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

.... daje referencu na objekat sa kojim se može postupati na bilo koji ("realno mogući") način, u tom smislu da objekat koji referenciramo garantovano ima sva polja, metode i svojstva (naravno, u kontekstu toga šta objekat ikako može "imati"/"biti" u programu koji kreiramo).

U "obrnutim okolnostima" (ako smo kreirali listu objekata klase Radnik) ....

C#
C++
Java
Python 3
    	
Radnik R = spisakRadnika.Peek();
		
    
    	
Radnik r = spisakRadnika.front();
		
    
    	
Radnik r = spisakRadnika.peek();
		
    
    	
r = spisak_radnika[len(spisak_radnika) - 1]
		
    
Slika 47. - Preuzimanje reference na objekat iz liste: budući da se preuzeti objekat tretira kao objekat klase Radnik, moramo biti vrlo 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 se preko reference R referencira objekat klase Osoba - i pri tom se poziva neko od polja klase Radnik (na primer RadniStaz).

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

Preopterećivanje metoda

U odeljku koji je posvećen konstruktorima, nagovestili smo da postoji mogućnost da se u okviru klase pojavi više metoda istog naziva (bilo da je u pitanju konstruktor ili neka od metoda opšteg tipa), a u prethodnom odeljku osvrnuli smo se na to da se pomenuti pristup zvanično prepoznaje kao "statički polimorfizam", međutim, kao što smo takođe nagovestili, u pitanju je zanimljiva pojava u objektno orijentisanom programiranju koja nema obavezno veze sa "osnovnim i nasleđenim klasama" (već se često javlja i u klasama koje nisu deo "hijerarhije"), i takva pojava nosi opšti naziv "preopterećivanje metoda" *.

U nastavku, objasnićemo kako je (tj. pod kojim uslovima), moguće implementirati dve ili više metoda istog naziva u okviru jedne klase.

* Usput: š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 poglavlja).

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, a sada se vraćamo na upoznavanje sa teorijom).

Uzmimo za primer klasu koja definiše automobile (prikazaćemo vrlo uprošćen primer takve klase, u kome je akcenat na preopterećivanju metoda), i uzmimo da su unutar klase definisana dva konstruktora:

  • konstruktor koji zahteva unos marke i modela (automobila)
  • konstruktor koji omogućava da se unese i komentar
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
# ('zaobilazne metode' biće prikazane drugom prilikom)
		
    
Slika 48. - Klasa Automobil, preko koje se beleže samo najosnovniji podaci o automobilima (klasa prikazuje primer preopterećivanja konstruktora).

Uz preopterećivanje konstruktora, inicijalizacija se može 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
# ('zaobilazne metode' biće prikazane drugom prilikom)
		
    
Slika 49. - 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 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
# ('zaobilazne metode' biće prikazane drugom prilikom)
		
    
Slika 50. - Primeri preopterećivanja metode za računanje sume brojeva (u okviru klase Kalkulator).

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

Ponegde se koristi i prevod "preklapanje metoda", ali, bez obzira na to što takav prevod deluje elegantnije, mora se 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 isti operatori koriste pri obavljanju operacija sa osnovnim tipovima podataka.

U najosnovnijem tehničkom smislu, iza svega stoji sledeća ideja: za određeni operator (koji se "preklapa"), potrebno je definisati metodu koja opisuje način funkcionisanja operatora. **

Zamislićemo (u svojstvu primera), 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 zamislićemo da je unutar klase definisana metoda za sabiranje brojeva - koju treba povezati sa operatorom + ....

* u originalu, takođe "overload".

** Moguće je definisati novu metodu, a moguće je i da se operator poveže sa nekom od postojećih metoda unutar klase (što će biti slučaj u primeru koji ćemo prikazati).

*** 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, kao što smo se u prošlom poglavlju susreli sa situacijom da Python ne omogućava preopterećivanje metoda (onako kako to omogućavaju ostali jezici koje koristimo u članku), u ovom poglavlju se susrećemo sa time da Java ne dozvoljava preklapanje operatora.

Ali, da se vratimo na definiciju klase VelikiBroj ....

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,
	// preko koje se 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,
	// preko koje se 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:

    def __init__(self, cifre):
		self.cifre = [ ]
        self.popunjavanje_niza_cifara(cifre)

	# Prepustićemo vam da sami osmislite
	# metodu popunjavanje_niza_cifara,
	# preko koje se 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 51. - 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 52. - Pozivanje metode sabiranje (uz prethodno instanciranje objekata).

.... ni u kom slučaju ne deluje "neintuitivno i problematično", ali, mogućnost da se veliki brojevi (koji su definisani preko klase VelikiBroj), sabiraju onako kako se inače sabiraju 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 53. - Primer sabiranja celobrojnih promenljivih.

.... svakako deluje još zanimljivije (i prirodnije).

Operator sabiranja (u okviru klase VelikiBroj), može se "preklopiti", tako što će biti povezan 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 54. - Preklapanje operatora za sabiranje u okviru klase VelikiBroj.

.... posle čega se i veliki brojevi mogu 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 55. - Primer upotrebe "preklopljenog" operatora za sabiranje (u okviru klase VelikiBroj).

Preklapanje operatora predstavlja svojevrstan uvod u naprednije tehnike objektno orijentisanog programiranja (kao što su apstraktne i statičke klase, delegati, interfejsi i sl), međutim, ovoga puta se nećemo baviti navedenim temama, već ćemo to ostaviti za neku drugu priliku (uobličićemo članak "OOP 2", u kome ćemo se osvrnuti na sve ono na šta smo hteli da se osvrnemo u članku "OOP 1", ali, negde smo ipak morali "podvući crtu", budući da je uvodni članak dovoljno obiman sam po sebi).

Primeri za kraj ....

Za kraj, iako deluje pomalo nepotrebno posle svega što smo naveli, vratićemo se još jednom na pravougaonike i pogledaćemo primer jednostavne procedure preko koje je potrebno ustanoviti 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 (što podrazumeva da se pravougaonici definišu preko zasebnih promenljivih).

Prvo, OOP pristup:

C#
C++
Java
Python 3
    	
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
		
    
    	
Pravougaonik pravougaonici[3];
		
    
    	
Pravougaonik[] pravougaonici = new Pravougaonik[3];
		
    
    	
pravougaonici = [ ]
		
    
Slika 56. - Kreiranje niza objekata koji su izvedeni iz klase pravougaonik.

Ako je potrebno pronaći veći među prva dva pravougaonika, ceo kod može se zapisati na sledeći način:

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[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 = [ ];

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

.... pri čemu bi situacija postala još "očiglednija" ukoliko bi bilo potrebno pronaći najveći od n pravougaonika. :)

Međutim, setićemo se na ovom mestu i primedbe iz uvodnih pasusa, o tome da tehnike objektno orijentisanog programiranja ne treba koristiti 'uvek i bez potrebe' (to jest, setićemo se toga da je proceduralno programiranje, u određenim okolnostima, takođe krajnje dobar i primeren način za rešavanje 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 nije uputno koristiti OOP pristup "za svaku sitnicu", već samo onda kada zatreba, a - ako zatreba - sada znamo kako se ideje mogu sprovesti u delo. :)

Autor članka Nikola Vukićević Za web portal codeblog.rs
Napomena: Tekstovi, slike, web aplikacije i svi ostali sadržaji na sajtu codeblog.rs (osim u slučajevima gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta 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-2026. Sva prava zadržana.
Facebook LinkedIn Twitter Viber WhatsApp E-mail
početna > Članci > Uvod u objektno orijentisano programiranje
codeBlog codeBlog
Sajt posvećen popularizaciji kulture i veštine programiranja.
Napomena: Tekstovi i slike na sajtu codeblog.rs (osim u slučajevima, gde je drugačije navedeno) predstavljaju intelektualnu svojinu autora sajta 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-2026. Sva prava zadržana.
Facebook - logo
Instagram - logo
LinkedIn - logo
Twitter - logo
E-mail
Naslovna
   •
Uslovi korišćenja
   •
Obaveštenja
   •
FAQ
   •
Kontakt