You are on page 1of 12

O objektno-orjentisanom programiranju

Ovo je članak koji će pokušati da te na nepompezan način uvede u objektno-orjentisano


programiranje - OOP.

Daleko bilo da je ovo sve što treba da znaš o toj temi. "Najbolji način da se započne put od
10000 kilometara je da se napravi prvi korak" - kineska poslovica (naravno original razdaljinu
nije merio kilometrima). Shvati ovo upravo kao pokušaj da se taj prvi korak napravi što
bezbolnije.

Pretpostavlja se da si programirao na Pascal-u, C-u ili nekom "običnom" procedurelnom jeziku


i da si osetio da ti nešto nedostaje. To "nešto" je viši nivo apstrakcije. OOP nije toliko tehnika
programiranja, koliko način razmišljanja. Naravno najbolje je da jezik direktno podržava OOP
- to omogućava da razmišljaš direktno koristeći njegova sredstva. Ali čak i na ne-objektnim
jezicima, OO razmišljanje se itekako isplati.

Parafraziraću Bjarne Stroustrup-a, tvorca C++-a: Mali program (recimo 1000 linija) može da
se napiše u bilo čemu, bilo kako. Ako si dovoljno vešt, na kraju ćeš ga naterati da radi. Ali u
velikom programu, ako se nisi pridržavao "lepog programiranja" i koristio odgovarajuće
tehnike - naročito OOP, nove greške će se pojavljivati jednako brzo kao što ispravljaš stare.

Razlog tome je što svaki deo programa zavisi od gomile drugih, ali te zavisnosti su toliko
zamršene i neintuitivne da ih čak i tvorac teško prati. Kada promeniš jedan deo programa, ne
možeš lako da sagledaš na čega sve u stvari to utiče. Naravno, valjanim procedurelnim
programiranjem može se prilično dobro modularizovati program i izdvojiti ponovo upotrebljiv
kod, ali ako tako radiš - onda već počinješ da misliš na OOP način. Ono što ti nedostaje je
bolji način da organizuješ gomilu procedura koje si kroz praksu napisao i da ih bolje povežeš
sa strukturama podataka kojima manipulišu.

Vratimo se malo na fakultet, definiciji tipa podataka: "Tip podataka je uređena dvojka čiji je
prvi element skup svih dozvoljenih vrednosti za taj tip, a drugi element skup svih operacija
koje nad tim vrednostima možemo izvršiti.". Tip je dvojstvo podataka i operacija nad njima.

Ako znaš šta je tip, onda znaš i šta je osnovni pojam OOP-a: klasa. Naime, klasa je ništa
drugo do jezičko sredstvo da se definiše tip podataka. Iskompajliraj ovo u glavi: klasa je,
znači, način da se u programskom jeziku procedure i funkcije eksplicitno povežu za strukturu
podataka. Logično pitanje sledi: "Ali ja sam u parametre procedure mogao da stavim bilo šta -
uključujući i strukturu podataka koja me interesuje - na taj način sam povezivao tip sa
njegovim operacijama. U čemu je razlika?". Razlika je u (podebljanoj) reči "eksplicitno". U
OOP-u procedure koje pripadaju klasi su na više načina "posebne" i sa njima se mogu raditi
stvari nemoguće za "smrtne" procedure. Takve procedure se nazivaju metodi.

Pa, šta će ti to u životu? Evo četiri dobra razloga:

Pod jedan, ako imaš klasu Trougao i metod Crtaj (koji joj pripada i vrši iscrtavanje trougla),
ništa te ne sprečava da napraviš i klasu Kvadrat koja takođe ima metod Crtaj (koji crta
kvadrat). Iako se isto zovu, to su različiti metodi. Znači, štedimo imena i izbegavamo
konflikte. Moduli (u Pascal-u su to unit-i) se mogu upotrebiti u ovu svrhu, a neki jezici pružaju
i posebne mehanizme u borbi za dodeljivanje logičnih imena (na pr. C++ prostori imena -
namespaces ili prilično unapređeni moduli Module-2). Ipak moduli su često preglomazni za
ovoj zadatak, pogotovo ako su (kao na Pascal-u) vezani za fizičko skladištenje sorsa - fajlove.
U Moduli-2, na primer, moduli mogu da sadrže i podmodule za koje je tačno definisano šta
daju spoljašnjem svetu i šta iz njega vide, ali to pomalo liči na gađanje muve maljem od dve
tone. Ovako, ako imaš promenljivu t klase Trougao i promenljivu k klase Kvadrat, trougao
crtaš sa t.Crtaj a kvadrat sa k.Crtaj - šta ćeš jednostavnije?

A sada dođoh i do pojma objekat. Prosto, objekat je promenljiva neke klase. Drugi naziv je
instanca ("primerak"). Klasa je definicija tipa, objekat je konkretan primerak klase. Na pr.
kada kažemo da promenljiva t ima tip (klasu) Trougao, drugi način da kažemo to isto je: t je
objekat tipa Trougao. Ma, da ne davim, t i k iz prethodnog pasusa su objekti.

