Navigacija
Lista poslednjih: 16, 32, 64, 128 poruka.

Osnove OOP-a

[es] :: Art of Programming :: Osnove OOP-a
(TOP topic)

[ Pregleda: 20525 | Odgovora: 5 ] > FB > Twit

Postavi temu Odgovori

Autor

Pretraga teme: Traži
Markiranje Štampanje RSS

Mihailo
Mihailo Đorić

Član broj: 1016
Poruke: 2875
*.verat.net



+1 Profil

icon Osnove OOP-a31.08.2002. u 02:17 - pre 262 meseci
Naleteo sam na jako dobar tekst o OOP-u (objektno-orjentisano programiranje) "za neupućene" na adresi http://bepp.8m.com/srpski/oop/o_oop.htm. Pošto ništa slično nemamo na forumu , a teško da neko može odvojiti toliko vremena da napiše kvalitetan teskt na tu temu, posle konsultacija sa Gojkom prenosim ceo tekst u originalu.

Ako imate komentar ili pitanje otvorite novu temu.

[Ovu poruku je menjao Mihailo dana 31.08.2002 u 02:31 AM GMT]
 
Odgovor na temu

Mihailo
Mihailo Đorić

Član broj: 1016
Poruke: 2875
*.verat.net



+1 Profil

icon Re: Osnove OOP-a31.08.2002. u 02:20 - pre 262 meseci
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.
 
Odgovor na temu

Mihailo
Mihailo Đorić

Član broj: 1016
Poruke: 2875
*.verat.net



+1 Profil

icon Re: Osnove OOP-a31.08.2002. u 02:23 - pre 262 meseci
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.
 
Odgovor na temu

Mihailo
Mihailo Đorić

Član broj: 1016
Poruke: 2875
*.verat.net



+1 Profil

icon Re: Osnove OOP-a31.08.2002. u 02:26 - pre 262 meseci
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.
 
Odgovor na temu

Mihailo
Mihailo Đorić

Član broj: 1016
Poruke: 2875
*.verat.net



+1 Profil

icon Re: Osnove OOP-a31.08.2002. u 02:29 - pre 262 meseci
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.
 
Odgovor na temu

Mihailo
Mihailo Đorić

Član broj: 1016
Poruke: 2875
*.verat.net



+1 Profil

icon Re: Osnove OOP-a31.08.2002. u 02:30 - pre 262 meseci
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.
 
Odgovor na temu

[es] :: Art of Programming :: Osnove OOP-a
(TOP topic)

[ Pregleda: 20525 | Odgovora: 5 ] > FB > Twit

Postavi temu Odgovori

Navigacija
Lista poslednjih: 16, 32, 64, 128 poruka.