Uvod
Objektno orijentisano programiranje je programska paradigma (pristup/metoda), koja podrazumeva rešavanje problema kreiranjem i međusobnom interakcijom objekata.
- objekte možemo shvatiti kao celine u programskom kodu koje odlikuje određeno stanje i ponašanje
- objekti nastaju instanciranjem klasa - programskog koda kojim se definišu moguće odlike budućih objekata
Ovakva definicija nije u stanju da mlađe programere koji se sa ovom tematikom prvi put sreću (ali pri tom već imaju bar ponešto početnog iskustva sa osnovnim, "ne-objektnim" pristupima u programiranju), uputi u sve pojedinosti i takođe, iz iskustva znamo da većina programera na početku ima poteškoća sa razumevanjem osnova objektno orijentisanog programiranja, tako da: dajte sebi vremena da 'pohvatate konce' kako dolikuje i da stvari razumete na pravi način (i naravno - od nečega se mora početi). ).
Razlike u odnosu na proceduralni pristup i malo istorije
Verujemo da većina čitalaca koji se prvi put hvataju ukoštac sa tematikom objektno orijentisanog programiranja već ima okvirnu predstavu o tome da je navedeni pristup veoma široko rasprostranjen u programiranju, a često se javlja i pitanje da li, pošto savladamo OOP tehnike (za pojam objektno orijentisanog programiranja, koristićemo u nastavku i uobičajenu skraćenicu OOP), ovakav pristup treba da koristimo uvek (a verovatno i pitanje - da li moramo), umesto "običnog"/proceduralnog?!
U tom smislu, lako može delovati 'preterano i nepotrebno', da za programe kao što je 'pretvaranje cene u evrima u cenu u dinarima (shodno unetom kursu)' koristimo "instanciranje klasa" ("šta god to bilo" - saznaćemo uskoro), koje sadrže uredno strukturirane kolekcije podataka i pripadajućih metoda - jer se navedeni problem mnogo lakše rešava običnim, proceduralnim pristupom na koji ste već navikli.
Ako tako razmišljate, recimo da ste u pravu (u povećoj meri), ali je takođe, za (iole) kompikovanije zadatke, kreiranje klasa koje odgovaraju podacima nad kojima program treba da operiše, krajnje uobičajena praksa.
Dakle, OOP pristup nije nešto što moramo koristiti ("obavezno"/"uvek"), već nešto što možemo koristiti po potrebi - s tim da se takva potreba javlja vrlo često. Takođe verujemo da ćete tokom čitanja ovog članka, a pogotovo širim i dubljim proučavanjem programiranja, sami doći do zaključka šta su prave pogodnosti objektno orijentisanog pristupa, kako vam takav pristup može pomoći i gde i kako ga treba koristiti: najprostije rečeno, u programima čija kompleksnost zavređuje takav pristup, što u praksi znači (da ponovimo) - jako često, ali, svakako ne uvek.
Potreba da se podaci u programima organizuju na što bolji način primećena je vrlo rano u razvoju programskih jezika i već je (sada već dalekih) šezdesetih godina dvadesetog veka OOP pristup počeo da poprima oblik. Na potencijal ovakvih, u to vreme novih, tehnologija, prvo su "bacili oko" ljudi iz agencija za istraživanje svemira. Nije bilo teško uvideti da računari mogu pomoći u simuliranju procedura vezanih za svemirske letelice i sl, ali da je preduslov za tako nešto to da programi budu što pregledniji (naravno, računalo se tada na primenu OOP pristupa i u drugim oblastima).
I upravo to je (pomalo uprošćeno, ali, za sam početak, reklo bi se više nego adekvatno) glavna prednost OOP pristupa u odnosu na proceduralni - preglednost koda.
Sve ono što nudi objektno orijentisani pristup, može se zapisati i deklarisanjem zasebnih promenljivih i nezavisnih funkcija, naravno - uz nezanemarljivo povećanje složenosti kodnog zapisa.
Tokom vremena, OOP pristup našao je mesto u (rekli bismo) svim (iole popularnim / korišćenim) programskim jezicima: C++, Java, C#, Python, JavaScript, PHP, kao i mnogim drugim. neki od njih su izrazito usmereni na klase i objekte (na primer, Java), dok su drugi manje izričiti.
Vreme je da se upoznamo sa tim kako sve izgleda u praksi, pa ćemo se za početak bavljenja objektno orijentisanim programiranjem prvo upoznati se sa najvažnijim konceptima (ili, kako neko voli da kaže, "stubovima") objektno orijentisanog programiranja.
To su:
- Klase (apstrakcija podataka)
- Enkapsulacija
- Nasleđivanje
- Polimorfizam
Svakom od ovih koncepata, posvetićemo više pažnje u zasebnim odeljcima u nastavku.
Klase i apstrakcija podataka
Klasa je programski kod (obično zapisan u zasebnoj datoteci) koji predstavlja obrazac po kome se kreiraju objekti.
Pojam apstrakcije, kao jedan od osnovnih principa u dizajnu klasa, podrazumeva odabir (samo) onih svojstava koja su bitna za izvršavanje programa i odbacivanje ostalih.
Potrebno je izabrati i opisati:
- koji će podaci biti predstavljeni
- kako će biti predstavljeni
- kako će biti međusobno povezani
- radnje koje će biti moguće obavljati nad podacima
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 podataka), bio bi sveden samo na one odlike koje su za datu aplikaciju bitne.
Na primer: u programu za poslovnu administraciju, zanemarili bismo većinu navedenih svojstava i ostavili datum rođenja, adresu stanovanja i podatke o učinku na radnom mestu.
Primer još jednostavnije klase (donekle uprošćene), koja definiše geometrijska tela, možemo videti na donjoj slici:

Klasa svakako jeste veoma jednostavna, ali, podaci su specijalizovani, odnosno izabrani prema konkretnim potrebama: opštim matematičkim svojstvima pravilnih mnogouglova, dodali smo ona svojstva koja se koriste pri prikazu na ekranu (zarad preglednosti, zanemarili smo koordinate centara).
Takođe, slika nam pruža mogućnost da uvidimo odnos između klasa i objekata: klasa je skup opštih karakteristika koje se mogu prispisati budućim objektima, dok objekat možemo shvatiti kao realizaciju klase , pri čemu je - među mogućim vrednostima različitih svojstava - izabrana određena kombinacija konkretnih vrednosti.
U opštem smislu, elementi (članovi) klasa, su:
- polja (i)
- metode
Kao što smo već naveli, polja su zapisani podaci koji definišu stanje objekta, a metode su funkcije koje definišu radnje koje objekat može obavljati (nad svojim sopstvenim podacima, ili, sa drugim podacima u programu).
Polje (field) - podaci klase
Pojam polja klase u objektno orijentisanom programiranju označava pojedinačni imenovani podatak koji može biti:
- primitivan podak koji pripadaju nekom od osnovnih tipova kao što su int, float ili char
- objekat klase (iste ili različite)
- kolekcija podataka (nizovi, liste, redovi, stek ...) bilo kog tipa
- pokazivač / referenca na podatak, objekat ili kolekciju bilo kog od navedenih tipova
Svi ovi podaci zajedno, kao što smo već nagovestili ranije, opisuju stanje (budućih) objekata.

