1. diel - Úvod do objektovo orientovaného programovania v C#
Vitajte v prvej lekcii úvodu do objektovo orientovaného programovania v C#. Kurz Základné konštrukcie jazyka C# už máme za sebou. V tomto kurze sa naučíme objektovo programovať a hlavne objektovo myslieť. Je to niečo trochu iné, než sme robili doteraz. 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 OOP smeroval. Jedná sa o modernú metodiku vývoja softvéru, ktorú podporuje väčšina programovacích jazykov. Ľudia sa často chybne 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. OOP by sa malo používať vždy – či už robíme malú utilitu, alebo zložitý databázový systém. OOP nie je len technikou alebo nejakou odporúčanou štruktúrou programu, je to hlavne nový spôsob myslenia, nový pohľad na problémy a nová éra vo vývoji softvéru.
Najprv sa pozrieme do histórie, ako sa programovalo predtým a ktoré konkrétne problémy OOP rieši. Je to totiž dôležité na pochopenie toho, 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 ani ich softvér nebol nijak zložitý. Vývoj hardvéru je však natoľko rýchly, že sa počet tranzistorov v mikroprocesoroch každý rok zdvojnásobí (Moorov zákon). Ľudia sa bohužiaľ nedokážu rozvíjať tak rýchlo, ako sa rozvíja hardvér. Stále rýchlejšie počítače vyžadujú stále zložitejší a zložitejší softvér (resp. ľudia toho chcú od počítačov stále viac a viac). Keď sa v jednej chvíli zistilo, že okolo 90 % softvéru je vytvorené s oneskorením, s dodatočnými nákladmi alebo vývoj zlyháva úplne, hľadali sa nové cesty, ako programy písať. Vystriedalo sa tak niekoľko prístupov, presnejšie povedané paradigmat (rozumej smery 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ý hardvér (procesor). Toto paradigma bolo čoskoro nahradené.
2. Nestruktúrované paradigma
Nestruktú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ý na hardvéri a bol lepšie čitateľný pre človeka. Tento prístup
na nejakú dobu umožnil vytvárať komplexnejšie programy. Bolo tu však
stále mnoho úskalí: jedinou možnosťou, ako urobiť niečo viackrát alebo
ako sa v kóde vetviť, bol príkaz GOTO
. GOTO
nám
umožňovalo "skákať" na jednotlivé miesta v programe. Miesta bola kedysi
š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ávästi".
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ť pre vývoj
zložitejších programov.
Uvedomme si, že obrovské rozšírenie počítačov za posledných niekoľko desaťročí má za následok rast dopytu po softvéri a logicky tiež rast dopytu po programátoroch. Určite 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ú? Programy je potrebné písať 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 prvé paradigma, ktoré sa udržalo dlhšie a naozaj chvíľu pre vývoj nových programov postačovalo. 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), čomu sme sa nevenovali, pretože to v jazyku C# (ktorý je objektový) ani dobre nejde. Radšej sme tento medzikrok preskočili a začneme rovno s OOP. Pri štruktúrovanom programovaní hovoríme o tzv. funkcionálnej dekompozícii. Problém sa rozloží na niekoľko podproblémov a každý podproblém potom rieši určitá funkcia s parametrami. Nevýhodou je, že funkcia vie len jednu činnosť, a keď chceme niečo iné, musíme napísať funkciu novú. Neexistuje totiž spôsob, ako vziať starý kód a len trochu ho modifikovať. Musíme písať znovu a znovu, a tak 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 však vzniká nové nebezpečenstvo – funkcie majú prístup k dátam ostatných. To je začiatok konca. Nikdy totiž neuhlídame, 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 dospeje do stavu, keď náklady na pridávanie nových funkcií vzrastú natoľko, že sa program už nevyplatí rozširovať. Zástupcovia 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. To 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 znovu využiť.
Ako sme už spomenuli na začiatku článku, niekedy sa uvádza, že jednoduché programy sa majú písať neobjektovo, teda štruktúrovane. Nie je to však pravda. Keď opomenieme fakt, že porušujeme filozofiu OOP ako takú, nikdy nemôžeme vedieť, či sa daný program neuchytí a z malej utility sa nestane niečo vážnejšie. Potom opäť nutne dospejeme do stavu, keď program nebude možné ďalej rozširovať a budeme ho musieť buď zahodiť, alebo celý prepísať pomocou OOP.
Neobjektovým metódam písania kódu sa pre ich neprehľadnosť prezýva "spaghetti code" (pretože špagety sú zamotané).
Objektovo orientovaný prístup
Jedná sa o filozofiu a spôsob myslenia, dizajnu a implementácie, kedy 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 sústružiť skrutky, proste ich už máme hotové).
Program poskladaný z komponentov je výhodnejší a lacnejší. Taktiež mu môžeme 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 vedel, že sa jeho kód nebude už 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). Podobne ako sme kedysi 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. Takéto programovanie má značné výhody už len v tom, že je pre nás prirodzenejšie a prehľadnejšie.
Základnou jednotkou OOP 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 sú vlastnosti alebo dáta, ktoré objekt
uchováva (napr. u človeka to je name
a age
, u
databázy password
). Jedná sa o prosté premenné, s ktorými sme
už stokrát pracovali. Niekedy o nich hovoríme ako o vnútornom stave
objektu.
Slovo vlastnosť si v Microsofte vyhradili pre špecifické využitie a to isté platí o atribúte. Premenným objektov hovoria field, čo sa zase kryje so slovenským slovom pole. Budeme teda používať slovo atribút, aj keď to nebude úplne korektné.
Metódy
Metódy sú schopnosti, ktoré vie objekt vykonávať. U
človeka by to mohli byť metódy: GoToWork()
, Greet()
alebo Blink()
. U databázy AddEntry()
alebo
Search()
. Metódy môžu mať parametre a môžu tiež vracať
nejakú hodnotu. Veľmi dobre ich už poznáme. Používali sme napr. metódu
Split()
na objekte string
. Aj string
je
vlastne objekt, ktorý reprezentuje v realite nejaký text. Vidíme, ž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é reťazec
vie vykonávať (kopírovanie, mazanie, splitovanie...), a má tiež nejaké
atribúty, napr. Length
, ktorý značí jeho dĺžku:
V starších jazykoch metódy nepatrili objektom, ale voľne sa nachádzali v
moduloch (jednotkách). Miesto text.Split()
by sme teda postaru
písali Split(text)
. Nevýhodou samozrejme bolo najmä to, že
metóda Split()
vtedy nikam nepatrila. Neexistoval spôsob, ako si
vyvolať zoznam toho, čo sa s reťazcom dá robiť, a v kóde bol preto
neporiadok. Navyše sme nemohli mať dve 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 však
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, bohužiaľ máte pravdu. PHP je v tomto naozaj hrozné, a to
z toho dôvodu, že jeho návrh je starý. PHP sa síce časom plne
preorientovalo na objekty, ale jeho základy sa už nezmenia. Jazyk C# je
našťastie moderný a celý .NET 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 (najmä dedičnosti) budú venované ďalšie lekcie, aby toho nebolo naraz príliš
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árajú, 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é atribúty (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
, v druhom prípade sú hodnoty
"Jack"
a 45
:
Komunikácia medzi objektmi prebieha pomocou odovzdávania správ, vďaka
čomu je syntax prehľadná. Správa obvykle vyzerá takto:
recipient.MethodName(parameters)
. Napr. správa
carl.Greet(neighbor)
by mohla 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 vnútri funguje, ale vieme, ako sa to navonok chová 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: adult
a age
. Keby niekto objektu zvonka atribút birthDate
zmenil, prestali by platiť premenné adult
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, zvonka
teda nebude viditeľný. Naopak von vystavíme metódu
ChangeBirthDate()
, ktorá dosadí nové dátum narodenia do
premennej birthDate
. Metóda zároveň vykoná potrebný prepočet
veku a prehodnotenie plnoletosti. Použitie objektu je bezpečné a aplikácia
zostáva stabilná.
Zapuzdrenie teda donúti programátorov používať objekt len tým správnym
spôsobom. Rozhranie (interface) triedy rozdelí na verejne prístupné
(public
) a vnútornú štruktúru
(private
).
V nasledujúcej lekcii, Prvá objektová aplikácia v C# - Hello object world, si vytvoríme svoj prvý objektový program.