V článku o základech OOP jsem se věnoval tomu nejdůležitějšímu, co je třeba umět k psaní objektového kódu. Nyní se zaměřím na pokročilé nástroje a techniky, kterými OOP vybrušuje krásu své koncepce a eliminuje některé své neduhy. Po osvojení těchto věcí získáte především nový pohled na řešení složitějších problémů a nemělo by pro vás být těžké osvojit si jakýkoliv konkrétní objektově orientovaný jazyk.
Na úvod začneme něčím, co by s klidem mohlo být zahrnuto ještě v základech OOP. Modifikátory přístupu jsou v podstatě klíčová slova umožňující do velké míry manipulovat se zapouzdřením a říkat kompilátoru, kdo a kdy má k jakým vlastnostem a metodám daného objektu přístup. Různé jazyky mají různé modifikátory přístupu, které se mohou mírně lišit sémantikou. Nejčastěji jsou to však tyto tři:
Kromě modifikátorů přístupu se v jazycích typicky setkáme s celou řadou dalších klíčových slov deklarujících různá pravidla, co s čím lze a nelze dělat. Např. u tříd máme často možnost uvést, že je ukončená (final) a nelze od ní dále dědit. Chci říct, že OOP není žádný přísný standard a záleží na jazyku, jak si jej uzpůsobí. Já se zde snažím probrat obecné věci, s nimiž se setkáme ve většině jazyků, a modifikátory přístupu si mezi tyto záležitosti dovolím klasifikovat, neboť jsou velmi frekventovaně využívané. Při použití konkrétního jazyka si nejdřív nastudujte, jak přesně v něm modifikátory přístupu fungují! Nyní uvedu jednoduchý příklad, jak modifikátory používat (můžete si stáhnout zdrojový kód v C++):
třída Geometrický tvar
{
atributy:
název, chráněný atribut // pro tuto třídu a potomky
poznámka, veřejný atribut // zvenku lze přistupovat
id číslo, soukromý atribut // pouze pro tuto třídu
metody:
změň id(číslo) // bez modifikátoru = veřejná
zjisti id()
změň název(nový název)
zjisti název()
}
třída Úsečka, potomek třídy Geometrický tvar
{
atributy:
první bod, chráněný atribut // zvenku nelze přistupovat
druhý bod, chráněný atribut
metody:
nastav body(a,b) // bez modifikátoru = veřejná
vzdálenost bodů(a,b), soukromá metoda // pomocná, soukromá
délka(), veřejná metoda // využívá metodu vzdálenost bodů
metoda s chybou()
}
// definice metod:
Geometrický tvar:změň id(číslo)
{
id číslo := číslo
}
Geometrický tvar:zjisti id()
{
vrať id číslo
}
Geometrický tvar:změň název(nový název)
{
název := nový název
}
Geometrický tvar:zjisti název()
{
vrať název
}
Úsečka:nastav body(a,b)
{
první bod := a
druhý bod := b
}
Úsečka:vzdálenost bodů(a,b)
{
vrať odmocnina ((a.x – b.x) \^2 – (a.y – b.y) \^2)
}
Úsečka:délka()
{
vrať vzdálenost bodů(první bod,druhý bod)
}
Úsečka:metoda s chybou()
{
první bod := bod(1,2) // OK
poznámka := "..." // OK
název := "..." // OK
změň id(12345) // OK
id číslo := 12345 // CHYBA! atribut je soukromý pro G. tvar
}
// zde se začne vykonávat program
Geometrický tvar tvar
Úsečka moje úsečka
tvar := nový Geometrický tvar
úsečka := nový Úsečka
tvar.změň id(123) // OK
vypiš(tvar.zjisti id()) // OK
moje úsečka.nastav body(bod(1,2),bod(5,0)) // OK
vypiš(moje úsečka.délka()) // OK
tvar.poznámka := "obecný tvar" // OK
moje úsečka.poznámka = "nějaká úsečka" // OK
moje úsečka.změň id(12345) // OK
vypiš(moje úsečka.zjisti id()) // OK
tvar.id číslo := 456 // CHYBA! přístup k soukromému atributu
tvar.název := "můj tvar" // CHYBA! přístup k chráněnému atributu
moje úsečka.první bod := bod (1,2) // CHYBA! chráněný atribut
vypiš(Úsečka.vzdálenost bodů(bod(1,2),bod(3,4))) // CHYBA!
moje úsečka.id číslo := 321 // CHYBA! přístup k soukromému atributu
Dodejme, že chyby se neprojeví až za běhu programu, nýbrž vždycky při překladu. Modifikátory přístupu používáme k tomu, aby potenciálně špatně napsaný program nešel přeložit, čímž se vyhneme sémantickým chybám, které, jak víme, jsou nejvíce nežádoucím druhem chyb.
Doposud jsme pojem třídy chápali čistě jen jako šablonu pro objekty. Metody a atributy, ač tak možná nebylo přímo řečeno, náležely vždy objektům - všechny objekty stejné třídy měly stejné atributy co do jména a datového typu, ale každý měl vždy své vlastní hodnoty těchto atributů. Obdobně nebylo možné volat metody jenom tak, ale musel se vždy uvést objekt, nad nímž se volání provádělo.
Takový pohled je správný, avšak nemusí být úplný. Existuje možnost, jak metody a atributy dát do vlastnictví třídě samotné. Uděláme to označením dané metody či atributu klíčovým slovem, většinou static. Typickým příkladem je v mnoha jazycích matematická třída, která má metody jako např. sinus, kosinus, odmocnina či atributy, mezi něž může patřit např. konstanta pí.
Kdy použít statické metody a atributy? Zkrátka tehdy, pokud nedává smysl, aby bylo potřeba vytvářet instanci pro volání metody nebo tehdy, když má atribut uchovávat nějakou globální informaci pro všechny objekty. Zkusme si uvést malý příklad:
třída Soubor
{
atributy:
cesta
je otevřen
počet otevřených := 0, statický atribut // náleží třídě
metody:
otevři(cesta)
zavři()
vypiš počet otevřených(), statická metoda // patří třídě
}
Soubor:vypiš počet otevřených()
{
vypiš("otevřených souborů: " + počet otevřených)
}
Soubor:otevři(cesta)
{
počet otevřených := počet otevřených + 1
\\\\ nějaký kód k otevření souboru
}
Soubor:zavři()
{
\\\\ nějaký kód k zavření souboru
počet otevřených := počet otevřených - 1
}
// zde se začne vykonávat program
Soubor můj soubor // odkaz na objekt typu Soubor
Soubor.vypiš počet otevřených() // voláme metodu třídy
můj soubor := nový Soubor() // vytvoříme nový objekt
vypiš("otevírám soubor...")
můj soubor.otevři("soubor.txt") // voláme metodu objektu
Soubor.vypiš počet otevřených()
vypiš("zavírám soubor...")
můj soubor.zavři()
Soubor.vypiš počet otevřených()
Vidíme, že statické metody se volají pomocí jména třídy a nikoliv objektu, jak by mělo být zřejmé. V příkladu máme jednu statickou proměnnou uchovávající informaci o počtu celkově otevřených souborů a jednu metodu, která vypisuje hodnotu tohoto atributu. Výstup programu bude následující:
otevřených souborů: 0 otevírám soubor... otevřených souborů: 1 zavírám soubor... otevřených souborů: 0
Mechanismus výjimek (exceptions) je nástrojem k efektivnějšímu ošetřování chybových stavů. Tento mechanismus sice není při programování nezbytný a nemusí se nutně vyskytovat spolu s OOP, ale je to velmi osvědčený a rozšířený mechanismus, který se s OOP velmi dobře kombinuje. Není proto divu, že jej podporuje drtivá většina významnějších objektově orientovaných jazyků.
Výjimkou rozumíme stav způsobující chybu při běhu aplikace, neboli stav, který by za správných okolností neměl nastat (tzn. jeho výskyt je výjimkou). Jako takovou situaci si představme třeba dělení nulou, přístup za hranici pole, neplatný vstup, převod řetězce "abc" na číslo apod. Výjimku tedy způsobí nějaký konkrétní příkaz a když k ní dojde, je spuštěn mechanismus zpracování výjimky, který si popíšeme dále. Pojďme se podívat, jak bychom ošetřili např. potenciální chybu dělení nulou klasickým způsobem bez výjimek:
načti x ze vstupu
načti y ze vstupu
pokud y != 0
{
z = x / y // spousta kódu okolo
}
jinak
{
vypiš ("chyba: neumím dělit nulou.")
ukonči program
}
vypiš(z)
Tento způsob není špatný, ale jeho nevýhodou je kód navíc okolo příkazu dělení a prolínání kódu výkonného algoritmu s kódem ošetřujícím chyby. Jistě si dokážete představit, že ve složitějším kódu může tento přístup vést k nepřehlednosti.
Nyní se podívejme, jak stejnou situaci ošetříme pomocí mechanismu výjimek. Nejdříve vytvoříme tzv. zkušební blok (často se nazývá try) a do něj zapíšeme čistý algoritmus bez ohledu na možné chybové stavy. Za tímto blokem následují bloky zachytávající výjimky (v praxi nazývané catch). Nakonec můžeme a nemusíme uvést ještě speciální blok, který se provede vždy nakonec, ať už k nějaké výjimce došlo nebo ne (tzv. finally blok). V našem případě máme jeden odchytávací blok. V případě dělení nulou se přeruší vykonávání algoritmu na daném příkazu ve zkušebním bloku a přesune se do bloku odpovídajícímu vyvolané výjimce. Kód vypadá následovně:
zkušební blok // zde provedeme výkonný algoritmus
{
načti x ze vstupu
načti y ze vstupu
z = y / x
vypiš(z)
}
odchyť výjimku (dělení nulou) // zde ošetříme případnou výjimku
{
vypiš(„chyba: neumím dělit nulou.“)
}
// mohly by následovat další odchytávací bloky nebo finally blok
Tento případ je evidentně přehlednější, není proto důvod jej nepoužívat. Pojďme se ale teď podívat, proč jsou výjimky tak hezky použitelné právě spolu s OOP.
Odpověď je prozaická – výjimky totiž mohou být objekty. Co je na tomto faktu tak úžasného? Především to, že existuje spousta druhů výjimek a je jasné, že každá se ošetřuje potenciálně jiným způsobem – chyba při čtení souboru se ošetří chybovým hlášením a zavřením souboru, kdežto chyba při převodu řetězce na číslo se ošetří třeba výzvou k novému zadání řetězce. Výjimek je ve skutečnosti nekonečně mnoho, protože si dokonce ve svém programu můžeme dle potřeby vymýšlet výjimky vlastní - např. při psaní ovladače nějakého hardwarového zařízení si můžeme vymyslet výjimku typu chyba při připojování zařízení. Výjimky jsou tedy objekty různých tříd a mohou mít své atributy využitelné při jejich ošetřování (např. čas výskytu výjimky, místo v paměti, kde k výjimce došlo, slovní popis apod.). Jazyky často implicitně obsahují obecnou třídu Výjimka, od níž dědí konkrétnější a konkrétnější výjimky. V odchytávacích blocích potom vždy uvádíme, jakou třídu výjimky odchytáváme a případná výjimka je nám do odchytávacího bloku předána jako objekt, s nímž můžeme pracovat např. následovně:
zkušební blok
{
soubor := nový Soubor("soubor.txt")
soubor.vypiš obsah()
soubor.zavři soubor()
}
odchyť výjimku (chyba otvírání souboru: chyba)
{
vypiš("chyba při otevírání souboru " + chyba.cesta() + "!")
}
Pokud je tedy výjimek velmi mnoho, nabízí se otázka, zda musíme vždy všechny odchytávat. Z hlediska množství kódu je asi jasné, že to po nás jazyky nevyžadují, ale některé výjimky mohou být speciálně označené tak, že odchyceny být musí. Tím však vyvstává otázka další a to co se stane, pokud nastane nějaká výjimka a my ji neodchytíme? Představme si metodu, v níž provádíme dělení a neodchytáváme výjimku dělení nulou. Pokud tato výjimka nastane, přeruší se vykonávání metody a jakmile se zjistí, že neexistuje blok, jenž by ji ošetřil, ukončí se naše metoda a sama jakožto příkaz volaný odjinud tuto výjimku vyvolá. Takto se předává zpracování výjimky stále "výš", dokud se pro výjimku nenajde vhodný ošetřovací blok. Pokud se to nestane nikdy, je zde implicitní funkce zpracování výjimek, která jako poslední záchrana odchytává obecné výjimky a ošetří je většinou zprávou uživateli o chybě neošetřené výjimky (jistě jste se s tím setkali u nějaké ne příliš dobře napsané Javové aplikace).
Snad je už trochu jasné, jak to celé funguje. Naše metody mohou vyvolávat výjimky podle toho, jaké výjimky mohou nastat v nich (pokud je tam neošetříme) a nebo tehdy, pokud výjimku uvnitř metody vyvoláme sami příkazem daného jazyka (podle konvence většinou nazýván throw). Výjimku můžeme vyvolat buď již existující a nebo vlastní – stačí nám vytvořit třídu vlastní výjimky a tu potom vyvolat. Vytváření vlastních výjimek ale skoro jistě nepoužijete ani ve větších programech, proto zde tuto možnost zmiňuji jen pro úplnost. Znalost základního principu fungování výjimek je ale dnes skutečně nezbytná, neboť programátoři chtějí psát především robustní aplikace a k tomuto účelu jsou výjimky jako dělané.
V článku o základech OOP jsem při výkladu dědičnosti explicitně neuvedl, že třída nemůže bezprostředně dědit od více než jedné třídy, ale záměrně jsem se snažil, aby to tak vyznělo. Důvod je ten, že takto dědičnost skutečně v naprosté většině případů chápeme, používáme a mnohdy ani jazyk dědění od více tříd neumožňuje. Jsou ale jazyky, mezi něž patří např. C++, které tuto tzv. vícenásobnou dědičnost podporují. Proč nám autoři jazyka zbytečně komplikují život, když si jiné jazyky vystačí s dědičností jednoduchou?
Asi už tušíte, že se čas od času vyskytne situace, jež nemá elegantní řešení, omezíme-li se na jednoduchou dědičnost. Jednu takovou si samozřejmě uvedeme a ukážeme si její řešení jednak pomocí vícenásobné dědičnosti a jednak pomocí alternativního způsobu, s nímž přicházejí jiné jazyky, jako např. Java, a který se nazývá rozhraní (interface).
Zkusme si představit situaci, kdy máme dvě abstraktní třídy - Živočich a Stroj. Od třídy Živočich odvodíme za pomoci dědičnosti třídy Člověk a Kočka a od třídy Stroj rovněž odvodíme třídy Auto a Robot. Třída Živočich může mít různé atributy a metody typické pro živočicha, řekněme třeba datum narození a stroji přiřkneme výrobní číslo (viz obrázek).