Pod dva, svaka klasa može da sakrije ono što želi. Ako imaš pomoćno parče koda koje
pozivaju samo metodi unutar klase, napravićeš privatan metod, nevidljiv spolja. Ako si pisao
veće programe, ovo si opet radio preko modula (unit-a) sakrivanjem u implementation sekciju
(cpp fajl kod C++-a). Međutim, opet rizikuješ da se zatrpaš gomilom modula od kojih svaki
koristi svaki i koji su međusobno isprepletani više nego što bi zaista želeo. Ovakvo sakrivanje
zove se enkapsulacija. Neki jezici, na pr. C++ i novije verzije Jave, čak mogu da imaju klase
unutar drugih klasa, koje opet mogu da budu privatne.

Do sada, uz dovoljno samodiscipline, mogao si da preživiš koristeći klasična sredstva lepog


programiranja - pre svega module. Sada ide nešto što se jednostavno ne može uraditi bez
OOP-a.

Pod tri i najvažnije: Nasleđivanje je način da već postojeću "roditeljsku" klasu proširiš ili
izmeniš i napraviš novu "klasu dete" koja ostaje povezana sa roditeljskom. Ovo je ključ onoga
što englezi zovu code-reusability (ponovna iskoristivost koda). Ako već imaš klasu koja radi
neznatno manje od onoga što ti treba, zašto ne iskoristiti taj već napisani kod i samo dopisati
ono što ti treba? Naravno copy-and-paste programiranje može učiniti da veoma lako
"iskoristiš" ono što je već napisano. Ali time dobijaš dve komadeške koda za koje samo ti znaš
da bi trebalo da rade istu stvar - dok ne zaboraviš. Sada zamisli da si našao grešku u prvoj
kopiji - to znači da treba treba da protrčiš kroz sve ostale i napraviš identične ispravke.
Zamisli žurku kada imaš desetak kopija! Samo da te upozorim: nije neobično da ozbiljne
biblioteke klasa (kakav je Delphi-jev VCL) imaju i po nekoliko desetina nivoa nasleđivanja.
Kakav bi to bio haos da je u pitanju prosto kopiranje istog koda! A efikasnosti se još nismo ni
dotakli.

Međutim, kada se sa napravi pravo nasleđivanje, ako promeniš kod u roditeljskoj klasi,
automatski si promenio i svu decu. Sa druge strane, u većini jezika, decu možeš dodavati bez
rekompajliranja roditelja, obično čak ni ceo izvorni kod nije potreban - dosta je samo interfejs
(deklaracija klasa/ metoda bez definicije). Naravno, nema višestrukog kompajliranja
(interpretiranja) jednog te istog, pa je sve brže i zauzima manje mesta. Do sada sam govorio
uglavnom o metodima, ali nasleđivanjem možeš dodati i nova polja, no o-tom-po-tom (još ne
znaš ni kako jedna klasa izgleda kad se napiše u konkretnom programskom jeziku).

Četvrto, tesno povezano sa nasleđivanjem: polimorfizam. Ovo je nešto o čega se početnici u


OOP-u najlakše spotaknu, pa nemoj da se sekiraš ako ne svariš sve odmah. Kada naslediš
neku klasu, na fizičkom nivou joj dodaš/promeniš metode ili članove (polja), ali na logičkom
nivou, ti samo praviš novu vrstu te klase. A'jmo sad, iz neba pa u rebra, pa ko shvati,
razumeće: dete jeste roditelj. To znači: tamo gde stoji roditelj, mogu da stavim i dete,
naprosto zato što dete sadrži sve što i roditelj (iako verovatno i nešto preko toga). Uoči da
obrnuto ne važi. Naime, roditelj nije dete - dete sadrži i nešto što roditelj nema, pa roditelja
ne možemo da stavimo svuda gde možemo dete jer će nešto da nam zafali.

A sada suština: čak iako dete stavimo mesto roditelja, ono i dalje ostaje dete. Ako pozovemo
metod koji postoji i u roditelju i u detetu, koja od ove dve verzije metoda će biti pozvana
zavisi od toga da li smo u vreme izvršavanja stavili roditelja ili dete. Ako smo stavili dete, biće
pozvan metod deteta. Na pr. bez obzira što je u vreme kompajliranja parametar neke
procedure deklarisan kao roditelj, u vreme izvršavanja možemo u njega da stavimo dete, i
ono će nastaviti da se ponaša kao dete.

Inače, ovo je razlog za korišćenje pokazivača u OOP-u.

Dakle suština polimorfizma je: Objekti se isto koriste ali se različito - u skladu sa svojom
konkretnom klasom - ponašaju. U vreme kompajliranja nemamo način da odredimo koji će
konkretni objekti to biti, ali u vreme izvršavanja za svaki objekat se pozivaju metode klase
kojoj pripada, a ne roditeljske klase koja je deklarisana u vreme kompajliranja.

Da opet pozovem u pomoć Bjarne Stroustrup-a: Programski jezik ima dva aspekta: jedan
omogućava da mašini narediš šta da radi, a drugi predstavlja konceptualni alat koga ti sam
koristiš u razmišljanju o tome kako rešiti problem. Principi koje sam izložio ne samo da
postoje u svakom pristojnom objektnom jeziku, već predstavljaju jako dobar način
apstrakcije. Više se nećeš gušiti u detaljima dijagrama toga, već ćeš se izmaći, pogledati
problem sa visine i početi da ga raščlanjuješ na objekte koji u njemu učestvuje. Te objekte
ćeš razvrstati u logične "stablaste" strukture - hijererhije nasleđivanja.

