1. diel - Úvod do objektovo orientovaného programovania v Jave
Vitajte pri prvej lekcii kurzu o objektovo orientovanom programovaní v Jave. Kurz Základná konštrukcia jazyka Java už máme za sebou, minule sme ho dokončili dielom Matematické funkcie v Jave a knižnica Math. V tomto kurze sa naučíme objektovo programovať a hlavne objektovo myslieť. Je to niečo trochu iné, než sme robili doteraz a samotný program už nebudeme chápať ako niekoľko riadkov príkazov, ktoré interpreter vykonáva zhora nadol.
Objektovo orientované programovanie (ďalej len OOP) nevzniklo náhodou, ale je dôsledkom vývoja, ktorý k nemu smeroval. Ide o modernú metodiku vývoja softwaru, ktorú podporuje väčšina programovacích jazykov. Častou chybou je, že sa ľudia domnievajú, že OOP sa využíva iba pri písaní určitého druhu programov a inak je na škodu. Opak je pravdou - OOP je filozofia, je to nový pohľad na funkciu programu a komunikáciu medzi jeho jednotlivými časťami. Malo by sa používať vždy, či už robíme malú utilitku alebo zložitý databázový systém. OOP nie je len technika alebo nejaká odporúčaná štruktúra programu, je to hlavne nový spôsob myslenia, nový náhľad na problémy a nová éra vo vývoji softwaru.
Najprv sa pozrieme do histórie, ako sa programovalo skôr a ktoré konkrétne problémy OOP rieši. Je dôležité, aby sme pochopili, prečo OOP vzniklo.
Evolúcia metodík
Medzi tým, ako sa programovalo pred 40 rokmi a ako sa programuje dnes, je veľký rozdiel. Prvé počítače neoplývali veľkým výkonom a aj ich software nebol nijako zložitý. Vývoj hardwaru je však natoľko rýchly, že sa počet tranzistorov v mikroprocesoroch každý rok zdvojnásobí (Moorov zákon). Bohužiaľ, ľudia sa nedokážu rozvíjať tak rýchlo, ako sa rozvíja hardware. Stále rýchlejšie počítače vyžadujú stále zložitejší a zložitejší software (resp. ľudia toho chcú po počítačoch stále viac a viac). Keď sa v jednej chvíli zistilo, že okolo 90 % softwaru je vytvoreného s oneskorením, s dodatočnými nákladmi alebo zlyhalo úplne, hľadali sa nové cesty, ako programy písať. Vystriedalo sa tak niekoľko prístupov, presnejšie sa im hovorí paradigmy (rozumejte ako smer myslenia). My si ich tu popíšeme.
1. Strojový kód
Program bol len súborom inštrukcií, kde sme nemali žiadnu možnosť pomenovávať premenné alebo zadávať matematické výrazy. Zdrojový kód bol samozrejme špecifický pre daný hardware (procesor). Táto paradigma bola čoskoro nahradená.
2. Neštruktúrované paradigmy
Neštruktúrovaný prístup je podobný assemblerom, jedná sa o súbor
inštrukcií, ktorý sa vykonáva zhora nadol. Zdrojový kód už nebol
závislý od hardwaru a bol lepšie čitateľný pre človeka, prístup na
nejaký čas umožnil vytvárať komplexnejšie programy. Bolo tu však stále
veľa úskalí: Jediná možnosť, ako urobiť niečo viackrát alebo ako sa v
kóde vetviť, bol príkaz GOTO
. Príkaz GOTO
nám
umožňoval "skákať" na jednotlivé miesta v programe. Miesta boli predtým
špecifikované číslom riadku zdrojového kódu, čo je samozrejme
nepraktické. Keď do kódu vložíme nový riadok, čísla prestanú súhlasiť
a celý kód je rozbitý. Neskôr vznikla možnosť definovať tzv.
„návestia“. Takto sa obchádzala napr. absencia cyklov. Takýto spôsob
písania programov je samozrejme veľmi neprehľadný a čoskoro prestal
postačovať na vývoj zložitejších programov.
Uvedomme si, že obrovské rozšírenie počítačov za posledných niekoľko dekád má na svedomí rast dopytu po softwari a logicky aj rast dopytu po programátoroch. Iste existujú ľudia, ktorí dokážu bez chyby písať programy v ASM alebo iných nízkych jazykoch, ale koľko ich je? A koľko si asi za takú nadľudskú prácu účtujú? Je potrebné písať programy tak, aby aj menej skúsení programátori dokázali písať kvalitné programy a nepotrebovali na tvorbu jednoduchej utility 5 rokov praxe.
3. Štruktúrované programovanie
Štruktúrované programovanie je prvou paradigmou, ktorá sa udržala dlhšiu dobu a naozaj chvíľu postačovala na vývoj nových programov. Programujeme pomocou cyklov a vetvenia. To je v podstate to, kam sme sa doteraz dostali.
Program je možné rozložiť do funkcií (metód), tomu sme sa nevenovali, pretože to v Jave (ktorá je objektová) ani veľmi dobre nejde a radšej som dal prednosť tento medzikrok preskočiť a začať rovno s OOP. Pri štruktúrovanom programovaní hovoríme o tzv. funkcionálnej dekompozícii. Problém sa rozloží na niekoľko problémov a každý problém potom rieši určitá funkcia s parametrami. Nevýhodou je, že funkcia vie len jednu činnosť, keď chceme niečo iné, musíme napísať novú. Neexistuje totiž spôsob, ako vziať starý kód a len trochu ho modifikovať, musíme písať znova a znova – vznikajú zbytočné náklady a chyby. Túto nevýhodu je možné čiastočne obísť pomocou parametrizácie funkcií (počet parametrov potom ale rýchlo narastá) alebo použitím globálnych premenných. S globálnymi dátami vzniká však nové nebezpečenstvo – funkcie majú prístup k dátam ostatných. To je začiatok konca, nikdy totiž neustrážime, aby niekde nedošlo k prepísaniu globálnych dát medzi funkciami a začne dochádzať k nekontrolovateľným problémom. Celý program sa potom skladá z nezapuzdrených blokov kódu a zle sa udržuje. Každá úprava programu zvyšuje zložitosť, program potom nutne dôjde do stavu, keď náklady na pridávanie nových funkcií vzrastú na toľko, že sa program už neoplatí rozširovať. Zástupcami tohto prístupu sú napríklad jazyky C a Pascal.
Medzi štruktúrovaným programovaním a objektovo orientovaným programovaním existoval ešte medzičlánok, tzv. modulárne programovanie, ktoré nám umožňuje zapuzdriť určitú funkcionalitu do modulov. Stále však neexistuje spôsob, ako už napísaný kód modifikovať a znova využiť.
Ako som už spomenul na začiatku článku, niekedy sa uvádza, že sa jednoduché programy majú písať neobjektovo, teda štruktúrovane. Nie je to však pravda. Keď zabudneme na fakt, že porušujeme filozofiu OOP ako takú, nikdy nemôžeme vedieť, či sa tento program neuchytí a z malej utilitky sa nestane niečo vážnejšie. Potom opäť nutne dospejeme do stavu, kedy program nebude možné ďalej rozširovať a budeme ho buď musieť zahodiť alebo celý prepísať s pomocou OOP.
Neobjektové metódy písania kódu sa prezývajú spaghetti code pre ich neprehľadnosť (pretože špagety sú zamotané).
Objektovo orientovaný prístup
Jedná sa o filozofiu a spôsob myslenia, dizajnu a implementácie, kde kladieme dôraz na znovupoužiteľnosť. Prístup nachádza inšpiráciu v priemyselnej revolúcii - vynález základných komponentov, ktoré budeme ďalej využívať (napr. keď staviame dom, nebudeme si páliť tehly a vyrábať skrutky, jednoducho ich už máme hotové).
Poskladanie programu z komponentov je výhodnejšie a lacnejšie. Môžeme mu veriť, je otestovaný (o komponentoch sa vie, že fungujú, sú otestované a udržiavané). Ak je niekde chyba, stačí ju opraviť na jednom mieste. Sme motivovaní k písaniu kódu prehľadne a dobre, pretože ho po nás používajú druhí alebo my sami v ďalších projektoch (priznajme si, že človek je od prírody lenivý a keby nevedel, že sa jeho kód bude znovu využívať, nesnažil by sa ho písať kvalitne 🙂 ).
Znalosti, ktoré sme sa doteraz naučili, samozrejme budeme používať ďalej. Náš kód budeme iba inak štruktúrovať a to do komunikujúcich objektov.
Ako OOP funguje
Snažíme sa nasimulovať realitu tak, ako sme zvyknutí ju vnímať. Môžeme teda povedať, že sa odpútavame od toho, ako program vidí počítač (stroj) a píšeme program skôr z pohľadu programátora (človeka). Ako sme vtedy nahradili assembler ľudsky čitateľnými matematickými zápismi, teraz ideme ešte ďalej a nahradíme aj tie. Ide teda o určitú úroveň abstrakcie nad programom. To má značné výhody už iba v tom, že je to pre nás prirodzenejšie a prehľadnejšie.
Základnou jednotkou je objekt, ktorý zodpovedá nejakému objektu z reálneho sveta (napr. objekt human alebo database).
Objekt má svoje atribúty a metódy.
Atribúty
Atribúty objektu sú vlastnosti alebo dáta, ktoré uchováva (napr. u človeka meno a vek, u databázy heslo). Ide o jednoduché premenné, s ktorými sme už stokrát pracovali. Niekedy o nich hovoríme ako o vnútornom stave objektu.
Metódy
Metódy sú schopnosťami, ktoré vie objekt vykonávať. U
človeka by to mohli byť metódy: goToWork()
, greet()
alebo blink()
. Pri databáze addEntry()
alebo
search()
). Metódy môžu mať parametre a môžu tiež vracať
nejakú hodnotu. Veľmi dobre ich poznáme, používali sme napr. metódu
split()
na objekte String
. String
je
vlastne objekt, ktorý reprezentuje v realite nejaký text. Vidíte, že si
môžeme jednoducho predstaviť, že jednáme s reťazcom textu, niečo mu
prikazujeme alebo na ňom niečo nastavujeme. Obsahuje metódy, ktoré vie
reťazec vykonávať (kopírovanie, mazanie, splitovanie...). Pole má
napríklad atribút length
, ktorý značí jeho dĺžku.
V starších jazykoch metódy nepatrili objektom, ale voľne sa nachádzali v
moduloch (jednotkách). Namiesto metódy text.split()
by sme teda
postarom písali split(text)
. Nevýhodou samozrejme bolo najmä to,
že metóda split()
tu nikam nepatrí. Nebol spôsob, akým si
vyvolať zoznam toho, čo sa s reťazcom dá robiť a v kóde bol neporiadok.
Navyše sme nemohli mať 2 metódy s rovnakým názvom, v OOP môžeme mať
user.remove()
a article.remove()
. To je veľmi
prehľadné a jednoduché, v štruktúrovanom programe by sme museli písať:
remove_user(user)
a remove_article(article)
. Takýchto
hlúpych metód by sme museli mať niekde rozhádzaných tisíce. Ak vám to
pripomína jazyk PHP, máte bohužiaľ pravdu. PHP je v tomto
naozaj hrozné a to z toho dôvodu, že jeho návrh je starý. Síce sa časom
plne preorientovalo na objekty, ale jeho základy sa už nezmenia. Java je
našťastie jazyk moderný a je silne postavená na objektoch.
V tomto článku si vysvetlíme len úplné základy, teda ako objekty vytvárať a ako zapuzdriť ich vnútornú logiku. Ďalším funkciám OOP (hovorím najmä o dedičnosti) budú venované ďalšie lekcie, aby toho nebolo naraz veľa 🙂
Trieda
S pojmom trieda sme sa už tiež stretli, chápali sme ju ako súbor príkazov. Trieda však umožňuje oveľa viac. Trieda je vzor, podľa ktorého sa objekty vytvárajú. Definuje ich vlastnosti a schopnosti.
Objekt, ktorý sa vytvorí podľa triedy, sa nazýva
inštancia. Inštancie majú rovnaké
rozhranie ako trieda, podľa ktorej sa vytvára, ale navzájom
sa líšia svojimi dátami (atribútmi). Majme napríklad triedu
Human
a od nej si vytvorme inštancie carl
a
jack
. Obe inštancie majú určite tie isté metódy a atribúty,
ako trieda (napr. name
a age
) a metódy
(goToWork()
a greet()
), ale hodnoty v nich sa líšia
(prvá inštancia má v atribúte name
hodnotu Carl
a
v atribúte age
hodnotu 22
, druhá Jack
a
45
).
Komunikácia medzi objektmi prebieha pomocou odovzdávania správ, vďaka
čomu je syntax prehľadná. Správa zvyčajne vyzerá takto:
recipient.methodName(parameters)
. Napr.
carl.greet(neighbor)
by mohol spôsobiť, že inštancia
carl
pozdraví inštanciu neighbor
.
OOP stojí na troch základných pilieroch:
- Zapuzdrenie
- Dedičnosť
- Polymorfizmus.
Vysvetlime si prvý z nich.
Zapuzdrenie
Zapuzdrenie umožňuje skryť niektoré metódy a atribúty tak, aby zostali použiteľné len pre triedu zvnútra. Objekt si môžeme predstaviť ako čiernu skrinku (anglicky blackbox), ktorá má určité rozhranie (interface), cez ktoré jej odovzdávame inštrukcie/dáta a ona ich spracováva.
Nevieme, ako to vo vnútri funguje, ale vieme, ako sa navonok správa a používa. Nemôžeme teda spôsobiť nejakú chybu, pretože využívame a vidíme len to, čo tvorca triedy sprístupnil.
Príkladom môže byť trieda Human
, ktorá bude mať atribút
birthDate
a na jeho základe ďalšie atribúty ako
fullAged
a age
. Keby niekto objektu zvonku zmenil
birthDate
, prestali by platiť premenné fullAged
a
age
. Hovoríme, že vnútorný stav objektu by bol nekonzistentný.
Toto sa nám v štruktúrovanom programovaní môže pokojne stať. V OOP však
objekt zapuzdríme a atribút birthDate
označíme ako privátny,
zvonku teda nebude viditeľný. Naopak von vystavíme metódu
changeBirthDate()
, ktorá dosadí nový dátum narodenia do
premennej birthDate
a zároveň vykoná potrebný prepočet veku a
prehodnotenie plnoletosti v atribúte fullAged
. Použitie objektu
je bezpečné a aplikácia je stabilná.
Zapuzdrenie teda donúti programátorov používať objekt len tým správnym
spôsobom. Rozhranie (interface) rozdelí triedy na verejne prístupné
(public
) a vnútornú štruktúru
(private
).
V nasledujúcej lekcii, Prvá objektová aplikácia v Jave - Hello object world, si vytvoríme prvý objektový program.