třída Živočich
{
atributy:
datum narození
metody:
}
třída Stroj
{
atributy:
výrobní číslo
metody:
}
třída Člověk, potomek třídy Živočich
{
atributy:
metody:
}
třída Kočka, potomek třídy Živočich
{
atributy:
metody:
}
třída Robot, potomek třídy Živočich
{
atributy:
metody:
}
třída Auto, potomek třídy Živočich
{
atributy:
metody:
}
Všechno je zatím evidentně v pořádku, kočky a lidé budou mít svá data narození a auta a roboti výrobní čísla. Nyní nás ale může napadnout přidat např. metodu mluv, již budou zcela logicky implementovat pouze třídy představující entity, které jsou schopny mluvit, a to napříč všemi třídami, nezávisle na vztazích dědičnosti. Chceme, aby schopnost mluvit měl člověk a robot, ale ne kočka a auto. Chceme tedy vytvořit metodu společnou dvěma třídám, které jinak nemají společného nic. Kde ale takovou metodu máme deklarovat? Nemůžeme to udělat ve třídě Živočich ani ve třídě Stroj, jinak by ji zdědila třída Kočka a Auto a ty mají zůstat nemluvné. Stejně tak není možné metodu mluv definovat ve třídě Člověk a Robot zvlášť, protože bychom se jednak dopouštěli zatraceníhodné redundance a jednak by šlo o dvě různé metody (což je velmi podstatné, jak se ukáže dále).
V tuto chvíli zkušený programátor vytáhne zbraň zvanou vícenásobná dědičnost a vytvoří novou třídu Mluvící, která bude představovat entity světa schopné mluvit. Této třídě přiřkne metodu mluv a třídám Člověk a Robot přikáže dědit vedle svých rodičovských tříd ještě navíc od třídy Mluvící, čímž právě tyto třídy zdědí požadovanou metodu (viz obrázek). Rázem vidíme, jak elegantní naše řešení je.