Enkapsulacija

Enkapsulacija je sakrivanje onih delova programa koji rade interne stvari i sami po sebi ne bi
trebali da se tiču nekog spolja. Zašto je uopšte dobro nešto sakriti? Iz dva razloga:

Time daješ do znanja korisniku tvog koda šta mu je namenjeno i šta može slobodno da
koristi, a šta je tvoja interna stvar.
Možeš da menjaš (popravljaš, unapređuješ) ono što si sakrio a da korisnik ne mora o tome da
vodi računa - nastaviće da koristi tvoj kod na isti način, samo će se tvoj kod ponašati bolje.
Stavi se u situaciju da pišeš parče programa koje radi neku opštu stvar (na pr. neka grafička
biblioteka i sl.). Tada si ti u stvari u ulozi programera koji piše za druge programere i tvoj
odnos prema njima je sličan odnosu između njih i krajnjih korisnika. Iako krajnji korisnici ne
treba (niti su u stanju) da sagledaju kako je jedan program napisan, oni su savršeno sposobni
da ga koriste. Tako i programere koji koriste tvoj kod ne treba opterećivati internim detaljima,
već im treba izložiti efikasan "korisnički interfejs" preko koga će ga koristiti.

Recimo da je to što pišeš glomazno. Recimo da ima mnogo pomoćnog/internog koda. Recimo
da ima 10000+ linija. Naravno stalno ga razvijaš. Stalno pronalaziš i ispravljaš greške i
dodaješ nove mogućnosti. Stavi se sada u situaciju programera koji koriste tvoj kod. Kako
god napraviš neku izmenu, oni se nađu na mukama: moraju da pregledaju sopstveni kod i da
provere da li je kompatibilan sa "novom verzijom" tvog koda. Ali šta me briga, nije moj
problem? JESTE tvoj problem! Vrlo moguće da su "drugi programeri" u stvari ti sam. Čak i ako
nisu, tim je ono što pobeđuje, a ne jedan igrač.

Rešenje je odvojiti ono što je konstantno od onoga što je podložno izmenama. Svaka
biblioteka/klasa/modul... ima jedan svoj deo koji je trajno takav kakav jeste zato što je to
logično i prirodno i zato što se upravo preko njega vrši komunikacija sa spoljnom svetom.
Način komunikacije je konstantan. Menja se samo način na koji se poruke prenesene tim
konstantnim "komunikacionim kanalom" obrađuju.

Ovaj konstantni, komunikacioni deo naziva se interfejs. Ostatak je implementacija.

Klasična programerska sredstva, pre svega moduli i, na nižem nivou, blokovi (begin..end na
Pascal-u, {..} u C-olikom jezicima) pružaju dosta mogućnosti za enkapsulaciju. Međutim OOP
ima veoma razrađen model vezan za klase.

Svaki element (član ili metod) jedne klase može imati jedan od tri novoa vidljivosti:

Public elementi su potpuno vidljivi izvan klase. Oni čine interfejs ka spolješnjem svetu.
Private elementi su vidljivi samo iz metoda klase. U nekim jezicima njihova vidljivost je
proširena na modul u kome se nalaze. Ovi elementi čine implementaciju.
Protected elementi su nešto izmedju 1 i 2. Oni se prema spoljašnjem svetu ponašaju kao
private, ali su potpuno vidljivi iz klasa naslednica - za njih imaju public ponašanje. U nekim
jezicima je protected element vidljiv ne samo iz klasa naslednica, već i u celom modulu u
kome se klasa tog elementa nalazi. Iako, strogo govoreći, ovi elementi pripadaju
implementaciji, oni u stvari čine interfejs ka klasama naslednicama.
Za objašnjenje nasleđivanja pogledajte O OOP-u i Nasleđivanje.

Napravimo jednu Delphi klasu:

TTrougao = class
private
X,Y,A: integer;
protected
procedure Initialize;
public
procedure Place(x_arg,y_arg: integer);
procedure Draw;
published
end;
Ovde samo metodi Initialize, Draw i Place mogu da pristupe X,Y i A. Nikakva spoljašnja
funkcija ne može da pristupi X, Y i A. Initialize može da se pozove iz Draw i Place i svih
metoda klasa naslednica, a Draw i Place mogu da se pozove svuda gde je i sama klasa
vidljiva.

Delphi još dozvoljava pristup private i protected elementima iz istog modula. Java ima slično
ponašanje kada su paketi (packages) u pitanju. C++ je stroži (a i nema prave module), pa
nema takve vidljivosti private i protected elemenata, ali zato podržava takozvani friend
mehanizam. Delphi podržava i četvrti, published specifikator koji ima ulogu u vizuelnom
programiranju. Ali to nije naša tema na ovom mestu.

Nasledjivanje