Da bi međusobne veze podataka klase bile korektne, potrebno je pravilno sprovesti postupak enkapsulacije, čemu ćemo posvetiti mnogo više pažnje u nastavku.
Metode klase
Ponašanje objekata zapisuje se preko metoda (funkcija), koje su direktno opisane (definisane) unutar klase.
U pitanju je programski kod koji je (gotovo) istovetan pojmu funkcije u programskom jeziku C (s tim da se, kao što je već navedeno, celokupan programski kod metoda opisuje unutar tela klase).

Metode mogu pristupati podacima klase, ali takođe i spoljnim podacima iz drugih delova programa.
Da bismo mogli da za prave razumemo kako se odvija interakcija između polja i objekata i pre svega - kako klasa (samim tim - i budući objekti) dobija željene odlike, počećemo polako da definišemo jednostavnu klase i proučavamo šta se sve dešava u različitim fazama razvoja klase ....
Primer jednostavne klase - osnovni oblik
Rekli smo na početku da se OOP pristup koristi onda kada su u pitanju kompleksni podaci koji takav pristup zavređuju. Naš primer će biti mnogo jednostavniji: definisaćemo klasu koja opisuje pravougaonik (primer deluje jednostavno - i jeste jednostavan - ali, upravo tako i treba da bude na samom početku), međutim, primer je sasvim adekvatan i u praktičnom smislu (u programima i skriptama u kojima se, na bilo koji način, koriste pravougaonici, više je nego očekivano da za kreiranje pravouganika bude definisana odgovarajuća klasa).
Klasa pravougaonik sadržaće polja a, b, O i P (podatake o stranicama pravougaonika, obimu i površini), metode kojima se obim i površina ažuriraju pri promeni vrednosti stranica a i b, kao i pomoćnu metodu za ispis podataka o pravougaoniku.
Uz sve navedeno, potrebno je udesiti i da ova četiri parametra uvek budu u međusobnoj vezi koja odgovara pravilima geometrije (a, da li će se to dešavati samo od sebe - videćemo u nastavku) ....
Definisaćemo prvo najosnovnije okvire klase i dodati samo navedene podatke, pa će klasa imati sledeći oblik:
public class Pravougaonik
{
public Double a, b, O, P;
}
class Pravougaonik
{
public:
double a, b, O, P;
};
public class Pravougaonik {
public double a, b, O, P;
}
class Pravougaonik:
# ne moramo definisati polja
# jeste čudno u odnosu na ostale
# jezike, ali, videćemo u nastavku
# kako sve funkcioniše
Četiri polja klase Pravougaonik (četiri promenljive tipa Double) beleže stranice pravougaonika (a i b), kao i obim (O) i površinu (P).
Iako ovakva klasa ni iz daleka nije dovršena, niti zapravo spremna za eksploataciju (tek smo počeli), u najosnovnijem (tehničkom) smislu, već je možemo koristiti za zapis podataka - naravno, uz napomenu da će u svemu biti nepredviđenih okolnosti koje i te kako zahtevaju pažnju (ali, upravo time se i bavimo: učimo kako se definišu klase i objekti).
Iskoristićemo navedenu situaciju da naučimo:
- kako se kreiraju (instanciraju) objekti
- zašto je neophodno pravilno uspostaviti veze međe objektima (enkapsulacija)
Kreiranje objekta (rezervisana reč new)
Pozivanjem sledećeg jednostavnog koda ....
Pravougaonik P = new Pravougaonik();
Pravougaonik P;
Pravougaonik P = new Pravougaonik();
P = Pravougaonik()
.... kreirali smo objekat P, posle čega možemo pristupiti njegovim poljima:
P.a = 12.55;
P.b = 10.40;
Console.WriteLine(P.a);
Console.WriteLine(P.b);
P.a = 12.55;
P.b = 10.40;
cout << P.a;
cout << P.b;
P.a = 12.55;
P.b = 10.40;
Szstem.out.printf("%f\n", P.a);
Szstem.out.printf("%f\n", P.b);
P.a = 12.55
P.b = 10.40
print(str(P.a))
print(str(P.b))
Polja a i b sada sadrže vrednosti koje, same po sebi, imaju smisla, ali, između njih ne postoji veza koja odgovara pravilima geometrije i verujemo da se mnogi od vas pitaju zašto je to 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 - to nije slučaj.
Klasa nije automatizovan šablon koji "magijskim putem" / "tek tako" / samim tim što smo klasi dali ime koje nama nešto znači (a računaru - baš ništa), definiše sve moguće objekte koji nam mogu pasti na pamet (geometrijska tela, vozila, uređaje i slično) pri čemu važe odnosi iz spoljnjeg sveta
Klasa je samo mehanizam koji omogućava da sve podatke i metode budućih objekata stavimo "pod jednu kapu" (na jedno mesto), zarad preglednosti i bolje čitljivosti, a za uspostavljanje odnosa među podacima - moramo se pobrinuti sami.
(Naravno, ništa od navedenog nije ni izdaleka "strašno", ali, svakako moramo biti pažljivi i svesni takve situacije.)
Šta se tačno dešava sa podacima koje nismo inicijalizovali sami?
Ukoliko se sami ne pobrinemo za inicijalizaciju, polja objekta čiji je tip Double (isto važi i za Int32) biće inicijalizovana na vrednost 0 (nula), niske se inicijalizuju kao prazne niske, a reference imaju vrednost null
.
(U slučaju klase Pravougaonik, ukoliko inicijalizujemo stranice pravougaonika, ali ne i obim i površinu, polja O i P će imati vrednost 0 - što svakako nije realistična situacija).
Dakle, budući da smo, u primeru koji koristimo, (naknadno) inicijalizovali stranice, a obim i površinu nismo inicijalizovali na odgovarajuć način (ova polja jesu inicijalizovana, ali po podrazumevanoj metodi koja im dodeljuje vrednost 0), pravougaonik ima sledeće stanje:
// P.a = 12.55
// P.b = 10.40
// P.O = 0
// P.P = 0
Ovakav objekat nema praktično značenje (suštinski, nije ništa bolji nego četiri međusobno nezavisne promenljive), pa ćemo preduzeti korake da to popravimo, uvođenjem nekoliko metoda koje će kontrolisati stanje podataka (polja).
Budući da je jedna od najbitnijih stvari vezanih za promenljive u programiranju inicijalizacija, to jest, postupak kojim se promenljivama zadaje početno stanje (u našem primeru, polja a i b dobila su vrednosti tek naknadno, a poljima O i P nedostaje početno stanje koje odgovara upisanim stranicama), inicijalizacija polja je prvo na šta ćemo obratiti pažnju u nastavku.
Jasno je da za računanje (ažuriranje) obima i površine moramo imati odgovarajuće metode i jasno je da objekat ne možemo inicijalizovati prostom naredbnom dodele (kao običnu promenljivu), već i to moramo obaviti pozivom posebne metode.
Takva specijalizovana metoda u objektno orijentisanom programiranju nosi naziv konstruktor i njena svrha je zadavanje početnog stanja objekta.
Konstruktor - metoda za definisanje početnog stanja objekta
Konstruktor je specijalizovana metoda kojom se zadaje početno stanje objekta. Polja se inicijalizuju prostim naredbama dodele (ukoliko je moguće), ili pozivanjem pomoćnih metoda za ažuriranje vrednosti polja, ukoliko vrednosti određenih polja zavise od drugih polja (kao što je slučaj sa obimom i površinom pravougaonika, koji zavise od stranica).
U pisanju, konstruktor se prepoznaje po tome što (za razliku od ostalih metoda) nema povratni tip i po tome što ima isti naziv kao i sama klasa.
Dodaćemo konstruktor klasi Pravougaonik ....
public class Pravougaonik
{
public Double a, b, O, P;
public Pravougaonik(Double a, Double b)
{
this.a = a;
this.b = b;
}
}
class Pravougaonik
{
public:
double a, b, O, P;
Pravougaonik(double a, double b)
{
this.a = a;
this.b = b;
}
};
public class Pravougaonik {
public double a, b, O, P;
public Pravougaonik(double a, double b) {
this.a = a;
this.b = b;
}
}
class Pravougaonik:
a, b, O, P
def __init__(self, a, b):
self.a = a;
self.b = b;
.... posle čega možemo zadati početno stanje objekta na sledeći način:
Pravougaonik P = new Pravougaonik(12.55, 10.40);
Pravougaonik P(12.55, 10.40);
Pravougaonik P = new Pravougaonik(12.55, 10.40);
P = new Pravougaonik(12.55, 10.40)
Kao što smo već videli, preko rezervisane reči new, poziva se konstruktor klase (pri čemu se zadaju konkretni argumenti, koji će biti prosleđeni odgovarajućim poljima).
Međutim, sve je (skoro) isto kao malopre. Polja a i b odmah dobijaju konkretne vrednosti (o rezervisanoj reči this koju primećujete u kodu, više ćemo govoriti kasnije), ali, polja O i P i dalje imaju vrednost 0 (ovo smo naravno uradili isključivo iz razloga da još jednom podcrtamo da se stvari u objektno orijentisanom programiranju ne dešavaju automatski).
Uvođenjem dve nove metode: jedne za računanje obima i druge, za računanje površine, koje ćemo potom pozvati preko konstruktora (ako se neko pita, ovo je moguće, kao što je i inače moguće da metode pozivaju jedna drugu), rešićemo problem:
public class Pravougaonik
{
public Double a, b, O, P;
public Pravougaonik(Double a, Double b)
{
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
public void RacunanjeObima()
{
O = 2 * (a + b);
}
public void RacunanjePovrsine()
{
P = a * b;
}
}
class Pravougaonik
{
public:
double a, b, O, P;
Pravougaonik(double a, double b)
{
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
void RacunanjeObima()
{
O = 2 * (a + b);
}
void RacunanjePovrsine()
{
P = a * b;
}
};
public class Pravougaonik {
public double a, b, O, P;
public Pravougaonik(double a, double b) {
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
public void RacunanjeObima() {
O = 2 * (a + b);
}
public void RacunanjePovrsine() {
P = a * b;
}
}
class Pravougaonik
a, b, O, P
def __init__ (self, a, b):
self.a = a
self.b = b
RacunanjeObima()
RacunanjePovrsine()
def RacunanjeObima():
O = 2 * (a + b)
def RacunanjePovrsine():
P = a * b
Sada će (konačno), pri sledećem pozivu ....
Pravougaonik P = new Pravougaonik(12.55, 10.40);
Pravougaonik P(12.55, 10.40);
Pravougaonik P = new Pravougaonik(12.55, 10.40);
P = new Pravougaonik(12.55, 10.40)
.... objekat biti korektno inicijalizovan.
Pri pozivu konsturktora, pozivaju se i metode za računanje obima i površine, te će stoga sva četiri polja dobiti korektne vrednosti.
Ako pozovemo sledeći programski kod ....
Console.WriteLine("-Stranica a: " + P.a.ToString());
Console.WriteLine("-Stranica b: " + P.b.ToString());
Console.WriteLine("-Obim: " + P.O.ToString());
Console.WriteLine("-Površina: " + P.P.ToString());
cout << "-Stranica a: " << P.a;
cout << "-Stranica b: " << P.b;
cout << "-Obim: " << P.O;
cout << "-Površina: " << P.P;
System.out.printf("-Stranica a: %f", P.a);
System.out.printf("-Stranica b: %f", P.b);
System.out.printf("-Obim: %f", P.O);
System.out.printf("-Površina: %f", P.P);
print("-Stranica a: " + str(P.a.ToString))
print("-Stranica b: " + str(P.b.ToString))
print("-Obim: " + str(P.O.ToString))
print("-Površina: " + str(P.P.ToString))
.... dobićemo sledeći ispis:
-Stranica a: 12.55
-Stranica b: 10.40
-Obim: 45.90
-Površina: 130.52
Konačno smo došli do toga da je (bar) početno stanje objekta pravilno zadato, a, ako nam se ne sviđa prethodni poziv, možemo kreirati zasebnu metodu za ispis vrednosti polja ....
public class Pravougaonik
{
public Double a, b, O, P;
public Pravougaonik(Double a, Double b)
{
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
public void RacunanjeObima()
{
O = 2 * (a + b);
}
public void RacunanjePovrsine()
{
P = a * b;
}
public void KonzolniIspis()
{
Console.WriteLine("-Stranica a: " + this.a.ToString());
Console.WriteLine("-Stranica b: " + this.b.ToString());
Console.WriteLine("-Obim: " + this.O.ToString());
Console.WriteLine("-Površina: " + this.P.ToString());
}
}
class Pravougaonik
{
public:
double a, b, O, P;
Pravougaonik(double a, double b)
{
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
void RacunanjeObima()
{
O = 2 * (a + b);
}
void RacunanjePovrsine()
{
P = a * b;
}
void KonzolniIspis()
{
cout << "-Stranica a: " << this.a << endl;
cout << "-Stranica b: " << this.b << endl;
cout << "-Obim: " << this.O << endl;
cout << "-Površina: " << this.P << endl;
}
};
public class Pravougaonik {
public double a, b, O, P;
public Pravougaonik(double a, double b) {
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
public void RacunanjeObima() {
O = 2 * (a + b);
}
public void RacunanjePovrsine() {
P = a * b;
}
public void KonzolniIspis() {
System.out.printf("-Stranica a: %f", this.a);
System.out.printf("-Stranica b: %f", this.b);
System.out.printf("-Obim: %f", this.O);
System.out.printf("-Površina: %f", this.P);
}
}
class Pravougaonik
a, b, O, P
def __init__(self, a, b):
self.a = a
self.b = b
RacunanjeObima()
RacunanjePovrsine()
def RacunanjeObima():
O = 2 * (a + b)
def RacunanjePovrsine():
P = a * b
def KonzolniIspis():
print("-Stranica a: " + str(self.a))
print("-Stranica b: " + str(self.b))
print("-Obim: " + str(self.O))
print("-Površina: " + str(self.P))
.... i pozvati je na sledeći način (koji je svakako nešto elegantniji):
P.KonzolniIspis();
P.KonzolniIspis();
P.KonzolniIspis();
P.KonzolniIspis()
Programski kod sada jeste mnogo pregledniji, ali, definicija klase još uvek nije potpuna ....
Iako dosadašnji zapis omogućava korektnu inicijalizaciju objekta, ne postoji (za sada) mehanizam koji omogućavaju automatsko ažuriranje obima i površine pri promeni vrednosti stranica, kao i mehanizmi koji onemogućavaju unošenje pogrešnih (recimo - negativnih) vrednosti stranica.
Skup tehnika OOP-a koje omogućavaju ovakve "sigurnosne" provere nose naziv - enkapsulacija, što će biti glavna tema u nastavku, ali ćemo se, pre nego što se posvetimo enkapsulaciji, vratiti na rezervisanu reč this
koju smo prethodno koristili unutar konstruktora (a detaljnije objašnjenje smo tada ostavili za kasnije).
Rezervisana reč "this"
Svrha rezervisane reči this je razrešavanje nedoumica koje mogu nastati u zapisu programskog koda, kada parametri konstruktora imaju iste nazive kao polja klase.
Da pojasnimo ....
Kada je objekat kreiran, njegovim poljima se pristupa navođenjem identifikatora objekta i polja, koji su spojeni operatorom pristupa (.
u C#-u i Javi ili ->
u C++ - u)
(Primer: P.a - kao način pristupa stranici a objekta P).
Takav pristup je moguć kad (ako) je objekat već kreiran, ali, u samoj klasi Pravougaonik nije moguće koristiti poziv "Pravougaonik.a"!
Rezervisana reč this se koristi u situacijama kada u klasi želimo da se obratimo "budućem" objektu (koji tek treba da nastane upotrebom klase). U primeru konstruktora koji smo koristili....
public Pravougaonik(Double a, Double b)
{
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
public:
Pravougaonik(double a, double b)
{
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
};
public Pravougaonik(double a, double b) {
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
def __init__(self, a, b):
self.a = a
self.b = b
RacunanjeObima()
RacunanjePovrsine()
.... upotrebom rezervisane reči this pravimo razliku između polja klase a (this.a) i parametra konstruktora a.
Enkapsulacija
Enkapsulacija je skup tehnika kojima se omogućava pravilan pristup podacima, uspostavljanje veza između podataka i ažuriranje podataka u svim situacijama kada je to neophodno.
Ali takođe, u još opštijem (ili, prostije rečeno, osnovnijem) smislu, enkapsulacija u objektno orijentisanom programiranju je princip koji nalaže da se pristup podacima mora regulisati tako da "unutrašnji mehanizmi" klasa treba da budu skriveni od krajnjih korisnika, ali - tako da se korisnicima klase omogući neometano korišćenje svih neophodnih funkcionalnosti.
Da pojasnimo to preko primera: vozač automobila (u prenesenom značenju - krajnji korisnik klase), na raspolaganju ima volan, menjač i papučice za gas, kvačilo i kočnicu, čime je u mogućnosti da upravlja automobilom, ali, nema uticaja na dizajn karburatora, prenosnog mehanizma, vešanja, klipova i drugih delova, već je to posao autoinženjera (u prenesenom značenju - programera koji su zaduženi za dizajn/održavanje klase).
Sa blagodetima enkapsulacije (koliko god to čudno delovalo) najbolje ćemo se upoznati dodatnim posmatranjem klase koju smo već koristili, koja (još uvek) ne koristi enkapsulaciju ....
Šta se dešava ukoliko ne koristimo enkapsulaciju?
Da li se sećamo naredbe P.a = 12.55 kojom smo zadali vrednost 12.55 polju a objekta P? Da li to znači da možemo napraviti i sledeće pozive (iako je objekat sada korektno inicijalizovan) ....
P.O = 4114.14;
P.P = -19.25;
P.O = 4114.14;
P.P = -19.25;
P.O = 4114.14;
P.P = -19.25;
P.O = 4114.14
P.P = -19.25
.... i da li će oni ponovo poremetiti objekat?
Možemo izvršiti navedene naredbe, ali će one će očekivano dovesti objekat u stanje nesklada (nadamo se da ste bili spremni na ovakav ishod, iako je nepovoljan): obim je mnogo veći nego što bi trebalo, a površina je negativan broj.
Međutim, pre svega treba se zapitati - da li uopšte treba da postoji mogućnost direktnog zadavanja vrednosti obima i površine?!
Pitanje je jednostavno, a odgovor je - ne (matematika kaže da postoji beskonačno mnogo kombinacija vrednosti stranica a i b koje daju određeni obim ili površinu, a u praktičnoj implementaciji na računaru, iako taj broj nije beskonačan, kombinacija ima izrazito mnogo).
U tehničkom smislu, enkapsulacija se postiže kritičkom upotrebom specifikatora pristupa u kombinaciji sa javnim metodama koje regulišu pristup privatnim poljima (ili upotrebom akcesora set
i get
, u jezicima koji to podržavaju).
Upravo smo izneli podosta novih/nepoznatih pojmova, pa ćemo se u nastavku potruditi da ih sve detaljno razjasnimo (i naravno, iako će nam ove novopomenute tehnikalije malo "zakomplikovati život", konačno ćemo srediti 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 datim podacima:
- private - poljima/metodama moguće je pristupati samo unutar klase i nije moguće pristupati im iz spoljnih delova koda
- public - poljima/metodama moguće je neposredno pristupati iz spoljnih delova koda (i naravno unutar klase)
- protected - poljima/metodama moguće je pristupati unutar osnovne klase i nasleđenih klasa (o nasleđivanju ćemo govoriti u nastavku), ali, nije im moguće pristupati im iz spoljnih delova koda
Najvažniji specifikatori pristupa su private i public, pa ćemo im posvetiti najviše pažnje.
Primetili smo da je, do sada, uz sva polja (promenljive) klase Pravougaonik stajala odrednica "public". Ova rezervisana reč sugeriše spoljnjim delovima programskog koda da mogu neposredno pristupati poljima uz koje ta odrednica stoji. Dobra strana ove "otvorenosti" je to što možemo neposredno pristupati poljima, a loša, to što (time što bilo kom polju koje ima specifikator pristupa public možemo neposredno pristupiti), možemo dovesti u pitanje korektnost međusobne povezanosti polja i (samim tim) funkcionalnost objekta (kao recimo malopre, kada smo upisali da je površina pravougaonika -19.25, što nije matematički moguće).
Prvi korak u enkapsulaciji je da, preko specifikatora pristupa private onemogućimo neposredan pristup određenom polju. U klasi Pravougaonik, to bi izgledalo ovako:
private Double a, b, O, P;
private:
double a, b, O, P;
private double a, b, O, P;
__a, __b, __O, __P
Međutim, kada to uradimo, spoljni delovi koda više ne mogu pristupati poljima a, b, O i P!
Ovo može, ali i ne mora, predstavljati problem (bićemo praktični: u našem slučaju, predstavlja problem).
Ako (u opštem slučaju) nemogućnost pristupa poljima ne predstavlja problem, ne moramo preduzimati dalje korake.
Budući da definicija klase Pravougaonik sadrži metodu za ispis vrednosti, moglo bi se desiti da je to sve što nam je potrebno, međutim, razumno je ipak pretpostaviti da će bar povremeno postojati potreba za ažuriranjem stranica pravougaonika, i pogotovo - za neposrednim čitanjem vrednosti pojedinačnih polja.
Za početak, pogledaćemo u sledećem odeljku na koji način možemo privatnim poljima pristupati preko javnih metoda, a u nastavku ćemo se pozabaviti i pristupom privatnim poljima upotrebom "svojstava" (property), preko (takozvanih) akcesora "set" i "get".
Pristup privatnim poljima preko javnih metoda
Javna metoda za čitanje vrednosti privatnog polja ima sledeći oblik:
public Double Citanje_a()
{
return this.a;
}
public:
double Citanje_a()
{
return this.a;
};
public double Citanje_a() {
return this.a;
}
def Citanje_a():
return self.a
Specifikator pristupa public omogućava pristup metodi iz spoljnih delova koda, a preko povratnog tip Double omogućava se direktno čitanje vrednosti polja a u izvornom obliku (a ne samo tekstualni ispis).
Za upis (ažuriranje), vrednosti polja a, koristićemo sledeću javnu metodu:
public void Upis_a(Double a)
{
// Radi preglednosti nećemo ovde
// pisati deo koda sa porukom o grešci,
// koji inače možemo upotrebiti
if (a <= 0) return;
// Ako je uneta vrednost pozitivan broj,
// ažuriraćemo obim i površinu
this.a = a;
RacunanjeObima();
RacunanjePovrsine();
}
public:
void Upis_a(double a)
{
// Radi preglednosti nećemo ovde
// pisati deo koda sa porukom o grešci,
// koji inače možemo upotrebiti
if (a <= 0) return;
// Ako je uneta vrednost pozitivan broj,
// ažuriraćemo obim i površinu
this.a = a;
RacunanjeObima();
RacunanjePovrsine();
};
public void Upis_a(double a) {
// Radi preglednosti nećemo ovde
// pisati deo koda sa porukom o grešci,
// koji inače možemo upotrebiti
if (a <= 0) return;
// Ako je uneta vrednost pozitivan broj,
// ažuriraćemo obim i površinu
this.a = a;
RacunanjeObima();
RacunanjePovrsine();
}
def Upis_a(a)
# Radi preglednosti nećemo ovde
# pisati deo koda sa porukom o grešci,
# koji inače možemo upotrebiti
if a <= 0:
return
# Ako je uneta vrednost pozitivan broj,
# ažuriraćemo obim i površinu
self.a = a
RacunanjeObima()
RacunanjePovrsine()
Upotrebom ovakvog pristupa rešavamo dva problema:
- Proveru ulaznog podatka (ukoliko je unet negativni broj, vrednost polja a se ne ažurira, a metode za ažuriranje obima i površine se ne pozivaju)
- Ažuriranje obima i površine pri zadavanju nove vrednosti stranice
Klasa je sada konačno spremna za upotrebu, pa možemo pogledati šematske prikaze klase koju smo napisali i objekta koji nastaje upotrebom date klase:
Klasa ....

Objekat ....

Primećujemo da klasa naizgled sadrži više. Međutim, to je samo naizgled. Na objektu je takođe sve tu, samo .... "ispod haube". :)
Ovo je (takođe) šematski prikaz principa enkapsulacije: "sve je tu", ali, direktno možemo pristupati samo određenim matodama.
Za kraj odeljka o enkapsulaciji, razmotrimo drugi primer enkapsulacije: primer upotrebe svojstava u programskom jeziku C# (što je pristup kojim se postiže još veća preglednost, ali, nažalost nije dostupan u ostalim jezicima).
Enkapsulacija upotrebom akcesora u programskom jeziku C#
Videli smo da je pristup privatnim poljima moguć preko javnih (public) metoda, a da je razlog zašto uopšte skrivamo polja klase to što, u mnogim situacijama (gde su vrednosti određenih polja uslovljene vrednostima drugih polja) želimo da očuvamo veze između tih podataka (enkapsulacija i skrivanje podataka nisu uvek obavezni, ali, u velikom broju situacija postoji potreba za takvim pristupom).
Ovakav pristup možemo koristiti u većini programskih jezika koji podržavaju OOP pristup, ali, neki jezici, kao što je C# koji koristimo u ovom članku, podržavaju i upotrebu takozvanih "svojstava", posebnih blokova programskog koda koji sadrže takozvane akcesore, kojima se omogućava uslovni pristup skrivenim poljima.
Ovde zapravo mislimo na isti pristup kakav smo već koristili u prethodnom odeljku (ali, zapisan na nešto elegantniji način) i sve je zapravo mnogo jednostavnije nego što deluje na prvi pogled.
Da bismo se uverili u to, pogledaćemo jednostavan primer (pri čemu ćemo napraviti i izvesne male izmene i prikazati odmah kako upotreba svojstava obično izgleda u klasama):
public class Pravougaonik
{
private Double _a, _b, _O, _P;
public Pravougaonik(Double a, Double b)
{
this.a = a;
this.b = b;
RacunanjeObima();
RacunanjePovrsine();
}
public Double a // Ovo je "svojstvo" a (nije isto što i polje a,
{ // kao što smo imali u prethodnom opisu klase)
get
{
return _a;
}
set
{
if(a > 0)
{
this.a = value;
RacunanjeObima();
RacunanjePovrsine();
}
}
}
}
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
Da pojasnimo šta je to što smo videli:
Deo koda ....
public Double a
{
}
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
.... definiše svojstvo a koje će biti dostupno spoljnim delovima koda (specifikator pristupa ovog svojstva je public).
Unutar zagrada, definisana su dva bloka. Prvi blok je get
....
get
{
return _a
}
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
.... kojim je određeno da će programski kod koji zatraži vrednost svojstva a
(zapravo) dobiti vredost polja _a
. Na primer, preko sledećeg koda ....
Pravougaonik P = new Pravougaonik(5.5, 10.5);
MessageBox.Show("Stranica a: " + P.a.ToString());
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
.... dobićemo ispis stranice a
. Pozivom P.a
dobijamo vrednost polja P._a
(a zatim se poziva opšta funkcija ToString()
kojom se ispisuje vrednost).
Drugi blok je set
....
set
{
if(a > 0)
{
this.a = value;
RacunanjeObima();
RacunanjePovrsine();
}
}
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
.... kojim je određeno da će polje _a
dobiti predatu vrednost (value
), samo u slučaju ako je predata vrednost veća od nule.
Prvo što primećujemo u odeljku set
je sličnost sa pristupom koji smo koristili u prethodnom odeljku (gde smo koristili metodu Upis_a), a drugo je rezervisana reč value
.
Posmatrajmo to ovako (postavimo sebi pitanje): pod kojim okolnostima određeno polje može da promeni vrednost? Odgovor je - preko naredbe dodele, a naredbe dodele, sa jedne strane imaju identifikator promenljive (može naravno biti i polje klase), a sa druge .... ništa drugo nego izračunatu vrednost.
P.a = 10; // vrednost se zadaje direktno
P.a = X; // vrednost se dobija čitanjem vrednost druge promenljive
P.a = X + Y; // vrednost se dobija računanjem vrednosti izraza
P.a = 10; // vrednost se zadaje direktno
P.a = X; // vrednost se dobija čitanjem vrednost druge promenljive
P.a = X + Y; // vrednost se dobija računanjem vrednosti izraza
P.a = 10; // vrednost se zadaje direktno
P.a = X; // vrednost se dobija čitanjem vrednost druge promenljive
P.a = X + Y; // vrednost se dobija računanjem vrednosti izraza
P.a = 10; # vrednost se zadaje direktno
P.a = X; # vrednost se dobija čitanjem vrednost druge promenljive
P.a = X + Y; # vrednost se dobija računanjem vrednosti izraza
Upravo to je razlog zašto se (makar u programskom eziku C#), koristi rezervisana reč value, u smislu: kakva god vrednost da se nađe sa desne strane naredbe dodele, biće prosleđena svojstvu a.
Na kraju, primetimo da su svojstva svojevrsne kombinacije polja i metoda. Svojstvo, spolja gledano (kada svojstvu pristupamo iz spoljnih delova koda), izgleda kao polje, ali, "iznutra" funkcioniše kao metoda. U našem slučaju (koji je tipičan primer upotrebe svojstava):
- dodela nije bezuslovna
- posle dodele pozivaju se metode za ažuriranje
.... a takođe, u opštem smislu:
- jedan od dva bloka može se i izostaviti
Na primer:
public class Pravougaonik
{
....
public Double O // Obim
{
get
{
return _O;
}
}
public Double P // Površina
{
get
{
return _P;
}
}
}
// Opcija nije dostupna u programskom jeziku C++
// Opcija nije dostupna u programskom jeziku Java
# Opcija nije dostupna u programskom jeziku Python
Svojstva O i P su "read only". Moguće je samo čitati vrednosti obima i površine (da li zaista ikako možemo odrediti stranice a i b ako unesemo vrednost obima ili površine?!).
Takođe, primetili smo i da ćemo sada, kada spoljnim delovima koda "otkrivamo" svojstva koja nose prethodne nazive polja (a, b, O i P), za sama polja koristiti nazive sa dodatkom podvučene crte (_a, _b, _O i _P). Ovo je stvar konvencije i uobičajena praksa u programiranju koju svakako treba da uvažimo.
Nasleđivanje i polimorfizam
Pred kraj, pozabavićemo se temom nasleđivanja u objektno orijentisanom programiranju.
Rekli smo da je (prava) svrha objektno orijentisanog programiranja dobra organizacija podataka.
U tom smislu, da bismo razumeli svrhu nasleđivanja u OOP-u, zamislićemo sledeću situaciju (nećemo ovoga puta koristiti klasu pravougaonik, već ćemo uzeti drugu klasu za primer):
- Potrebno je definisati klasu Osoba, sa nekoliko opštih polja kao štu Ime, Prezime, DatumRodjenja i Starost
- Potrebno je definisati klasu Radnik koja sadrži sva svojstva prethodne klase i pri tom sadrži polja koja su specifično vezana za radnike: RadnoMesto, PocetakRada i RadniStaz
Možemo definisati dve potpuno nezavisne klase, ali, možemo primeniti i bolji pristup koji omogućava određene pogodnosti.
Nasleđivanje
Prvo ćemo definisati opštiju od dve klase (onu koja sadrži zajedničke podatke), što je u našem slučaju klasa Osoba:
public class Osoba
{
public String Ime, Prezime;
public DateTime DatumRodjenja;
private UInt32 _Starost;
public Osoba(String Ime, String Prezime, DateTime DatumRodjenja)
{
this.Ime = Ime;
this.Prezime = Prezime;
this.DatumRodjenja = DatumRodjenja;
RacunanjeStarosti();
}
public void RacunanjeStarosti()
{
_Starost = DateTime.Now.Year - DatumRodjenja.Year;
if(DateTime.Now.Month < DatumRodjenja.Month)
{
_Starost--;
}
if(DateTime.Now.Month == DatumRodjenja.Month &&
DateTime.Now.Day < DatumRodjenja.Day)
{
_Starost--;
}
}
public UInt32 Starost
{
return _Starost;
}
}
struct Datum {
int godina, mesec, dan;
};
class Osoba
{
private:
int _starost;
public:
string ime, prezime;
Datum datumRodjenja;
Osoba(string ime, string prezime, Datum datumRodjenja)
{
this->ime = ime;
this->prezime = prezime;
this->datumRodjenja = datumRodjenja;
racunanjeStarosti();
}
racunanjeStarosti()
{
time_t d = time(NULL);
tm* datum = localtime(&d);
int godina = datum->tm_year + 1900;
int mesec = datum->tm_mon + 1;
int dan = datum->tm_mday;
_starost = godina - datumRodjenja.godina;
if(mesec < datumRodjenja.mesec)
{
_starost--;
}
if(mesec == datumRodjenja.mesec &&
dan < datumRodjenja.dan)
{
_starost--;
}
}
int citanjeStarosti() {
return _starost;
}
};
public class Osoba {
public String Ime, Prezime;
public LocalDate DatumRodjenja;
private int _Starost;
public Osoba(String Ime, String Prezime, LocalDate DatumRodjenja) {
this.Ime = Ime;
this.Prezime = Prezime;
this.DatumRodjenja = DatumRodjenja;
RacunanjeStarosti();
}
public void RacunanjeStarosti() {
LocalDate datum = LocalDate.now();
_Starost = datum.getYear() - DatumRodjenja.getYear();
if(datum.getMonthValue() < DatumRodjenja.getMonthValue()) {
_Starost--;
}
if(datum.getMonthValue() == DatumRodjenja.getMonthValue() &&
datum.getDayOfMonth() < DatumRodjenja.getDayOfMonth()) {
_Starost--;
}
}
public int citanjeStarosti() {
return _Starost;
}
}
import datetime.datetime
class Osoba:
def __init__(self, ime, prezime, datumRodjenja):
self.ime = ime
self.prezime = prezime
self.datumRodjenja = datumRodjenja
self.racunanjeStarosti()
def racunanjeStarosti(self):
datum = datetime.datetime.now()
self.__starost = datum.year - self.datumRodjenja.year
if datum.month < self.datumRodjenja.month:
self.__starost = self.__starost - 1
if datum.month == self.datumRodjenja.month and
datum.day < self.datumRodjenja.day:
self.__starost = self.__starost - 1
def citanjeStarosti(self):
return self.__starost
Vidimo da ovakva klasa nema baš previše podataka, ali, čak i na ovakvom veoma jednostavnom primeru, vidimo da bi definisanje klase Radnik u kojoj bismo bukvalno "prepisali" prethodni kod, ne deluje kao najpraktičnije rešenje.
Klasa Radnik nalsediće klasu Osoba (koristi polja/metode/svojstva ove klase), pri čemu ćemo naravno biti u mogućnosti da dodamo nova polja, metode i svojstva.
public class Radnik : Osoba
{
private String _RadnoMesto;
private DateTime _PocetakRada;
private UInt32 _RadniStaz;
public Radnik(String Ime, String Prezime,
String DatumRodjenja, String RadnoMesto,
DateTime PocetakRada) : base(Ime, Prezime, DatumRodjenja)
{
this._RadnoMesto = RadnoMesto;
this._DatumRodjenja = DatumRodjenja;
this._PocetakRada = PocetakRada;
RacunanjeStaza();
}
public void RacunajeStaza()
{
_RadniStaz = DateTime.Now.Year - PocetakRada.Year;
if(DateTime.Now.Month < PocetakRada.Month)
{
_RadniStaz--;
}
if(DateTime.Now.Month == PocetakRada.Month &&
DateTime.Now.Day < PocetakRada.Day)
{
_RadniStaz--;
}
}
public UInt32 RadnoMesto
{
get
{
return _RadnoMesto;
}
}
public UInt32 RadniStaz
{
get
{
return _RadniStaz;
}
}
}
class Radnik : public Osoba {
private:
Datum _pocetakRada;
string _pozicija;
int _staz;
public:
Radnik(string ime, string prezime, Datum datumRodjenja, string pozicija, Datum pocetakRada) : Osoba(ime, prezime, datumRodjenja)
{
this->_pozicija = pozicija;
this->_pocetakRada = pocetakRada;
racunanjeStaza();
}
racunanjeStaza()
{
time_t d = time(NULL);
tm* datum = localtime(&d);
int godina = datum->tm_year + 1900;
int mesec = datum->tm_mon + 1;
int dan = datum->tm_mday;
_staz = godina - _pocetakRada.godina;
if(mesec < _pocetakRada.mesec)
{
_staz--;
}
if(mesec == _pocetakRada.mesec &&
dan < _pocetakRada.dan)
{
_staz--;
}
}
int citanjeStaza()
{
return _staz;
}
string citanjePozicije()
{
return _pozicija;
}
};
public class Radnik extends Osoba {
private String Pozicija;
private int _Staz;
public LocalDate PocetakRada;
public Radnik(String Ime, String Prezime, LocalDate DatumRodjenja, String Pozicija, LocalDate PocetakRada) {
super(Ime, Prezime, DatumRodjenja);
this.PocetakRada = PocetakRada;
this.Pozicija = Pozicija;
RacunanjeStaza();
}
public void RacunanjeStaza( ) {
LocalDate datum = LocalDate.now();
_Staz = datum.getYear() - PocetakRada.getYear();
if(datum.getMonthValue() < PocetakRada.getMonthValue()) {
_Staz--;
}
if(datum.getMonthValue() == PocetakRada.getMonthValue() &&
datum.getDayOfMonth() < PocetakRada.getDayOfMonth()) {
_Staz--;
}
}
public int citanjeStaza() {
return _Staz;
}
public int citanjePozicije() {
return _Pozicija;
}
}
class Radnik(Osoba):
def __init__(self, ime, prezime, datumRodjenja, radnoMesto, pocetakRada):
self.radnoMesto = radnoMesto
self.pocetakRada = pocetakRada
Osoba.__init__(self, ime, prezime, datumRodjenja)
self.racunanjeRadnogStaza()
def racunanjeRadnogStaza(self):
datum = datetime.datetime.now()
self.__radniStaz = datum.year - self.pocetakRada.year
if datum.month < self.pocetakRada.month:
self.__radniStaz = self.__radniStaz - 1
if datum.month == self.pocetakRada.month and
datum.day < self.pocetakRada.day:
self.__radniStaz = self.__radniStaz - 1
def citanjeStaza(self):
return self.__radniStaz
Da pojasnimo: kada napišemo ....
public class Radnik : Osoba
{
}
class Radnik : public Osoba
{
};
public class Radnik extends Osoba {
}
class Radnik(Osoba):
.... definisali smo klasu Radnik koja nasleđuje klasu osoba. To znači da klasa Radnik ima sva polja, metode i svojstva koje ima klasa Osoba, pri čemu je moguće dodavati nove, svojstvene (samo) klasi radnik, kao na primer u konstruktoru ....
public Radnik(String Ime, String Prezime, String DatumRodjenja,
DateTime PocetakRada) : base(Ime, Prezime, DatumRodjenja)
{
this._RadnoMesto = RadnoMesto;
this._DatumRodjenja = DatumRodjenja;
this._PocetakRada = PocetakRada;
RacunanjeStaza();
}
Radnik(string ime, string prezime, Datum datumRodjenja, string pozicija,
Datum pocetakRada) : Osoba(ime, prezime, datumRodjenja)
{
this->_pozicija = pozicija;
this->_pocetakRada = pocetakRada;
racunanjeStaza();
}
public Radnik(String Ime, String Prezime, String DatumRodjenja,
String RadnoMesto, DateTime PocetakRada) {
super(Ime, Prezime, DatumRodjenja);
this._RadnoMesto = RadnoMesto;
this._PocetakRada = PocetakRada;
RacunanjeStaza();
}
def __init__(self, ime, prezime, datumRodjenja, radnoMesto, pocetakRada):
self.radnoMesto = radnoMesto
self.pocetakRada = pocetakRada
Osoba.__init__(self, ime, prezime, datumRodjenja)
self.racunanjeRadnogStaza()
.... gde preko koda .....
: base(Ime, Prezime, DatumRodjenja)
: Osoba(Ime, Prezime, DatumRodjenja)
super(Ime, Prezime, DatumRodjenja);
Osoba.__init__(self, ime, prezime, datumRodjenja)
.... praktično pozivamo konstruktor klase Osoba, što podrazumeva inicijalizaciju tri navedena polja i pozivanje metode za računanje starosti, ali, budući da konstruktor klase Radnik sadrži i druge parametre i poziv metode za računanje radnog staža, vidimo da je povezivanje podataka koje smo ostvarili na ovaj način krajnje optimalno i praktično.
Polimorfizam
Sama reč polimorfizam (koja u prevodu označava "višeobličje"), u objektno orijentisanom programiranju označava da se osnovne klase i njihove izvedene klase u određenim okolnostima mogu koristiti ravnopravno, odnosno istovremeno (u nekim okolnostima - ali ne i uvek).
Pogledajmo jedan praktičan primer koji koristi klase koje smo već definisali.
Recimo, smatraćemo da dve klase koristimo za organizaciju podataka radnika sopstvene firme i članova njihove bliže familije. Podatke o radnicima ćemo beležiti preko klase Radnik, a podatke o ostalim osobama ćemo beležiti preko klase Osoba.
Sledeći zahtev je sastavljanje jedinstvene lista svih osoba, bez obzira na to da li su radnici, ili nisu (smatramo da je najbolje da postoji samo jedna takva lista, zarad dobre organizacije i uštede prostora). Recimo, potreban nam je način da pregledamo spisak svih osoba i (u određenim okolnostima) pronađemo:
- decu mlađu od 7 godina (uoči Novogodišnjih praznika)
- sve osobe starije od 55 godina (zarad organizacije karaoke večeri za sve seniore, bez obzira da li su radnici ili nisu)
- sve žene (uoči 8. marta, radi dodele poklona, bez obzira da li su radnici ili nisu)
Naslućujete da je ovo sve moguće izvesti upotrebom tehnika OOP-a i da će nam pomenuti polimorfizam u tome "nekako" već pomoći.
Ako napravimo listu u kojoj su elementi objekti osnovne klase (Osoba) ....
List<Osoba> SpisakOsoba = new List<Osoba>();
list<Osoba> SpisakOsoba;
List<Osoba> SpisakOsoba = new ArrayList<Osoba>();
spisakOsoba = []
.... takva lista moći će da prima objekte klase Osoba i objekte klase Radnik. Na primer, sledeći kod ....
Osoba osoba = new Osoba("Dejana", "Marković", "1970-05-27");
SpisakOsoba.Add(osoba);
Radnik radnik = new Radnik("Petar", "Marković", "1969-11-21",
"Upravnik magacina", "1997-01-05");
SpisakOsoba.Add(radnik);
Datum datumRodjenja;
Datum pocetakRada;
datumRodjenja.godina = 1970;
datumRodjenja.mesec = 5;
datumRodjenja.dan = 27;
Osoba osoba = Osoba("Dejana", "Marković", datumRodjenja);
SpisakOsoba.push_back(osoba);
datumRodjenja.godina = 1969;
datumRodjenja.mesec = 11;
datumRodjenja.dan = 21;
pocetakRada.godina = 1997;
pocetakRada.mesec = 1;
pocetakRada.dan = 5;
Radnik radnik = new Radnik("Petar", "Marković", datumRodjenja,
"Upravnik magacina", pocetakRada);
SpisakOsoba.push_back(radnik);
LocalDate datumRodjenja1 = LocalDate.of(1970, 5, 27);
Osoba osoba = new Osoba("Dejana", "Marković", datumRodjenja1);
SpisakOsoba.Add(osoba);
LocalDate datumRodjenja2 = LocalDate.of(1969, 11, 21);
LocalDate pocetakRada2 = LocalDate.of(1997, 1, 5);
Radnik radnik = new Radnik("Petar", "Marković", datumRodjenja2,
"Upravnik magacina", pocetakRada2);
SpisakOsoba.Add(radnik);
datumRodjenja = datetime.datetime(1970, 5, 27)
osoba = Osoba("Dejana", "Marković", datumRodjenja)
spisakOsoba.append(osoba)
datumRodjenja = datetime.datetime(1969, 11, 21)
pocetakRada = datetime.datetime(1997, 1, 5)
radnik = Radnik("Petar", "Marković", datumRodjenja,
"Upravnik magacina", pocetakRada)
spisakOsoba.append(radnik)
.... izvršavaće se besprekorno.
Završićemo uz napomenu da je "obrnuti" pristup moguć (kreiranje liste objekata klase Radink nu koju bismo dodavali i objekte klase Osoba), ali, u tom slučaju moramo biti vrlo pažljivi. Recimo, sledeći kod ....
Osoba O = SpisakOsoba.Peek();
Osoba O = SpisakOsoba.front();
Osoba O = SpisakOsoba.peek();
O = spisakOsoba[len(spisakOsoba) - 1]
.... daje referencu na objekat sa kojim možemo postupati na bilo koji (moguć) način, u tom smislu što objekat koij referenciramo garantovano ima sva polja, metode i svojstva koje bi nam moglo pasti na pamet da koristimo.
U obrnutim okolnostima (smatraćemo da smo kreirali listu objekata klase Radnik) ....
Radnik R = SpisakRadnika.Peek();
Radnik R = SpisakRadnika.front();
Radnik R = SpisakRadnika.peek();
R = spisakRadnika[len(spisakRadnika) - 1]
.... može doći do problema ukoliko preko reference R referenciramo objekat klase Osoba i pri tom (recimo) pozivamo neko od svojstava klase Radnik (recimo 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 besprekorno.
Primer za kraj ....
Iako deluje pomalo nepotrebno posle svega što smo naveli, vratićemo se na pravougaonike i pogledati kako bi izgledao osnovni rad sa nizom pravougaonika koji su definisani preko klase (Pravougaonik), u poređenju sa proceduralnim pristupom gde se svojstva pravougaonika definišu preko zasebnih promenljivih:
Prvo koristimo OOP pristup:
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonik[] Pravougaonici = new Pravougaonik[3]
Ako je potrebno da pronađemo veći među prva dva pravougaonika, ceo kod izgledaće ovako:
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonici[0] = new Pravougaonik(10.5, 12.4);
Pravougaonici[1] = new Pravougaonik(21.3, 7.2);
Pravougaonici[2] = new Pravougaonik(4.5, 22.6);
if (Pravougaonici[0].P > Pravougaonici[1].P)
{
Console.WriteLine("Prvi pravougaonik ima veću površinu.");
}
else
{
Console.WriteLine("Drugi pravougaonik ima veću površinu.");
}
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonici[0] = new Pravougaonik(10.5, 12.4);
Pravougaonici[1] = new Pravougaonik(21.3, 7.2);
Pravougaonici[2] = new Pravougaonik(4.5, 22.6);
if (Pravougaonici[0].P > Pravougaonici[1].P)
{
cout << "Prvi pravougaonik ima veću površinu." << endl;
}
else
{
cout << "Drugi pravougaonik ima veću površinu." << endl;
}
Pravougaonik[] Pravougaonici = new Pravougaonik[3];
Pravougaonici[0] = new Pravougaonik(10.5, 12.4);
Pravougaonici[1] = new Pravougaonik(21.3, 7.2);
Pravougaonici[2] = new Pravougaonik(4.5, 22.6);
if (Pravougaonici[0].P > Pravougaonici[1].P) {
System.out.printf("Prvi pravougaonik ima veću površinu.");
}
else {
System.out.printf("Drugi pravougaonik ima veću površinu.");
}
Pravougaonici = [3];
Pravougaonici.append(Pravougaonik(10.5, 12.4))
Pravougaonici.append(Pravougaonik(21.3, 7.2))
Pravougaonici.append(Pravougaonik(4.5, 22.6))
if Pravougaonici[0].P > Pravougaonici[1].P:
print("Prvi pravougaonik ima veću površinu.")
else:
print("Drugi pravougaonik ima veću površinu.")
Za poređenje, pogledajmo kako bismo isti program zapisali bez klasa i objekata:
Double P1_a = 10.5, P1_b = 12.4;
Double P2_a = 21.3, P2_b = 7.2;
Double P3_a = 4.5, P3_b = 22.6;
Double P1_O = 2 * (P1_a + P1_b);
Double P2_O = 2 * (P2_a + P2_b);
Double P3_O = 2 * (P3_a + P3_b);
Double P1_P = P1_a * P1_b;
Double P2_P = P2_a * P2_b;
Double P3_P = P3_a * P3_b;
if (P1_P > P2_P)
{
Console.WriteLine("Prvi pravougaonik ima veću površinu.");
}
else
{
Console.WriteLine("Drugi pravougaonik ima veću površinu.");
}
Double P1_a = 10.5, P1_b = 12.4;
Double P2_a = 21.3, P2_b = 7.2;
Double P3_a = 4.5, P3_b = 22.6;
Double P1_O = 2 * (P1_a + P1_b);
Double P2_O = 2 * (P2_a + P2_b);
Double P3_O = 2 * (P3_a + P3_b);
Double P1_P = P1_a * P1_b;
Double P2_P = P2_a * P2_b;
Double P3_P = P3_a * P3_b;
if (P1_P > P2_P)
{
cout << "Prvi pravougaonik ima veću površinu.") << endl;
}
else
{
cout << "Drugi pravougaonik ima veću površinu.") << endl;
}
double P1_a = 10.5, P1_b = 12.4;
double P2_a = 21.3, P2_b = 7.2;
double P3_a = 4.5, P3_b = 22.6;
double P1_O = 2 * (P1_a + P1_b);
double P2_O = 2 * (P2_a + P2_b);
double P3_O = 2 * (P3_a + P3_b);
double P1_P = P1_a * P1_b;
double P2_P = P2_a * P2_b;
double P3_P = P3_a * P3_b;
if (P1_P > P2_P) {
System.out.printf("Prvi pravougaonik ima veću površinu.");
}
else {
System.out.printf("Drugi pravougaonik ima veću površinu.");
}
P1_a = 10.5
P1_b = 12.4
P2_a = 21.3
P2_b = 7.2
P3_a = 4.5
P3_b = 22.6
P1_O = 2 * (P1_a + P1_b)
P2_O = 2 * (P2_a + P2_b)
P3_O = 2 * (P3_a + P3_b)
P1_P = P1_a * P1_b
P2_P = P2_a * P2_b
P3_P = P3_a * P3_b
if P1_P > P2_P:
print("Prvi pravougaonik ima veću površinu.")
else:
print("Drugi pravougaonik ima veću površinu.")
Razlika nije drastična na ovako malom i jednostavnom primeru (a veći i kompleksniji ipak nismo želeli da pišemo, zarad očuvanja preglednosti), ali, nije teško uvideti koliko je programski kod lepši i pregledniji u slučaju da problem rešavamo metodom OOP-a.
Komplikovaniji primeri samo dodatno povećavaju ovu razliku u korist objektno orijentisanog programiranja (ali, setimo se ovde primedbe iz uvodnog pasusa da OOP ne treba koristiti uvek i bez potrebe i da je proceduralno programiranje, u slučaju jednostavnih programa, krajnje dobar i primeren način rešavanja problema).
Jednostvno, kao što u životu možemo od Beograda do Novog Sada putovati biciklom ili automobilom, dok bismo do Australije ipak radije putovali avionom, tako i u programiranju nećemo OOP pristup koristiti "za svaku sitnicu", već onda kada zatreba - a sada znamo i kako.