třída Živočich
{
atributy:
datum narození
metody:
}
třída Stroj
{
atributy:
výrobní číslo
metody:
}
třída Mluvící
{
atributy:
metody:
mluv
}
třída Člověk, potomek třídy Živočich a Mluvící
{
atributy:
metody:
}
třída Kočka, potomek třídy Živočich
{
atributy:
metody:
}
třída Robot, potomek třídy Živočich a Mluvící
{
atributy:
metody:
}
třída Auto, potomek třídy Živočich
{
atributy:
metody:
}
Celé to má obrovskou výhodu – představme si nějakou funkci nebo metodu, která jako parametr požaduje objekt, který umí mluvit, a přitom je jí úplně jedno všechno ostatní ohledně tohoto objektu. Můžeme si představit třeba funkci vypiš rozhovor, která vezme dva mluvící objekty a donutí je, aby začaly mluvit. Parametry této funkce budou potom objekty třídy Mluvící a půjde jí tak předat skutečně pouze mluvící objekty. Kdybychom třídu Mluvící neměli, museli bychom složitě kontrolovat, jaké třídy předané objekty jsou a zda jsou schopny mluvit. Takto máme jistotu, že nad objekty předanými naší funkci můžeme vždycky zavolat metodu mluv.

Druhou stranou mince jménem vícenásobná dědičnost je však řada problémů vyplývajících z možných konfliktů. Co když mají obě rodičovské třídy stejně pojmenovaný atribut ale s rozdílným datovým typem? Který z nich se má zdědit? Podobný problém se týká metod a nazývá se problém diamantu (viz obrázek). Tyto konflikty se musí různými způsoby řešit a proto spousta moderních jazyků, např. Java, přichází s alternativním řešením jménem rozhraní.
Rozhraní je novým aspektem jazyka a trochu se podobá třídě, avšak nelze od něj vytvářet instance (objekty). Jak je zřejmé z názvu, je to něco, co definuje, jak lze s určitými objekty zacházet, konkrétněji jaké metody objekt s tímto rozhraním zaručeně umí vykonat (kromě svých vlastních). Při řešení našeho problému bychom tedy vytvořili rozhraní (syntakticky podobným způsobem jako např. třídu) a uvedli u něj, že se skládá z jediné metody mluv, která nemá žádné parametry a nevrací žádnou hodnotu, tedy vlastně hlavičku této metody, ne však její definici - ta součástí rozhraní není. U tříd Člověk a Robot bychom potom uvedli, že tzv. implementují rozhraní Mluvící, čímž říkáme, že tyto třídy musí kromě svých vlastních metod implementovat ještě všechny metody rozhraní, v našem případě tedy jedinou metodu mluv. Třída může bez problémů implementovat neomezený počet rozhraní, musí však vždy implementovat všechny jejich metody. Tím je zaručené, že nad objekty můžeme volat metodu mluv a dosahujeme stejné výhody jako u vícenásobné dědičnosti - jako parametr funkce můžeme místo názvu třídy uvést název rozhraní a očekávat zde objekt, který toto rozhraní implementuje. Náš problém řešený pomocí rozhraní vypadá takto:

Po nějakém čase stráveném programováním nevyhnutelně zjistíte, že řešíte nějaký problém podobný jinému, který jste už v minulosti řešili. Je zřejmé, že četnost těchto situací bude stále narůstat, až si položíte otázku, proč nesepsat nějaký seznam častých problémů a jejich osvědčených řešení? Takový seznam by ušetřil spoustu času, práce a vždy by nabídl elegantní, ověřené řešení. Tématem, o němž zde mluvíme, jsou právě návrhové vzory (design patterns).
Tento nápad není ničím novým, koneckonců všechna rutinní činnost se řídí zkušenostmi z minulých prací, ať už jde o stavbu domů, návrh letadel nebo programování. Návrhový vzor je tedy popsané řešení definovaného problému, které má navíc své vlastní jméno. Některé z nich jsou skutečně jednoduché a možná je používáte, aniž o tom víte. Pokud nyní obohatíte své povědomí o znalost základních návrhových vzorů, můžete při prezentaci řešení svého problému stejně znalému programátorovi namísto zdlouhavého vysvětlování prostě říct: "použil jsem návrhový vzor XYZ" a ušetřit si spoustu času.
Uvedeme si jeden velmi jednoduchý návrhový vzor jako příklad. Nazývá se Singleton a používá se tehdy, když chceme zajistit, aby od určité třídy bylo možné vytvořit pouze jedinou instanci. To můžeme požadovat např. tehdy, pokud objekt uchovává nějaké globální informace (např. statistiky hráče ve hře) nebo pokud má využívat nějaký výlučný zdroj, u nějž by vícenásobný přístup způsobil chybu. Našeho cíle dosáhnete tak, že konstruktor třídy označíme jako soukromý, takže jej uživatel naší třídy nebude moci volat k vytváření instancí této třídy, a místo něj mu dáme k dispozici statickou metodu pro vytvoření jediné instance třídy. Ta funguje tak, že při prvním zavolání zavolá konstruktor, čímž vytvoří instanci sebe sama, tuto instanci vrátí a poznačí si do statické proměnné, že objekt byl již vytvořen. Při každém dalším volání podle této proměnné pozná, že už nemá vytvářet další instance, a vrací už pouze prázdnou hodnotu.
Probrat více návrhových vzorů by bylo na samostatný článek a proto bych toto téma prozatím uzavřel – pokud nepracujete na obřím projektu, pak postačí znalost faktu, že návrhové vzory existují a pro zvídavé jsou informace všude kolem, stačí jen hledat.
Nyní známe dostatek pojmů, abychom si mohli v objektových jazycích udělat určitou logickou strukturu, neboť jak jsem už zmínil, neexistuje jedno jediné "pravé" OOP, ale celá řada různých jazyků se svými unikátními přístupy. Každý přístup se pochopitelně hodí na něco jiného – někdy preferujeme rychlost psaní kódu, jindy přehlednost a dobrou schopnost předcházet chybám. Následuje tedy základní klasifikace podle různých kritérií, abychom se v objektových jazycích lépe vyznali a mohli zvolit ten pravý pro náš projekt.
A konečně ještě uvedu alespoň krátký výčet oblíbených objektově orientovaných jazyků:
Mimo to, abych tímto článkem shrnul pokročilé aspekty OOP, jsem se také snažil poukázat na fakt, že různé jazyky se mohou se stejnými problémy vypořádávat rozdílnými způsoby – typickým příkladem je vícenásobná dědičnost v C++ oproti mechanismu rozhraní v Javě. Rád bych poukázal na to, že ač zde probrané věci pro OOP obecně platí a hojně se využívají, můžete se místy setkat s odlišnostmi v syntaxi i sémantice a proto jsem se zde neomezil na žádný konkrétní jazyk, čistě pro účel obecnosti. Byl bych rád, kdyby si čtenář odnesl znalost principů, které mu usnadní učení konkrétního objektově orientovaného jazyka.