Jedno od najvažnijih (i još uvek samo delimično rešenih) pitanja u programiranju glasi: "Kako
ne izmišljati ponovo točak?". Malo drugačije formulisano, problem glasi: kako napisati nešto,
a da se to nešto može upotrebiti što više puta? Priznajem, zvuči čudno.

Ipak, svako ko je iole intenzivnije programirao, pisao je mnogo puta sličan (ili isti!) kod u
različitim programima ili čak istom programu.

Zbog čega?

Ima više razloga - jedan je tradicionalna lenjost programera. Naprosto, bilo je jednostavnije
ad-hoc uraditi tačno ono što treba, tačno tamo gde treba, nego pokušati izdvojiti opšte
rešenje koje bi se kasnije specijalizovalo na konkretne slučajeve. Takvo "copy-and-paste"
programiranje, u kome samo prepišeš (i po potrebi izmeniš) kod¸ predstavlja sredstvo da se
program završi brzo i da se brzo natera da radi. Ali reč "brzo" u ovom slučaju znači "kuso" - i
jeste noćna mora kada je održavanje i nadogradnja u pitanju.

Da ne krivimo samo programere, ni jezici nisu podržavali "apstraktni" način razmišljanja koji
bi omogućio da razmišljaš u pojmovima bliskim problemu a ne u pojmovima, kakvi su bajtovi
i pokazivači, bliskim mašini.

U klasičnom, "proceduralnom" ili "strukturnom" programiranju postoje tehnike da se kod, koji


radi neku opštu stvar, izdvoji i ponovo upotrebi (mada neki popularni xBase jezici čak i u
tome veoma šantaju). Najznačajnije je postajanje "procedura", "funkcija" ili "potprograma".
Druga tehnika su moduli, koji se zapravo javljaju u tri uloge: enkapsulacija, ponovna
upotreba koda kroz procedure koje se u njima nalaze i (u većini jezika) fizička organizacija -
moduli su često u bijekciji sa fajlovima.

Čini se da ovo rešava problem ponovne upotrebljivosti koda, ali na duže staze (i u većim
programima) to jednostavno nije to - upadamo u zamku razmišljanja na nivou mašine, a ne
problema.

Postoje bolja, objektna, rešenja koja ne negiraju te klasične tehnike, već ih organizuju na
jednom višem nivou. Ključno od njih je nasleđivanje (engl. inheritance).

Pogledaj O objektno orjentisanom programiranju za još malo argumentacije šta će nam


uopšte ponovna upotrebljivost koda (engl. code-reusability) i šta su osnovni pojmovi OOP-a
koje ću koristiti u priči koja sledi.
Nasleđivanje je način da već postojeću klasu proširiš ili izmeniš, praveći njenu naslednicu
(dete) u kojoj napišeš samo to što je promenjeno ili izmenjeno. Sve ostalo, ona nasleđuje od
roditelja. Bitno je da klasa dete ostaje vezana za roditelja - kada se roditelj promeni, time se
automatski menjaju i sva njegova deca, u onim svojim delovima koje su nasledila od roditelja.
Time, ako ispraviš bag u roditelju, ispravio si bag u svoj deci; ako nadogradiš roditelja
nadogradio si svu decu.

Sada bi bio red da dam primer, a upravo su primeri nezgodna tačka objašnjavanja OOP-a.
Naime pimeri su mali, a upravo na male količine koda odnosi se ona primedba Bjarne
Stroustrup-a "da se mali program može napisati bilo kako ako si dovoljno vešt". Na žalost nije
praktično davati primere od 10000 linija, pa nemoj odmah da se buniš: "Ali ovo sam ja radio
klasičnim tehnikama...".

Recimo da nam treba klasa koja crta kvadrat:

Pisaću na objektnom Paskalu, Delphi-jevom jeziku. Namerno ću je oblikovati tako da


ispočetka nije najsažetija, ali da se može dobro naslediti.

// Ovo ide u interface sekciju Pascal-ovog unit-a.


type
Square = class
private
Visible: boolean; // false ako je nevidljiv.
X,Y: integer; // Koordinate centra.
A: integer; // Duzina stranice.
protected
// Fizicki crta na osnovu X,Y,A.
procedure Draw; virtual;
// Brise i obnavlja pozadinu.
procedure Clear; virtual;
public
procedure Place(x_arg,y_arg: integer); // Pozicioniraj.
procedure SetA(a_arg: integer); // Postavi stranicu.
procedure Show; // Prikazi kvadrat.
procedure Hide; // Sakrij ga.
end;
// Ovo se stavlja u implementation sekciju Pascal-ovog unit-a.
procedure Square.Draw;
begin
// Crtaj kvadrat sa stranicom A, na poziciji X,Y.
end;
procedure Square.Clear;
begin
// Izbrisi kvadrat i obnovi pozadinu.
end;
procedure Square.Place(x_arg,y_arg: integer);
begin
X := x_arg;
Y := y_arg;
if Visible then
Show; // Da bi se po potrebi pozvao i Clear.
end;
procedure Square.SetA(a_arg: integer);
begin
A := a_arg;
if Visible then
Show;
end;
procedure Square.Show;
begin
if Visible then
Clear
else
Visible := true;
Draw
end;
procedure Square.Hide;
begin
if Visible then
begin
Clear;
Visible := false
end
end;
Pogledaj "Enkapsulacija" za objašnjenje šta znače public, protected i private.

Ovo je jedan živahan kvadrat koji se automatski pomeri kada mu promeniš veličinu ili
poziciju, pri čemu pazi da ne ostavi đubre za sobom - automatski se izbriše ono što je iscrtao
dok je bio na prethodnoj poziciji/veličini. Kako to brisanje (i iscrtavanje) fizički realizovati
jedna je druga tema koja jako zavisi od konkretnog jezika i ciljne platforme - za naš primer to
je totalno nebitno.

Takođe, metodi Draw i Show su deklarisani kao virtuelni. Ovo je jedan pojam vezan za
polimorfizam - objasniću ga za trenutak.

Sada, recimo da hoćemo da napravimo dvostruki kvadrat koji se sastoji iz jednog spoljašnjeg
i jednog unutrašnjeg:

Uočimo da da ovaj dvostruki kvadrat ima dosta zajedničkog sa "normalinom" jednostrukim -


moraju mu se znati koordinate centra i dužina stranice. Postoji jedan detalj koji je nov -
dužina stranice drugog kvadrata (A1).

Pa zašto onda ne bismo iskoristili ono što smo već napisali (klasu Square) i samo dodali taj
novi detalj - prava prilika za nasleđivanje. Suma sumarum, treba uraditi tri stvari:

Dodati metod kojim zadajemo dužinu stranice drugog kvadrata.


Proširiti metod koji crta kvadrat, tako da iscrta i drugi kvadrat. Najzgodnije je pozvati već
postojeći metod Square.Draw, a zatim samo dopisati kod koji crta drugi kvadrat.
Proširiti metod koji briše kvadrat, tako da izbriše i drugi kvadrat. Kao i pod 2), pozvaćemo već
postojeći metod Square.Clear, uz dopisivanje koda za brisanje drugog kvadrata.
Naravno treba naterati već napisane metode koji pozivaju Draw i Clear da pozovu baš ove
nove, izmenjene, verzije. Opet uočite da su one deklarisane kao virtuelne i strpite se još malo
do konačnog objašnjenja.

Evo koda:

type
DoubleSquare = class(Square)
private
A1: integer; // Stranica drugog kvadrata.
protected
procedure Draw; override;
procedure Clear; override;
public
procedure SetA1(a1_arg: integer);
end;
procedure DoubleSquare.Draw;
begin
inherited; // Poziva Square.Draw.
// Ovde treba iscrtati kvadrat staranice A1.
end;
procedure DoubleSquare.Clear;
begin
// Ovde treba izbrisati kvadrat staranice A1.
inherited; // Poziva Square.Clear.
end;
procedure DoubleSquare.SetA1(a1_arg: integer);
begin
A1 := a1_arg;
if Visible then
Show;
end;
Napomena: override je kljucna reč Delphi-ja koja označava da nasleđujemo virtualnu metodu.
Neki jezici (C++) ne zahtevaju nikakve ključne reči.

Znači, dobili smo dvostruki kvadrat koji uz metode iz klase Square ima i metod SetA1
(postavlja dužinu stranice drugog kvadrata) i koji se, začudo, ispravno iscrtava iako nismo
direktno promenili stare javne metode. Ali promenili smo metode za iscrtavanje koji su od
početka bili deklarisani kao virtuelni i time naterali Show da pozove novi DoubleSquare.Draw i
DoubleSquare.Clear, a ne Square.Draw i Square.Clear. Isto se desilo za Hide, a pošto se ostali
metodi oslanjaju na Show i Hide, time i oni posredno pozivaju ispravne Draw i Clear.

Pa šta su virtuelni metodi? To su metodi koji se pozivaju u zavisnosti od toga za koju klasu
(zadatu u vreme izvršavanja) su pozvani. Drugim rečima, koji će virtuelni metod biti pozvan
ne zavisi od deklaracije klase u vreme kompajliranja, već od toga koje je klase objekat za
koga pozivamo metod u vreme izvršavanja.

U ovom konkretnom slučaju, (na primer) Show je deklarisan kao metod klase Square i
sledstveno tome poziva Square.Draw. Da Draw nije virtuelan, i kada pozovemo Show za
objekat klase naslednice kakva je DoubleSquare, on bi pozivao metode koje je originalno
pozivao. Znači pozvao bi Square.Draw. Međutim, pošto je Draw virtuelan, Show će prvo
proveriti koja je klasa objekta za koga je pozvan i pozvaće metod te klase - znači
DoubleSquare.Draw.

Kako koristimo dobijenu klasu? Isto kao i početnu!

// ds je tipa DoubleSquare.
ds.Place(100,50);
ds.SetA(40);
ds.SetA1(30);
ds.Show // Bice pozvan DoubleSquare.Draw.
Odmakni se sada korak unazad i pogledaj šta smo dobili. Sa izuzetno malo koda napravili smo
klasu DoubleSquare. Ostavili smo mogućnost da i DoubleSquare bude nasleđena (u na pr.
TripleSquare). I na kraju, ako klasi Square (na pr.) dodamo mogućnost da menja boju, sve
njegove naslednice će dobiti tu mogućnost.

Ovde je izložen i jedan važan slučaj polimorfizma, a za još primera pogledaj "Polimorfizam".

--------------------------------------------------------------------------------

Još neke tehnike za ponovnu upotrebu koda


Komponente su samo specijalne klase koje mogu da se vizuelno programiraju i predstavljaju
osnovu za RAD (Rapid Application Development - brz razvoj aplikacija). Objektno-orjentisane
biblioteke komponenti, kakva je Delphi-jev VCL, predstavljaju istovremeno i veliku,
razgranatu hijerarhiju nasleđivanja. Primer moći ovakvog koncepta predstavlja Anchor
property, koji je, u verziji 4, dodat jednoj od osnovnih klasa VCL-a, čime su sve vizuelne
komponente (njene naslednica) dobile tu istu osobinu - moćniju kontrolu pozicioniranja na
formi!

Neki jezici, kao što je Visual Basic, nemaju mogućnost pravog nasleđivanja od strane
programera (VB za sada podržava samo tzv. nasleđivanje interfejsa), ali podržavaju
komponente koje su obično interno objektno orjentisane. Biblioteke ovakvih komponenti su
po pravilu manje elegantne.

Jedna od novijih (i spektakularnijih) tehnika za ponovno iskorišćavanje već napisanog koda je


i takozvano generičko programiranje. Od komercijalnijih jezika C++ i, na jednom drugom
nivou, Java imaju podršku za njega. Generičko programiranje samo po sebi ne mora da bude
vezano za OOP, ali mu njegovo prisustvo veoma prija. Ali to je već druga (i duga) tema.

Polimorfizam
Ažurirano: 08-10-1999

Recimo da nam treba gomila objekata koji se međusobno razlikuju, ali se koriste na isti način.
Tipičan primer, koji se naširoko rabi u OOP literaturi su geometrijske figure. Zar ne bi bilo
lepo ostaviti korisniku da napuni neku strukturu podataka kvadratima, trouglovima,
krugovima,... i pozvati Crtaj za sve njih bez dodatnih komplikacija (ispitivanja koji su to u
stvari tipovi).

Tradicionalno, u te svrhe su se koristile klasične strukture ili unije i polje za označavanje tipa.
Ovakav pristup ima dve glavne mane:

Loša apstrakcija i neracionalno trošenje prostora jer sve tipove moramo da sabijemo u jedan
format. Ako to sabijanje (sa stanovišta prostora) efikasno uradimo, sva je prilika da smo vršili
previše "peglanja" samih tipova, koji zbog toga više ne odgovaraju na najbolji način onome
što reprezentuju. Ako smo, pak, tipove ostavili nepromenjenim, ostaje mnogo "lufta" između
fizički najvećeg i i najmanjeg od njih, pa je to razbacivanje prostora. Naravno mogu se
koristiti i trikovi, poput pokazivača i dinamičkog lociranja memorije, ali to opet nosi probleme.

Nestandardizovanost polja za označavanje tipa. Moraš da pamtiš šta koja vrednost označava.
Enumeracije (nabrajanja) mogu da pomognu, ali opet - to nije to.
Polimorfizam je sposobnost objekta da se ponaša u skladu sa klasom kojoj zaista u vreme
izvršavanja pripada.

Bez nasleđivanja, polimorfizam i nema mnogo smisla. Naime, da nije nasleđivanja, ne bi ni


mogla da se desi situacija da se tamo gde je deklarisana jedna klasa, stavi objekat neke
druge (njene naslednice). A pošto to, zahvaljujući nasleđivanju, može da se desi, treba nam
mehanizam koji će delovati u vreme izvršavanja (run-time), a ne kompajliranja, koji će
obezbediti da objekat bude svestan kojoj klasi pripada, bez obzira što je stavljen na mesto
koje je deklarisano za klasu-roditelja.

Polimorfizam je upravo takav mehanizam.

Polimorfizam je izuzetno važan za efikasno nasleđivanje. Obično služi da "natera" metode


roditelja da koriste redefinisane metode deteta, za čega je dobar primer dat u nasleđivanju.

Druga situacija je izložena na početku: imamo gomilu objekata koji se isto koriste, ali različito
ponašaju. Rešenje je napraviti jednu osnovnu klasu koja će sadržati zajedničke metode. Ove
metode ćemo obavezno učiniti virtuelnim. Zatim ćemo iz nje izvesti sve ostale klase koje nam
trebaju, deklarisati niz čiji su elementi deklarisani kao objekti osnovne klase, ali ćemo u
vreme izvršavanje stavljati objekte izvedenih klasa.
Virtuelni metod je metod za koga važi polimorfizam.

U nekim jezicima (Smalltalk) čak ne može da se definiše metod koji nije virtuelan -
polimorfizam uvek aktivan. U većini jezika, pak, možeš ostaviti metod nevirtuelnim, čime
osiguravaš da će uvek biti pozvan baš on, a ne neki koji ga je zamenio u naslednici. Treba
dobro paziti kojim metodima dati ovakvo ponašanje, a kojim ne.

Pa da damo primer. Imamo neke figure koje treba iscrtati na ekranu. Prvo deklarišemo
osnovnu klasu:

Figura = class
public
x,y: integer;
procedure Draw; virtual; abstract;
end;

Bitno je uočiti da je metod Draw deklarisan kao virtuelan. On je takođe deklarisan i kao
abstraktan. To znači da je deklarisan samo zato da bi bio nasleđen. Nikakva implementacija
tog metoda ne postoji u klasi Figura. Tek njene naslednice će ga redefinisati (override) i
konkretno implementirati. U jeziku C++, ovo se zove čista virtuelna funkcija članica.

Napomena: engleski termin override označava redefinisanje metoda prilikom nasleđivanja,


bez obzira na to da li je taj metod bio ne-virtuelan, virtuelan ili abstraktan virtuelan.

Klasa koja sadrži bar jedan abstraktni metod zove se abstraktna klasa. Klasa čiji su svi metodi
abstraktni i koja nema polja, često se zove interfejs - to i nije ništa drugo do deklaracija
interfejsa koji nema praktični smisao sve dok ga klase naslednice ne implementiraju.

Sada nam treba Kvadrat, Trougao i Krug:

// Zajednicko za kvadrat i trougao.


Mnogougao = class(Figura)
private
a: integer; // Duzina stranice.
end;

Kvadrat = class(Mnogougao)
public
procedure Draw; override;
end;

Trougao = class(Mnogougao)
public
procedure Draw; override;
end;

Krug = class(Figura)
private
r: integer; // Poluprecnik.
public
procedure Draw; override;
end;

...

// Implementacija za Kvadrat.Draw, Trougao.Draw i Krug.Draw.

...

Sada već možemo da napravimo strukturu podataka koja će se okoristiti o ovakvu


organizaciju. Neka to bude niz figura:

TFigure = array[0..2] of Figura;

Kao što je i objašnjeno u OOP i pokazivači, ovo je u stvari niz pokazivača koji su deklarisani
da pokazuju na klasu Figura, s tim što ćemo u run-time mi stavljati njene naslednice.

var
Figure: TFigure;
i: integer;

...

// Ovde moze da stoji i neki slozeni crtacki GUI, koga ne mozemo potpuno da kontrolisemo.
Figure[0] = Kvadrat.Create;
Figure[1] = Trougao.Create;
Figure[2] = Krug.Create;
for i:=Low(Figure) to High(Figure) do
begin
Figure.x := Rnd; // Neka Rnd generise slucajne vrednosti.
Figure.y := Rnd
end;

// Crtamo unete figure. Ovde ne moramo da znamo koje su u stvari klase ti objekti.
for i:=Low(Figure) to High(Figure) do
Figure.Draw;

...

// Ovo u Javi nije potrebno.


for i:=Low(Figure) to High(Figure) do
Figure.Free; // Sprecavamo memory-leak.

--------------------------------------------------------------------------------

Kako je to realizovano?

Virtuelni metodi imaju takozvano "dinamičko vezivanje", nasuprot "statičkom", kod običnih
procedura. Kod statičkog povezivanja, kompajler i linker tako srede mašinski kod da se u
njemu nalazi konkretna adresa procedure.

Kod dinamičkog povezivanja, prvo se referencira pokazivač unutar samog objekta koji
pokazuje na jednu specijalnu struktuuru - virtuelnu tabelu poziva. Taj dodatni pokazivač
postoji u svakom objektu čija klasa ima virtuelne metode. Virtuelna tabela poziva sadrži
pokazivače na metode složene po tačno utvrđenom redosledu koji se ne menja
nasleđivanjem.

Mašinski kod koji treba da izvrši poziv virtuelne metode ne sadrži nikakvu adresu, već redni
broj metoda u virtuelnoj tabeli poziva. Kada se taj kod izvrši, pročitaće pokazivač na virtuelnu
tabelu iz samog objekta i time dobiti virtuelnu tabelu koja odgovara klasi tog objekta u vreme
izvršavanja. Zatim će pročitati pokazivač na metod koji odgovara rednom broju i skočiti na
njega - što je tačno ono što nam treba.
Na mašinskom nivou ovo se svodi na jedno dereferenciranje i jedno indeksiranje niza, što je
prilično brza operacija, ali izvesno usporenje ipak postoji. Zbog toga treba biti oprezan kod
malih ili često korišćenih metoda.

Za svaku novu klasu generiše se njena virtuelna tabela. U prošlosti su (u složenim


hijerarhijama) te tabele dostizale neprihvatljive razmere (neki 16-bitni memorijski modeli
behu osetljivi na ovo), pa su smišljani razni mehanizmi koji su upravo suprotni dobroj OOP
teoriji. C++ biblioteke MFC i OWL su primeri.

Danas, u vreme velikih i jeftinih računarskih resursa, to u najmanju ruku nije problem, pa
modernije biblioteke, poput VCL-a i AWT-a ne opterećuju programera ovakvim
anahronizmima.

OOP i pokazivaci

