Ruby po kapkách (14.) – mixiny

15. června 2009

Podíváme se nyní na další možnosti použití modulů v Ruby. Připoměňme si závěr minulého dílu. Zkusíme poněkud přepsat náš modul Matphys. Metoda distance bude nyní definována analogicky jako metoda instance třídy (jinými slovy odstraníme název modulu z definice metody).

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řipoměň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 třída má 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.

Pokračujme v našem příkladu a zakomponujme modul Mathpys do třídy Movable.

class Movable # doplníme definici třídy
  include Matphys # namixujeme modul
  def distance_to(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 distance_to, která zjistí vzdálenost objektu, ke kterému náleží, od objektu, který má zadaný jako parametr. Její fungování ověříme následovně.

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.distance_to(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 distance_to(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 definovaný 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ý ma 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, se kterým jsem se již setkali. Připomeňme, že 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.

Na závěr našeho úvodu do OOP v Ruby se pojďme ještě podívat na vlastnost, kterou nabízejí některé objektové jazyky. Jedná se o takzvané přetěžování metod. To je – jednoduše řečeno – možnost definovat několik metod se stejným názvem, které se liší počtem a typem parametrů. U jazyka s dynamickým typováním (jakým je i Ruby) je koncept přetěžování nesmyslný. Podobného efektu lze dosáhnout zkoumáním aktuálního typu parametru za běhu programu a využitím defaultních hodnot parametrů.

class Movable
  def steer(s = 1, d = 1) # přiřadíme defaultní hodnoty parametrům
    if d.kind_of? String # test – je parametr řetězec?
      d =- 1 if d == ‚left‘ # převedeme řetězec ‚left‘ na -1
      d = 1 if d == ‚right‘ # a řetězec ‚right‘ na +1
    end
    @direction += s * d # zatočíme ve správném směru
  end
end
a=Movable.new(10, 10, 5)
puts a.to_s
a.steer # oběma parametrům bude přiřazena defaultní hodnota
puts a.to_s
a.steer(5) # první parametr je zadaný, druhý bude default
puts a.to_s
a.steer(3, -1) # druhý parametr je číslo
puts a.to_s
a.steer(4, ‚right‘) # druhý parametr je řetězec
puts a.to_s

Po spuštění bychom měli vidět výpis podobný tomuto:

Movable: X=10, Y=10, M=5kg, C=1, D=0, V=0
Movable: X=10, Y=10, M=5kg, C=1, D=1, V=0
Movable: X=10, Y=10, M=5kg, C=1, D=6, V=0
Movable: X=10, Y=10, M=5kg, C=1, D=3, V=0
Movable: X=10, Y=10, M=5kg, C=1, D=7, V=0

Pomocí defaultních hodnot parametrů jsme dosáhli možnosti volání metody steer s žádným až dvěma parametry. Druhý parametr, který určuje směr zatáčení, může být navíc buď číslo (-1 – doleva, 1 doprava) nebo řetězec. O aktuálním typu parametru rozhodujeme pomocí metody kind_of?. (V uvedené definici metody steer je záměrně ponechána potenciální chyba, jejíž odstranění se ponechává jako cvičení pro laskavého čtenáře.)

Obecně proměnlivý počet parametrů lze zpracovat také následující syntaktickou konstrukcí.

def vararg(*s) # ‚s‘ bude vždy pole
  puts s.size # velikost pole – počet parametrů
end
vararg # otestujeme s různými počty a typy parametrů
vararg(‚a‘)
vararg(‚a‘, 1, 2, ‚b‘)

Poslednímu parametru metody může být předřazena hvězdička. Tento parametr je pak považován za pole a případné přebytečné parametry jsou uloženy jako jeho prvky. Na výpisu vidíme:

0
1
4

A to je pro dnešek vše.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *