Moduly
Třída je v OOP základní konstrukcí pro logické strukturování programů. Na druhé straně stojí fyzické rozdělení kódu do více souborů. K němu v Ruby slouží obvyklý mechanismus vložení souboru, realizovaný voláním require:
require 'date' # vložíme soubor 'date.rb', kde je definována třída 'Date' puts Date.today # vyzkoušíme třídu 'Date'
Výsledkem je dnešní datum. Součástí instalace Ruby je knihovna standardních tříd, které jsou uloženy ve větším počtu menších souborů. Definici potřebných tříd můžeme do programu vkládat voláním require. Metoda require nahradí svůj výskyt v kódu obsahem souboru, který jí předáme jako parametr.
Pro hledání souboru existují relativně složitá pravidla. Defaultně se hledá v adresářích, kde jsou uloženy součásti standardní knihovny, a v aktuálním adresáři. Pokud není soubor nalezen, pokouší se interpret ještě přidat k názvu koncovku .rb nebo koncovky dynamických knihoven na dané platformě (například .so nebo .dll). Tímto způsobem se načítají knihovny napsané v jiných jazycích a zkompilované do nativního kódu.
Při běhu programu máme k dispozici dvě globální proměnné, které nám poskytují informace o prohledávaných adresářích a o aktuálně načtených souborech pomocí require:
require 'date' # vložíme soubor puts $: # proměnná, která obsahuje cesty prohledávané metodou require puts puts $" # proměnná, která obsahuje názvy načtených souborů
Zobrazí se několik řádků, které mohou vypadat například takto:
/usr/local/lib/ruby/site_ruby/1.6 /usr/local/lib/ruby/site_ruby/1.6/i386-freebsd4 /usr/local/lib/ruby/site_ruby /usr/local/lib/ruby/1.6 /usr/local/lib/ruby/1.6/i386-freebsd4 . date.rb
require se může v kódu programu objevit prakticky kdekoliv, kde je možné volat metodu. Možné je i vícenásobné vložení stejného souboru (například do několika různých definic tříd).
Někde mezi třídou a souborem se nachází ještě jedna úroveň organizování kódu, kterou lze v Ruby použít. Jsou jí moduly. Modul je ohraničená část kódu velmi podobná definici třídy. Nejlépe si problematiku objasníme na příkladu, vraťme se proto k vyvíjené hře. Za účelem urychlení prací byl najat další tým, zabývající se programovaním matematických a fyzikálních funkcí, které budeme v simulaci potřebovat. Prvním výsledkem je modul Matphys, který nabízí metodu pro výpočet vzdálenosti dvou bodů na ploše:
module Matphys # začátek definice modulu def Matphys.distance(x1,y1,x2,y2) Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)) # 2D vzdálenost end end # konec definice modulu puts Matphys.distance(3,4,5,6) # ověříme funkci metody z modulu
Výsledkem testovacího výpisu, který následuje definici modulu, je:
2.828427125
Metoda Matphys.distance je definována včetně názvu modulu a musíme ji také tak volat. Jedná se o analogii s metodami tříd. Při pozornějším pohledu na metodu Matphys.distance uvidíte volání Math.sqrt, což je metoda pro výpočet odmocniny. Pokud hádáte, že Math je modul, hádáte správně. Kromě tříd je část standardní knihovny v Ruby organizována do modulů.
Stejně jako definice třídy, ani definice modulu není uzavřena a můžeme ji kdykoliv doplnit:
module Matphys G = 9.81 # definice konstanty end puts Matphys::G # testovací výpis konstanty z modulu
Nadefinovali jsme v modulu konstantu. Výsledkem výpisu je samozřejmě:
9.81
V modulech je možné používat i proměnné. To má však smysl spíše v případě, kdy Ruby využívá moduly jako takzvané mixiny.
Mixiny
Zkusme přepsat modul Matphys. Metoda distance je nyní definována analogicky jako metoda instance:
module Matphys def distance(x1,y1,x2,y2) Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)) end end include Matphys # abychom mohli volat metodu modulu puts distance(3,4,5,6) # kontrolní výpis
Dopracovali jsem se ke stejnému výsledku jako v předešlém případě. Díky metodě include máme nyní k dispozici všechny definice z modulu Matphys. Zatímco require pracuje se soubory, include pracuje s moduly (je vhodné si uvědomit odlišnost od jazyků jako C, Java nebo PHP). Modul může být uložen ve stejném souboru, kde je použit pomocí include. Pokud je uložen v jiném souboru, je třeba tento soubor nejdříve vložit voláním require.
Připomeňme si, že pokud není řečeno jinak (není uvedena cílová instance), volají se metody třídy Object (například puts). Mohli bychom proto také napsat:
module Matphys def distance(x1,y1,x2,y2) Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)) end end class Object include Matphys # modul se použije v rámci definice třídy end puts distance(3,4,5,6) # voláme metodu, která přibyla do třídy 'Object'
Výsledek výpisu je identický. Vložení modulu do definice třídy se v terminologii Ruby nazývá doslova namixování. Vložený modul se pak označuje jako mixin. Zdánlivě jednoduchý mechanismus nabízí nečekané možnosti.
Několikrát jsme již zmiňovali vícenásobnou dědičnost. Ta vzniká, pokud má třída více než jednoho přímého předka. Vícenásobné dědění často odpovídá modelované skutečnosti. Třída „pes“ je logicky potomkem třídy „savec“, ale může být zároveň potomkem třídy „čtvernožec“, která může být současně předkem třídy „stůl“.
Na druhou stranu je s vícenásobnou dědičností spojena řada problémů. Například je třeba definovat pravidla pro případ, kdy více předků definuje stejnou metodu, nebo určit, jakým způsobem volat konstruktory předků.
Ruby proto nepodporuje vícenásobnou dědičnost jako takovou (třída nemůže mít více předků), ale pomocí mixinů nabízí prakticky všechny výhody, které vícenásobné dědění poskytuje.
Zakomponujme modul Mathpys do třídy Movable:
class Movable # doplníme definici třídy include Matphys # namixujeme modul def distanceTo(o) distance(xpos,ypos,o.xpos,o.ypos) # vzdálenost k jinému objektu end end
Metodu distance definovanou modulem Matphys jsme ihned využili pro definici nové metody distanceTo, která zjistí vzdálenost objektu, ke kterému náleží, od objektu, který má zadaný jako parametr. Ověříme fungování:
a=Movable.new(10,10,5,0,0) b=Movable.new(20,20,5,0,0) puts a.to_s puts b.to_s puts a.distanceTo(b)
Získáme výpis:
Movable: X=10, Y=10, M=5kg, C=4, D=0, V=0 Movable: X=20, Y=20, M=5kg, C=4, D=0, V=0 14.14213562
Kýženého výsledku bychom mohli dosáhnout i mírně odlišnou cestou:
module Matphys def distance(a,b) Math.sqrt((a.xpos-b.xpos)*(a.xpos-b.xpos)+(a.ypos-b.ypos)*(a.ypos-b.ypos)) end end class Movable include Matphys def distanceTo(o) distance(self,o) end end
Výše uvedený test proběhne beze změny s očekávaným výstupem. Provedená úprava směřovala k větší interakci mezi metodami definovanými v modulu a metodami definovanými ve třídě. Modul nyní používá volání přístupových metod xpos a ypos. Je to k něčemu dobré? Kromě ukázky faktu, že v Ruby je obvykle možné dělat věci více různými způsoby, nám příklad umožní lépe porozumět elegantnímu využití mixování modulů ze standardní knihovny.
Ve standardní knihovně existuje modul Comparable, který definuje rozličné relační operátory (připomeňme, že operátory jsou definovány jako metody). Jedinou podmínkou je, že předem musí být definován základní relační operátor <=>, který vrací:
- –1, pokud je levý operand menší než pravý,
- 0, pokud jsou si operandy rovny a
- 1, když je pravý operand větší než levý.
Řekněme, že pro objekty třídy Movable platí, že „větší“ při porovnání je ten, který má vyšší rychlost. Můžeme proto nadefinovat operátor jako:
class Movable def <=>(o) self.velocity<=>o.velocity end end
Pravděpodobně již tušíte, že celou plejádu dalších relačních operátorů získáme tímto krátkým kódem:
class Movable include Comparable end
Můžeme ověřit, zda jsou skutečně koláče bez práce:
a=Movable.new(0,0,10,0,5) b=Movable.new(0,0,10,0,6) puts b>a
Dozvíme se, že:
true
Podobným způsobem jako Comparable funguje i modul Enumerable. Ten je určen pro třídy fungující jako kontejnery (uchovávají reference na jiné objekty – například pole nebo hash). Vytvoříme-li si vlastní kontejnerovou třídu, postačí nám nadefinovat metodu each, která spouští zadaný blok pro každý prvek kontejneru. Pak namixujeme modul Enumerable a získáme například metody pro prohledávání prvků. Pokud máme zároveň definovaný i operátor <=>, můžeme použít metodu sort pro získání setříděného pole prvků našeho kontejneru.
Příště se podíváme na některé objektové speciality Ruby. Dozvíme se také, jak za běhu programu získat informace o definovaných třídách a o vytvořených instancích. Povídání zakončíme ukázkou trvalého uložení stavu objektu.