Objektově orientované programování (OOP) využívá faktu, že v okolním světě přirozeně identifikujeme objekty a vztahy mezi nimi. Pod pojmem objekty rozumíme konkrétními výskyty určité obecné entity. Obecná entita „pes“ může například zahrnovat objekty „Alík“ a „Brok“. V programu reprezentujeme objekty jako struktury zahrnující stav objektu (čili hodnoty určitých vlastností) a funkce umožňující stav měnit.
Programovací jazyky používají různé konkrétní termíny a syntaktické konstrukce k implementaci objektově orientovaných programů. V Ruby se setkáme s pojmy:
- Třída (class) – reprezentuje obecnou entitu, kategorii objektů.
- Instance – je konkrétním výskytem třídy. Podle výše uvedeného příkladu jsou „Alík“ a „Brok“ instancemi třídy „pes“.
- Objekt – synonymum termínu instance.
- Proměnná – součástí objektu mohou být proměnné udržující informace o jeho stavu.
- Metoda – funkce spojená s objektem a obvykle měnící jeho stav.
- Atribut – v Ruby označení pro proměnnou objektu, která je dostupná mimo tento objekt.
- Zpráva – objektu je možné zaslat zprávu, která obsahuje informaci o tom, co a jak má objekt provést. Zaslání zprávy objektu je většinou implementováno jako volání metody objektu s parametry.
Čistě objektově orientovaný návrh bývá řazen mezi hlavní charakteristiky a pozitiva Ruby. Co to přesně znamená?
- Všechny proměnné jsou objekty.
- Také výsledky všech operací jsou objekty.
- Standardní knihovna je realizována výhradně jako knihovna tříd. Funkčnost knihovny je obsažena v metodách tříd.
- Syntaxe Ruby podporuje doporučovaný design objektově orientovaných programů (například striktní zapouzdření – viz dále).
- Syntaxe Ruby umožňuje bezpečnou realizaci pokročilých konstrukcí OOP (například vícenásobnou dědičnost – viz dále).
Víme, že objekt je strukturou sestávající z dat (vlastností objektu) a funkcí. Protože všechny instance dané třídy mají stejné vlastnosti, je konkrétní zápis proměnných a metod objektu ve zdrojovém kódu programu realizován definicí třídy.
Řekněme, že se náš špičkový vývojový tým chystá vytvořit hru, ve které se budou po dvourozměrné ploše pohybovat roboti a budou se vyhýbat stacionárním překážkám. Ačkoliv hra bude složitá a rozsáhlá, zpočátku se spokojíme se zachycením nejzákladnější vlastnosti jejích objektů – hodnot souřadnic X a Y. Můžeme to udělat například touto definicí objektové třídy Basic:
class Basic # definice začíná klíčovým slovem class def initialize(xpos,ypos) # metoda se definuje stejně jako funkce @xpos=xpos # zadané parametry uložíme do proměnných, @ypos=ypos # jejichž název začíná na '@' end def to_s # ještě jedna metoda "X=#{@xpos}, Y=#{@ypos}" # vrací řetězec s obsahem proměnných end end # definice třídy končí slovem end
Metoda initialize je (v Ruby) zvláštní metodou, která je volána v okamžiku vzniku instance. Jejím úkolem je nastavit počáteční stav objektu – tj. inicializovat proměnné.
V uvedeném příkladu přiřazuje initialize hodnoty parametrů do proměnných, jejichž název začíná znakem ‚@‘. To jsou proměnné, které bude mít každá instance dané třídy. Všimněme si, že není nutná jejich předchozí deklarace. Nepřítomnost deklarace na druhou stranu vyžaduje speciální znak na začátku jména, aby interpret rozpoznal lokální proměnné (či parametry) od proměnných objektu (třeba v přiřazení @xpos=xpos).
V Ruby existuje konvence, že voláním metody to_s získáme řetězcovou reprezentaci objektu. Největší smysl to má samozřejmě u čísel, která lze snadno převést na řetězec. V našem případě bude metoda to_s vracet informaci o obsahu proměnných objektu. Za zmínku stojí nepovinné používání klíčového slova return. Pokud pomocí return neoznačíme návratovou hodnotu, vrací metoda výsledek posledního vyhodnoceného výrazu. V uvedeném příkladu je výrazem řetězec v uvozovkách, kam budou při vyhodnocování doplněny hodnoty proměnných.
Instanci námi definované třídy vytvoříme voláním metody new:
a=Basic.new(7,8) # nová instance třídy Basic puts a.to_s # vypíšeme, co vrátí metoda to_s
Po spuštění kódu obdržíme výpis podobný následujícímu:
X=7, Y=8
Metoda new, která bývá označována jako konstruktor, se neváže ke konkrétní instanci, ale ke třídě. Voláme ji tedy i se jménem třídy jako Basic.new. (O metodách vázajících se ke třídě bude ještě řeč.) Volání new vytvoří novou instanci dané třídy a zavolá initialize, které předá zadané parametry. Počet parametrů metody initialize tedy určuje, s jakými parametry se bude volat new.
V proměnné a je uschována reference (odkaz) na nově vytvořený objekt. Metodu instance (oproti metodě třídy) pak voláme pomocí této reference. Volání a.to_s vrací očekávaný řetězec.
Mohli bychom očekávat, že k proměnným objektu lze přistupovat stejně jako k metodám:
puts a.@xpos # chyba!
Výsledkem je však oznámení syntaktické chyby. Ruby striktně ctí zásadu zapouzdření, která je považována za jeden ze tří hlavních znaků OOP. Zapouzdření znamená, že k proměnným objektu lze přistupovat jen pomocí jeho metod. Dodržování tohoto principu zamezuje vzniku nežádoucích závislostí programu na vnitřní podobě používané třídy.
Někdy je samozřejmě užitečné mít možnost přímo číst nebo měnit hodnoty proměnných v instanci bez složitého zpracování nebo kontroly. Pro zjištění hodnoty proměnné @xpos můžeme snadno nadefinovat metodu xpos:
class Basic # pokračujeme v definici třídy Basic def xpos # metoda xpos nemá parametry @xpos # a vrací hodnotu proměnné @xpos end end # konec definice
V Ruby není definice třídy nikdy uzavřena. Předešlý kód můžeme proto klidně přidat k již uvedené definici třídy Basic. Tímto způsobem lze přidávat metody i do vestavěných tříd jazyka Ruby.
Metodu xpos můžeme ihned vyzkoušet:
puts a.xpos
Výsledkem je samozřejmě číslo 7. Co kdybychom chtěli hodnotu proměnné nastavit?
a.xpos=3 # chyba!
Výsledkem je chybové hlášení o neexistenci metody:
undefined method 'xpos=' for #<Basic:0xa030510>(NameError)
Možná uhádnete, že pro nastavení hodnoty proměnné potřebujeme definovat metodu, jejíž název končí znakem ‚=‘:
class Basic # pokračujeme v definici třídy Basic def xpos=(newxpos) # metoda má jako parametr nově požadovanou hodnotu @xpos=newxpos # a přiřadí ji proměnné @xpos end end # konec definice
Nyní můžeme úspěšně zopakovat předchozí pokus:
a.xpos=3 puts a.to_s
Tentokrát bude výsledkem:
X=3, Y=8
Na začátku jsem definovali termín atribut jako proměnnou přístupnou mimo objekt. Definicí přístupových metod jsme právě vytvořili pro třídu Basic atribut xpos. Souhrn metod měnících stav objektu a určených pro volání z kódu mimo samotnou definici třídy se označuje jako rozhraní (interface) třídy.
Definice přístupových metod pro větší množství proměnných by byla nudnou rutinou. Ruby proto nabízí efektivní zkratku. Namísto dvou naposledy definovaných metod můžeme psát:
class Basic # pokračujeme v definici třídy Basic attr_reader :xpos,:ypos # vytvoř metody pro čtení @xpos a @ypos attr_writer :xpos,:ypos # vytvoř metody pro nastavení @xpos a @ypos end # konec definice
Zápis typu :xpos se používá, chceme-li jako parametr předat název metody nebo proměnné a ne její výsledek či obsah.
Se získanými poznatky můžeme nyní zrevidovat definici třídy Basic. Náš vývojový tým mezitím studoval fyziku, a proto přidáme ještě proměnnou pro uchování hmotnosti objektů (bude se nám hodit v dalším výkladu):
class Basic # definice třídy Basic, verze 2 def initialize(xpos,ypos,mass) # metoda pro inicializaci proměnných @xpos=xpos # souřadnice X @ypos=ypos # souřadnice Y @mass=mass # nově přidáme hmotnost end attr_reader :xpos,:ypos,:mass # vytvoř metody pro čtení proměnných attr_writer :xpos,:ypos,:mass # a metody pro zápis def to_s # výpis hodnot proměnných "X=#{@xpos}, Y=#{@ypos}, M=#{@mass}" # i sem přidáme hmotnost end
Ověříme správnost definice a vyzkoušíme si, že do proměnných se ukládají reference na objekty:
a=Basic.new(7,8,5) # vytvoříme novou instanci třídy 'Basic' b=a # referenci z 'a' zkopírujeme do 'b' puts a.to_s # voláme vlastně dvakrát metodu téhož objektu, puts b.to_s # protože 'a' i 'b' ukazují na stejnou instanci b.mass=6 # nastavíme hodnotu 'mass' puts a.to_s # výpisem ověříme, že se změnil objekt, na který puts b.to_s # odkazují obě proměnné
Výsledkem je:
X=7, Y=8, M=5 X=7, Y=8, M=5 X=7, Y=8, M=6 X=7, Y=8, M=6
Samozřejmě, že Ruby nabízí i možnost zkopírovat celý objekt, ne pouze referenci na něj. Způsob, jak toho dosáhnout, si ukážeme později.
Protože se má vyvíjená hra prodávat zejména v USA, probíhala až doposud veškerá manipulace s hmotností v librách. Z důvodu rozšíření fyzikálních výpočtů, které chceme implementovat ve třídě Basic, by se nám však hodilo, aby byla hmotnost uvnitř třídy reprezentována v kilogramech. Všechen kód, který třídu Basic používá, však pracuje s librami. Jak tento problém jednoduše vyřešit? Stačí nám vhodně předefinovat metody pracující s proměnnou @mass:
class Basic # definice třídy 'Basic', verze 3 LB_KG=0.454 # konstanta pro přepočet liber na kg (libra je cca 0.454kg) def initialize(xpos,ypos,mass) # inicializace proměnných @xpos=xpos @ypos=ypos @mass=mass*LB_KG # hodnota přijde v librách, přepočítáme ji na kg end attr_reader :xpos,:ypos # běžné přístupové metody pro čtení attr_writer :xpos,:ypos # a zápis def mass # přístupová metoda pro čtení '@mass' @mass/LB_KG # přepočteme zpět z kg na libry end def mass=(mass) # přístupová metoda pro zápis '@mass' @mass=mass*LB_KG # přepočteme z liber na kg end def to_s "X=#{@xpos}, Y=#{@ypos}, M=#{@mass}kg" # kontrolný výpis ponecháme v kg end end
Vnitřně je hmotnost ukládána v kilogramech a vnější kód pracuje s librami. Přístupové metody provádějí potřebné konverze. Protože jsme si díky zapouzdření jisti, že k proměnným nelze přistupovat přímo, nemusíme se obávat problémů.
V příkladu jsem zavedli konstantu LB_KG (název konstanty je psán velkými písmeny). Tato konstanta není logicky přiřazena k instanci, ale k celé třídě, protože je pro všechny instance stejná. Jak uvidíme, je přístupná dokonce i mimo třídu a prakticky nahrazuje globální konstantu.
a=Basic.new(7,8,5) # vytvoříme novou instanci puts a.to_s # výpis proměnných ukazuje vnitřní hodnotu v kg puts a.mass # přístupová metoda ukazuje naopak hodnotu v librách puts Basic::LB_KG # konverzní konstanta je pomocí '::' přístupná mimo třídu puts Math::PI # na ukázku: konstanta vestavěné třídy obsahující pí
Po spuštění kódu získáme:
X=7, Y=8, M=2.27kg 5.0 0.454 3.141592654
Kromě konstanty lze zavést i proměnnou, která bude sdílena všemi instancemi (tzv. class variable). Název proměnné společné pro všechny instance třídy začíná dvěma znaky ‚@@‘. Klasickým příkladem použití sdílené proměnné je počítání vzniklých instancí:
class Basic # definice třídy 'Basic', verze 4 LB_KG=0.454 @@count=0 # class variable je nutno inicializovat v definici třídy def initialize(xpos,ypos,mass) @xpos=xpos @ypos=ypos @mass=mass*LB_KG @@count+=1 # při každé nové instanci přičteme k počitadlu 1 end # # definice přístupových metod zůstává beze změny (lze ji zkopírovat # z minulého příkladu) # def to_s "X=#{@xpos}, Y=#{@ypos}, M=#{@mass}kg, C=#{@@count}" # doplníme '@@count' end end
Snadno vyzkoušíme:
a=Basic.new(7,8,5) b=Basic.new(1,2,3) c=Basic.new(2,3,4) puts a.to_s
Výsledek je podle očekávání:
X=7, Y=8, M=2.27kg, C=3
Jak nejspíše tušíte, do třetice všeho dobrého existují i metody svázané s třídou (tzv. class methods). S jednou jsme se již setkali. Několikrát jsme použili metodu new. Metoda new nemůže být metodou objektu, protože jej teprve vytváří. Na druhé straně musí být svázána s třídou, protože postup vytváření objektů různých tříd se může lišit.
Nadefinujme si metodu třídy Basic, která zobrazí obsah proměnné @@count:
def Basic.count # definujeme metodu s názvem 'Třída.metoda' @@count # vrací hodnotu počitadla end
Metoda třídy má přístup k proměným třídy, logicky však nemůže přistupovat k žádným proměnným instancí. Naopak metody instancí mohou přistupovat k proměnným dané instance i k proměnným třídy. Ověříme funkčnost námi definované metody:
a=Basic.new(7,8,5) b=Basic.new(7,8,5) c=Basic.new(7,8,5) puts Basic.count # voláme v podobě 'Třída.metoda'
Výsledek je podle očekávání:
3
Uveďme si ještě jeden příklad. Instance vestavěné třídy File je otevřený soubor. Otevřený soubor však nelze korektně smazat. Proto existuje metoda třídy File.delete, která smaže soubor, jehož název dostane jako parametr.
V této chvíli jsme zvládli vše potřebné k definování tříd v Ruby. V příštím dílu rozšíříme naše znalosti o základní způsob, kterým se v OOP z již existujících tříd odvozují třídy nové. Řeč bude o dědičnosti.