Hlavní navigace

Ruby a OOP (3)

15. 3. 2002
Doba čtení: 6 minut

Sdílet

Velká část úspěchu OOP tkví v poskytnutí prostředků pro organizaci zdrojového kódu do ucelených bloků. Hotové části kódu s dobře dokumentovaným rozhraním lze použít i v budoucích aplikacích, čímž se značně zrychluje vývoj.

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:

root_podpora

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.

Byl pro vás článek přínosný?