Ako si programirao u alatima poput Delphi-ja ili Jave, verovatno si uočio da se promenljive
koje ne pripadaju nekom osnovnom tipu, nego su primerci (instance) klase, ne ponašaju baš
očekivano. To je zbog toga što u pitanju nisu objekti već pokazivači na njih!

Verovatno si u početku bio zbunjen zašto se u Delphi-ju mora zadati Owner ili dobro paziti da
se pozove Free, C++ ima new i delete (ili u boljem slučaju nekekav auto_ptr), a Java ima
"sakupljač smeća". Evo u čemu je stvar.

Naime, objekti koji se u vreme izvršavanja stavljaju na potrebna mesta u programu (kao
parametri funkcija i sl.) ne moraju da budu instance (primerci) one klase koja je tu
deklarisana u vreme kompajlieranja, već mogu da budu iz neke klase NASLEDNICE. Znači
možeš da deklarišeš funkciju koja kao parametar prima klasu Roditelj, a u vreme izvršavanja
pozoveš je prosleđujući joj parametar klase Dete.

Sposobnost da mesto klase roditelja staviš klasu dete značajna je za polimorfizam - dete se
ponaša slično, a opet na svoj način.

Pa u čemu je problem? Naime, prosti tipovi imaju tačno određenu veličinu i strukturu. Zbog
toga promenljiva preko koje pristupaš prostom tipu u sebi samoj sadrži objekat (instancu,
primerak) tog tipa. Drugim rečima, kada upotrebiš promenljivu prostog tipa, ti direktno
pristupaš komadu memorije koji leži "pod njom".

Kod klasa, situacija se iz korena menja. Kada napraviš svoju klasu, kompajler tačno zna
(izračuna) koje je veličine, t.j. koliko memorije treba za nju i na kojim mestima se nalaze
njena polja. Međutim, pošto mesto roditeljske klase možeš da staviš dete (čiji se tip, pa ni
veličina i interna struktura, ne zna u vreme kompajliranja, već samo u vreme izvršavanja),
kompajler dolazi u nezgodnu situaciju da NE ZNA koliko memorije treba da odvoji za nju. A to
znanje je jednostavno NEOPHODNO za ostvarivanje mehanizama kakav je pozivanje funkcija
(tu se koristi struktura podataka koja se zove stek). Zbog toga se pribegava triku.

Ono što je u Delphi-ju promenljiva class tipa, a u Javi se zove "reference type" je u stvari
POKAZIVAČ (pointer) na stvarni objekat. Pokazivač je promenljiva koja sadrži memorijsku
ADRESU druge promenljive i time "pokazuje" na nju. Zbog toga, kada praviš promenljivu koja
pripada nekoj klasi, ti u stvari (implicitno) praviš dva objekta: 1) objekat klase i 2) pokazivač
(referencu) na njega. Objektu klase u stavari ne pristupaš direktno, već preko pokazivača.

Zašto je to dobro? Zato sto je pokazivač u stvari celobrojna promenljiva čija se veličina tačno
zna, pa kompajler njime može slobodno da manipuliše.

Inače, "referenca" i "pokazivač" su manje-više sinonimi, samo što se "referenca" više


upotrebljava u jezicima u kojima je rad sa njima automatizovan. Verovatno si čuo da Java
nema pokazivače. Java u stvari vrvi od pokazivača, samo što su namenjeni isključivo u
objektne svrhe i ne postoji mogućnost za (veruj mi) podmukle greške tipa:
"Pokazivač pokazuje na nepostojeći objekat" (tzv. viseći pokazivač) - jer ne možeš da
deklarišeš referencu, a da ona ne pokazuje na neki objekat, niti tu referencu možeš kasnije da
promeniš tako da ne pokazuje ni na šta.
"Objekat postoji, ali mu se ne može pristupiti jer ne postoji ni jedan pokazivač na njega" (tzv.
memory leaking - curenje memorije) - jer Java ima "skupljanje smeća" (garbage collecting).
Java virtuelna mašina automatski "broji" reference na neki objekat, i kada ih više nema,
automatski uništava objekat.
Delphi, pak nije toliko zaštićen od ovakvih grešaka. Ako deklarišeš promenljivu a ne dodeliš
joj vrednost sa promenljiva:=klasa.Create, ti u stvari imaš viseći pokazivač. Ako pak uništiš
objekat, u referenci ostaje adresa na kojoj je objekat nekada bio. Ako pak izvršiš Create, a ne
zadaš Owner ili izvršiš Free, dobio si curenje memorije. C++ ima slične probleme.

Upotreba ovakvog "pokazivačkog" objektnog modela ima jednu slabost (ili barem osobinu o
kojoj se mora voditi računa). Kada dodeljuješ jednu reference promenljivu drugoj, ti u stvari
kopiraš samo pokazivač, a ne i objekat na koga pokazuje. To te u ovom slučaju ostavlja sa
dve reference na isti objekat. Onaj drugi objekat će verovatno u slučaju Jave odmah biti
počišćen od strane garbage collector-a, a u drugim jezicima postaje memory leak. Ako to nije
ono što si želeo, treba da upotrebiš poseban metod (na pr. Delphi-jev Assign) koji će da
napravi efektivnu kopiju objekta klase a ne samo pokazivača.

You might also like