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.