Podsetnik: napisali ste klase za jezike Java i C++. Napravili ste osnovnu i izvedene klase čitalaca našeg časopisa, i zatim napravili konkretne čitaoce, tj. objekte tih klasa. Klase su zapisane u odvojenim fajlovima, dok izvršavanje programa i kreiranje objekata počinje unutar glavnih funkcija main() u fajlovima main.cpp (C++) i primer.java (Java). Zahvaljujući nasleđivanju, deo kôda iz bazne klase automatski važi i u izvedenim klasama. Međutim, to nije jedina korist koju imamo od nasleđivanja. Jedna od najnaprednijih tehnika u programiranju je klasni polimorfizam.Četiri pravila objektno orijentisanog programiranja Polimorfizam Java i C++, za razliku od JavaScripta, zahtevaju da već prilikom uvođenja promenljive u program, odredite i njen tip. To može da predstavlja problem u nekim situacijama, što ćemo prikazati na primeru iz prošlog broja. Otvorite prošlomesečni „"Svet kompjutera"” i još jednom pogledajte kako izgledaju naše klase. Bazna klasa koja predstavlja čitaoca „"Sveta kompjutera"” ima osnovne informacije o čitaocu i metodi koja ispisuje te informacije na konzolu. Nasleđene klase bliže određuju čitaoce tako što uvode novu promenljivu koja sadrži ime omiljene igre, dok je metoda za ispisivanje podataka overrideovana, tako da dodatno ispisuje i tu igru. Nadograđujući primer, zamislite da želite da čitaoce stavite u niz. Pored klase Avanturista koju ste napravili, na sličan način možete da dodate klase Strateg, Sportista itd. Neka je Pera objekat klase Avanturista, Žika - Strateg, a Laza - Sportista. Međutim, mi ne možemo sve njih da stavimo u niz, jer niz mora da bude samo jednog tipa, dakle, ili niz avanturista, ili niz stratega, ne mogu da se mešaju.Nasleđivanje uskače u pomoć. Pošto su sve klase čitalaca izvedene iz naše bazne klase SKČitalac, rešenje je da napravite niz bazne klase. U taj niz možete da stavite sve objekte iz klasne hijerarhije, bili oni bazni ili izvedeni! Da biste prikazali podatke igrača koje ste stavili u niz, dovoljno je da prođete kroz niz, recimo preko for petlje, kao što ste već radili, i da za svaki član niza pozovete metodu za ispisivanje podataka. Iako izgleda da smo degradirali naše objekte, jer smo ih stavili u niz bazne klase od koje su potekli, svaki član niza će pozvati metodu svoje prave, izvedene klase! To je polimorfizam. U listingu smo prikazali samo izmene u glavnom delu programa u odnosu na prethodni broj. Da bi se kôd uspešno pokrenuo, napravite sami klasu Strateg, potpuno analogno klasi Avanturista. ----------------------- C++ ----------------------- //main.cpp #include <iostream> #include "Avanturista.h" #include "Strateg.h" using namespace std; int main(int argc, char** argv) { Avanturista* Pera = new Avanturista(); Pera->brojGodina = 25; Pera->ime = "Pera"; Pera->omiljenaRubrika = "Test Play"; Pera->omiljenaAvantura = "The Dig"; Strateg* Zika = new Strateg(); Zika->brojGodina = 35; Zika->ime = "Zika"; Zika->omiljenaRubrika = "Test Play"; Zika->omiljenaStrategija = "Myth"; SKCitalac* nizCitalaca[2]; nizCitalaca[0]=Pera; nizCitalaca[1]=Zika; nizCitalaca[0]->podaciCitaoca(); nizCitalaca[1]->podaciCitaoca(); delete Pera; delete Zika; delete nizCitalaca; } ---------------------- Java ---------------------- //primer.java package primer; public class Primer { public static void main(String[] args) { Avanturista Pera = new Avanturista(); Pera.brojGodina = 25; Pera.ime = "Pera"; Pera.omiljenaRubrika = "Test Play"; Pera.omiljenaAvantura = "The Dig"; Strateg Zika = new Strateg(); Zika.brojGodina = 25; Zika.ime = "Zika"; Zika.omiljenaRubrika = "Test Play"; Zika.omiljenaStrategija = "Myth"; SKCitalac[] citaoci = {Pera, Zika}; for (int i = 0; i < citaoci.length; i++) { citaoci[i].podaciCitaoca(); } } } Da stvar bude jasnija pogledajte primer polimorfizma iz drugog, prostijeg ugla. Dodajte ovo, recimo na kraj postojećeg kôda. ----------------------- C++ ----------------------- Avanturista* Pera = new Avanturista(); SKCitalac* nekiCitalac; //ne mora new, pokazacemo na Peru nekiCitalac = Pera; nekiCitalac->podaciCitaoca(); //prikazuju se podaci Perine klase ---------------------- Java ---------------------- Avanturista Pera = new Avanturista(); SKCitalac nekiCitalac; // ne mora new jer ce postati Pera nekiCitalac = Pera; nekiCitalac.podaciCitaoca(); //prikazuju se podaci Perine klase Java primer već radi, ali do kraja poglavlja slede objašnjenja za C++, jer smo uveli neke nove pojmove, i njegov kôd mora da se dopuni da bi primer „proradio”. Članovi niza će ispisati podatke bazne, umesto izvedene klase. Zato metod u baznoj klasi proglasite virtuelnim. To se jednostavno radi dodavanjem reči virtual u header fajlu klase SKČitalac. virtual void podaciCitaoca(); U slučaju Jave, sve metode su podrazumevano virtuelne, pa ovaj korak nije neophodan. Potrebna je još jedna izmena za C++, a to je da se koriste pokazivači umesto običnih promenljivih. Kod Jave ne morate da se dvoumite, uvek morate objekat klase da kreirate naredbom new. Za polimorfizam u C++-u morate takođe da koristite naredbu new, s tim što se tada stvari komplikuju uvođenjem pokazivača, na čemu se ne bismo previše zadržavali na ovom nivou znanja. Kada koristite new, vi sami alocirate memoriju kada pravite pokazivač (pointer). Promenljiva postaje pokazivač, tako što između tipa i naziva objekta dodate zvezdicu. Taj objekat ubuduće koristite sa zvezdicom u imenu. Zbog nezgrapnog pristupa članovima objekta, često se koristi skraćeni zapis: ----------------------- C++ ----------------------- (*Zika).brojGodina = 35; Zika->brojGodina = 35; // laksi zapis Za razliku od Jave, korišćenje pokazivača zahteva od vas da sami očistite memoriju koju je on zauzeo, jer ne postoji garbage collector. Ovo može da bude komplikovanije nego što izgleda (kada su recimo članovi klase takođe pokazivači, a ne obične promenljive). Osim za polimorfizam, nemojte da koristite pokazivače, ukoliko za to nemate valjani razlog. Alternativa pokazivačima je korišćenje referenci koje imaju jednostavniji kôd, ali i manju fleksibilnost, ili korišćenje pametnih pokazivača koji sami oslobađaju memoriju, što je tek kasnije dodato u C++-u. Ako budete želeli da detaljnije učite ovaj jezik, pokazivačima i referencama se više pozabavite kada postanete iskusniji programer. Obične promenljive će, u većini slučajeva, biti sasvim dovoljne. Mi smo, dobre prakse radi, obrisali naše pokazivače komandom delete, iako nije bilo neophodno, jer odmah sledi kraj programa. Pošto u prethodnom broju nismo napisali primer za više od jedne izvedene klase, moramo skrenuti pažnju na tzv. include guard-ove (C++). U baznoj klasi SKČitalac, u header fajlu, napišite: ----------------------- C++ ----------------------- //SKCitalac.h #include <string> #ifndef SKCitalac_H #define SKCitalac_H class SKCitalac { //...ostatak klase virtual void podaciCitaoca(); }; #endif Razlog pisanja guard-ova je sprečavanje dupliranja kôda bazne klase. Kako i Strateg i Avanturista inkluduju fajl sa baznom klasom, a to je isto kao da ste sadržaj tog fajla samo prekopirali na mesto include-a, izgledaće kao da ste baznu klasu dva puta definisali. To nije dozvoljeno. NetBeans će automatski po kreiranju klase da napravi ove guard-ove za svaku klasu, i njih uvek ostavite u kôdu. U našim primerima to nije bilo potrebno sve do sada, pa smo ih brisali zbog čitkosti tekstova. Konstruktori i overload-ovanje funkcija, This Konstruktor je metoda u klasi, čije ime je isto kao i ime klase. Može imati proizvoljan broj parametara, a ne vraća vrednost. Ova specijalna funkcija klase, automatski se izvršava svaki put kada kreirate objekat. Pogledajte u primeru kako da kreirate konstruktor za baznu klasu SKČitalac. Zatim, pogledajte kako smo napravili konstruktor u izvedenoj klasi, koji poziva konstruktor bazne klase, kako ne bismo duplirali kôd (sintaksu sa dve tačke za C++ naučite napamet). Klase dopunite kôdom iz primera. U glavnom delu programa možete da vidite kako smo skratili zapis kreiranja objekata pomoću konstruktora (stari kôd zakomentarišite). Oni se često koriste da bi u jednoj liniji kreirali objekat i dodelili vrednosti njegovim članovima: ----------------------- C++ ----------------------- //SKCitalac.h class SKCitalac { SKCitalac() SKCitalac(std::string ime, int brojGodina, std::string omiljenaRubrika) //... } //SKCitalac.cpp SKCitalac::SKCitalac() {}; SKCitalac::SKCitalac(std::string ime, int brojGodina, std::string omiljenaRubrika) { this->ime = ime; this->brojGodina = brojGodina; this->omiljenaRubrika = omiljenaRubrika; } //... //Avanturista.h class Avanturista : public SKCitalac{ Avanturista(); Avanturista(std::string ime, int brojGodina, std::string omiljenaRubrika, std::string omiljenaAvantura ); //... } //Avanturista.cpp Avanturista::Avanturista(){} Avanturista::Avanturista(std::string ime, int brojGodina, std::string omiljenaRubrika, std::string omiljenaAvantura) : SKCitalac(ime, brojGodina, omiljenaRubrika) { this->omiljenaAvantura = omiljenaAvantura; } //... //Main.cpp Avanturista* Pera = new Avanturista("Pera", 25, "Test Play", "The Dig"); //Avanturista* Pera = new Avanturista() //Pera->brojGodina = 25; //Pera->ime = "Pera"; //Pera->omiljenaRubrika = "Test Play"; //Pera->omiljenaAvantura = "The Dig"; //... ---------------------- Java ---------------------- //SKCitalac.java public class SKCitalac { SKCitalac() {}; SKCitalac(String ime, int brojGodina, String omiljenaRubrika) { this.ime = ime; this.brojGodina = brojGodina; this.omiljenaRubrika = omiljenaRubrika; } //... } //Avanturista.java public class Avanturista extends SKCitalac { Avanturista(){}; Avanturista(String ime, int brojGodina, String omiljenaRubrika, String omiljenaAvantura) { super(ime, brojGodina, omiljenaRubrika); this.omiljenaAvantura = omiljenaAvantura; } //... } //Primer.java Avanturista Pera = new Avanturista("Pera", 25, "Test Play", "The Dig"); //Avanturista Pera = new Avanturista(); //Pera.brojGodina = 25; //Pera.ime = "Pera"; //Pera.omiljenaRubrika = "Test Play"; //Pera.omiljenaAvantura = "The Dig"; //... Kao što vidite, argumente koje šaljete konstruktoru, pišete u zagradama posle imena klase i reči new. U konstruktoru ste iskoristili novu važnu reč this. Pošto smo članove klase i parametre konstruktora isto imenovali, na ovaj način pravite razliku između njih. Klasa može da ima i više od jednog konstruktora. Pošto oni moraju imati isto ime, razlikovaće se u broju ili tipu parametara koje prihvataju. Funkcije, makar one bile i konstruktori, koje se isto zovu a imaju različite parametre, nazivaju se overload-ovane funkcije. Naše klase, do sada, nisu imale konstruktor, pa se u tom slučaju, prilikom kreiranja objekta ostavljaju prazne zagrade. Ako vas zanima teorija, u slučaju da klasa nema konstruktor (tj. vi ga niste napisali), kompajler će sam napraviti podrazumevani konstruktor bez parametara (što je za vas transparentna akcija). U slučaju da ste u klasu dodali konstruktor sa parametrima, prestaje da važi pravilo automatskog kreiranja podrazumevanog konstruktora. U tom slučaju ga uvek sami dopišete, što smo mi i uradili, kao što ste videli u prethodnom primeru. Jezik C++ zahteva i postojanje destruktora (a onda i još nekih stvari, pravilo trojke), metode koja se automatski poziva kada objekat klase prestane da postoji. Pravilo je da se destruktor zove isto kao i konstruktor, ali sa tildom (~) ispred imena. Obično nema potrebe da ga pišete ako nemate pokazivače kao članove klase, već se automatski poziva podrazumevani destruktor kada je to potrebno. Java nema destruktore, jer automatski briše zauzetu memoriju. Getter/Setter, skrivanje podataka Već znate da članovima klase pristupate tako što napišete ime objekta, stavite tačku, pa napišete ime metode ili promenljive. Ovakav, direktan pristup, ne smatra se dobrom praksom OOP-a. Iz više razloga se preporučuje da promenljivu unutar klase menjate samo preko metode iz klase. Direktan pristup se zabranjuje tako što ispred člana napišete reč private. Ako sada probate da pristupite članu klase preko tačke, dobićete grešku. Dok pišete kôd, čak ni autocomplete u sugestijama neće prikazati privatne članove. Metode unutar klase uvek mogu da pristupe promenljivama klase. Zato napravite metodu koja je public i koja će, kad je pozovete, promeniti vrednost privatne promenljive. Takve metode se, po pravilu, zovu getter-i i setter-i, zavisno od toga da li vam služe da postavite novu vrednost promenljivoj, ili samo da je pročitate. Klase dopunite kôdom koji prilažemo, i, kao posledicu toga, izmenite glavni deo kôda: ----------------------- C++ ----------------------- //SKCitalac.h class SKCitalac { private: std::string ime; //... public: void setIme(std::string ime); std::string getIme(); //... } //SKCitalac.cpp void SKCitalac::setIme(std::string ime) { this->ime = ime; } std::string SKCitalac::getIme() { return ime; } //... //main.cpp Avanturista* Pera = new Avanturista(); Pera->ime = "Pera" //greska, private Pera->setIme("Pera"); //... ---------------------- Java ---------------------- //SKCitalac.java public class SKCitalac { private String ime; public String getIme() {return ime;} public void setIme(String ime) {this.ime = ime;} //... } //primer.java Avanturista Pera = new Avanturista(); Pera.ime = "Pera"; //greska, private Pera.setIme("Pera"); //... Pored najčešćih public i private atributa, postoji još nekoliko drugih. Na primer, ključna reč protected je slična komandi private, sa tim što private ograničava vidljivost članova, tako da ih ni izvedene klase ne mogu videti. Ovakvo skrivanje podataka unutar klase je jedna od ideologija OOP-a. Zamislite ogromnu klasu sa velikim brojem promenljivih i sa mnogo metoda. Mnoge od metoda su pomoćne, jer je to lep način da se podeli komplikovaniji kôd, umesto da se „nagura” u jednu metodu. Dobar programer bi zbog toga, te metode i promenljive označio da budu privatne. Kada vi napravite objekte te klase, nakon tačke ćete videti samo korisne metode koje vam je kreator klase ostavio kao javne. Korišćenje getera i setera, može se opravdati na više načina. Na primer, u telu seter funkcije možete da proverite da li je, na primer, uneti broj između jedan i deset, pa ako nije, da odbijete dodelu. Druga dobra osobina getera je ta što lako možete postaviti breakpoint prilikom debagovanja, što ćemo videti drugom prilikom. Zaključak, static, abstract, struct, interface, annotation itd. Ne bismo više trošili prostor na detaljne lisitinge, jer sada već imate dovoljno znanja da nastavite da učite OOP i C grupe jezika kroz neki od savremenih izvora o kojima smo već pisali. Nećemo prestajati da ukazujemo na neke važne pojmove, kako biste imali što manje neprijatniih iznenađenja na startu samostalnog učenja. Apstraktne klase služe da bi se od njih izvele obične klase. Na primer, našu klasu SKČitalac bismo mogli da proglasimo apstraktnom klasom, jer nam ona služi samo kao kalup da ne bismo ponavljali kôd. Zatim, želimo da u programu kreiramo konkretne igrače, avanturistu Peru, stratega Žiku itd, dok nam je bazna klasa suviše apstraktna, i ne želimo da dozvolimo kreiranje njenih objekata. Static je još jedna ključna reč kojom možete označiti člana klase, tako što napišete reč static ispred njegovog imena. Ukoliko ste označili metodu kao statičku, to znači da ne morate da pravite instancu te klase (tj. objekat) da biste tu metodu pozvali, već možete koristiti sintaksu: ImeKlase.ImeStatickeMetode(parametri). Sa druge strane, statička promenljiva je dobra ukoliko vam treba neka vrednost karakteristična za celu klasu, a ne vezana za konkretan objekat. Na primer, u klasi SKČitalac možete da uvedete statičku promenljivu koja govori koliko je ukupno čitalaca pročitalo poslednji broj. Kada Pera Avanturista kaže da je pročitao časopis i poveća broj te statičke promenljive za jedan, taj broj će se povećati i kôd Žike Stratega. Kada ste overrideovali metodu u Javi, poželjno je da napišete anotaciju „@overriden” iznad imena te metode u izvedenoj klasi. To olakšava posao i kompajleru, a i doprinosi čitkosti kôda. NetBeans sugeriše da to obavi umesto vas, ako već niste sami. Takođe ste na početku tela metode napisali super(), što je omogućilo izvršavanje kôda roditeljske funkcije. U C++-u morate umesto super() da pišete pun naziv roditeljske klase i metode. Razlog tome je što klasa u C++-u može da ima više roditeljskih klasa (višestruko nasleđivanje), pa kompajler ne zna na koju bi se od tih klasa super() odnosilo. Što se klasa tiče, u C++-u postoje Strukture, koje su im veoma slične i više su relikt običnog C jezika, tako da ne morate mnogo da brinete o njima, osim ako ne želite da učite taj jezik. Mnogo važnije je da pogledate šta su to Interfejsi u Javi (u Swiftu se, na primer, zovu Protokoli). Interfejsi izgledaju skoro isto kao klase. Oni se definišu zasebno, ali se mogu dodati klasi u Javi. Tada klasa, pored roditeljske klase, dobijene preko nasleđivanja, može da se pokorava i jednom ili većem broju interfejsa. To znači da u klasi morate obavezno da napravite sve članove koji postoje u interfejsu. Metode u interfejsu ne moraju da imaju telo (pre Jave verzije 8 nisu ni smele), već svaka klasa koja se pokorava interfejsu, može drugačije da ih implementira na svoj način. Interfejsi su pogodni za klase koje su u različitim hijerarhijama nasleđivanja, a dele slične osobine. Sledi mnogo učenja ako želite da postanete dobar OOP programer. Na primer, nije neobično da su promenljive unutar klase objekti neke druge klase. Problem je što vas ovaj način programiranja ne sprečava da pišete loš kôd i često se dešava da neiskusni programeri naprave papazjaniju klasa koje su nefleksibilne. Ipak, sada možete reći da poznajete četiri pravila OOP-a, a to su apstrakcija, enkapsulacija (plus skrivanje podataka), nasleđivanje i polimorfizam. Možete naleteti i na drugačije podele, a tačne definicije ostavljamo da sami istražite. |