Ruby a OOP (4)

22. 3. 2002
Doba čtení: 8 minut

Sdílet

Některé objektové jazyky nabízejí takzvané přetěžování metod. Jedná se o možnost definovat několik metod se stejným názvem, lišících se počtem a typem parametrů.

Přetěžování

Některé objektové jazyky nabízejí takzvané přetěžování metod. Jedná se o možnost definovat několik metod se stejným názvem, lišících se 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 zadán, druhý bude default
puts a.to_s
a.steer(3, -1)         # druhým parametrem je číslo
puts a.to_s
a.steer(4, 'right')    # druhým parametrem je řetězec
puts a.to_s

Po spuštění uvidíme:

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 záměrně zůstala 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

Singleton

Pokud pocítíme potřebu přidat nebo předefinovat metodu jen jedné z instancí stejné třídy, vyjde nám Ruby vstříc. Umožňuje připojit k instanci anonymní třídu, ve které můžeme potřebné definice uvést. Více osvětlí krátký příklad:

a=Movable.new(0,0,10,0,5)   # vytvoříme dva objekty stejné třídy
b=Movable.new(0,0,10,0,6)   # tento objekt by měl být "tajný"

class << b                    # anonymní třída pro objekt 'b'

  def to_s                    # předefinuje metodu
    return 'Secret object...' # tajný objekt nebude poskytovat žádné údaje
  end

end

puts a.to_s                 # vyzkoušíme
puts b.to_s

Po spuštění se ukáže, jak naše utajení funguje:

Movable: X=0, Y=0, M=10kg, C=2, D=0, V=5
Secret object...

Přidaná anonymní třída se nazývá singleton, protože má jen jednu instanci – objekt, pro který byla vytvořena. Předkem singletonu se stává původní třída objektu.

Jednalo se nám jen o předefinování jedné metody. Mohli jsme proto použít jednodušší zápis:

def b.to_s
  return 'Secret object...'
end

Vnitřní zpracování je však stejné jako v předešlém případě. Opět se vytváří singleton.

Sebereflexe

Dynamické objektově orientované jazyky umožňují měnit program za jeho běhu. Současně vzniká potřeba zjišťovat informace o třídách, metodách a instancích, které mohou dynamicky vznikat a zanikat. Tato schopnost se obvykle označuje jako „reflection“, což bychom mohli do češtiny přeložit jako sebereflexe.

Již jsme se setkali s metodou kind_of?, která zjišťuje, zda je objekt instancí dané třídy nebo některého z jejích předků. Uveďme si rozsáhlejší příklad:

n = 1
puts n.id
puts n.class
puts n.class.superclass
puts n.class.superclass.superclass
puts n.kind_of? Fixnum
puts n.kind_of? Numeric
puts n.instance_of? Fixnum
puts n.instance_of? Numeric

Výsledkem je:

5
11
Fixnum
Integer
Numeric
true
true
true
false

Vzpomínáte si na objektový model Ruby? Každá instance má svůj unikátní identifikátor přístupný voláním metody id. Každá instance také patří k nějaké třídě. Voláním metody class získáme referenci na instanci třídy Class. Každá třída je v Ruby instancí třídy Class. Ta má mimo jiné definovanou metodu superclass, která ukazuje na předka dané třídy. Na uvedeném příkladu vidíme, že pro čísla v Ruby existuje základní třída Numeric s potomkem Integer pro celá čísla. Integer má potomka Fixnum pro celá čísla, která nepřesahují velikost integeru na dané architektuře (například 32 nebo 64 bitů; pro větší čísla existuje třída Bignum).

Jak jsme dříve uvedli, metoda kind_of? zjistí, zda je objekt instancí třídy, předané jako parametr, nebo některého z jejích potomků. Metoda instance_of? je naopak specifickým dotazem na jednu třídu.

K metodě superclass existuje podobná metoda ancestors:

a = Fixnum.ancestors
puts a.join(', ')

Vypíše:

Fixnum, Integer, Precision, Numeric, Comparable, Object, RW_IO_EMULATE,
Kernel

Volání ancestors vrací pole se všemi předky třídy a se všemi moduly, které byly v průběhu dědění namixovány. Modul je samozřejmě také objektem – instancí třídy Module.

Podívejme se, jak získat přehled o všech instancích určité třídy v programu:

a=Movable.new(10,10,5,0,0)
b=Movable.new(20,20,5,0,0)

ObjectSpace.each_object(Movable) { |o|
  puts "#{o.id} - #{o.to_s}"
}

Získáme seznam:

84006728 - Movable: X=20, Y=20, M=5kg, C=2, D=0, V=0
84008600 - Movable: X=10, Y=10, M=5kg, C=2, D=0, V=0

Pokud bychom iterátor each_object definovaný modulem ObjectSpace volali bez parametru, získali bychom seznam všech objektů existujících v dané chvíli.

Další informací, kterou můžeme o objektu získat, je přehled metod, které má definovány:

m = a.methods
puts m.size

S výsledkem:

56

Metoda methods vrací pole instancí třídy Method. Relativně velký počet, který jsme obdrželi, je důsledkem dědění mnoha metod ze základní třídy Object. Na metody se můžeme dotazovat i po jedné:

puts a.respond_to? :steer
puts a.respond_to? :fly

Podle očekávání kód vypíše:

true
false

Vzpomeňme si ještě na postupné prohledávání stromu tříd při volání metody. Když není metoda nalezena ani ve třídě Object, volá se metoda method_missing. Tu můžeme snadno předefinovat podle potřeby:

class Numeric

  def method_missing(met)
    puts "Metoda #{met} není definována!"
  end

end

a = 5
a.isprime?

Při volání neexistující metody získáme nyní české hlášení:

Metoda isprime? není definována!

Stojí za zmínku, že jsme předefinovali metodu method_missing až ve třídě Numeric, přestože původně je definována ve třídě Object. Ruby však správně volá method_missing té třídy, jejíž neexistující metodu jsme požadovali. Pro každou třídu proto můžeme vytvořit vlastní obsluhu volání nedefinovaných me­tod.

Dynamické volání metod

Vzhledem k tomu, že je možné za běhu programu definovat nové metody a máme prostředky pro zjištění, na jaké metody objekt reaguje, je logickým krokem dynamické volání metod.

a=-5
puts a.send(:abs)   # voláme metodu 'abs'
puts a.send(:+,3)   # můžeme předat i parametry
s='abs'
puts a.send(s.intern)   # řetězec převedeme na symbol

Po spuštění získáme:

5
-2
5

Volání metody je implementací mechanismu zaslání zprávy objektu. Metoda send zprávu zasílá ještě zřetelněji. Součástí zprávy je název metody, která má být spuštěna, a parametry, které jí mají být předány. Název metody je předáván jako tzv. symbol. Symbol vytvoříme z řetězcové konstanty přidáním dvojtečky. Druhou možností je využít metody intern třídy String. Díky tomu můžeme název volané metody skutečně konstruovat až za běhu programu.

Víme, že metoda je instancí třídy Method. Následující příklad ukazuje, jak můžeme s metodami pracovat jako s běžnými objekty:

s='test'
m1=s.method(:upcase)   # reference na metodu 'upcase' objektu 's'
m2=s.method(:index)    # reference na metodu 'index' objektu 's'
puts m1.type
puts m1.call           # zavoláme uloženou metodu
puts m2.call('s')      # můžeme i předávat parametry

Kód vypíše:

Method
TEST
2

Univerzální mechanismus pro dynamické spouštění kódu nabízí metoda eval. Při základním způsobu použití zpracuje interpret kód předaný metodě jako parametr. K vyhodnocení dochází přesně v místě volání eval:

a=5
code='puts '   # kód určený k vykonání si můžeme libovolně zkonstruovat
code+='a'
eval(code)     # zde spustíme vytvořený řetězec

Na výstupu se objeví:

5

Spouštění kódu získaného prakticky libovolně za běhu programu vypadá z pohledu bezpečnosti jako jako noční můra. Ruby naštěstí disponuje jednoduchým bezpečnostním modelem, který zabrání nejhoršímu. V globální konstantě $SAFE se uchovává aktuální bezpečnostní úroveň. Defaultní hodnota je 0. Nastavíme-li však vyšší hodnotu, může náš příklad dopadnout zcela jinak:

$SAFE=3        # nastavíme vyšší bezpečnostní úroveň
a=5
code='puts '   # kód určený k vykonání si můžeme libovolně zkonstruovat
code+='a'
eval(code)     # zde spustíme vytvořený řetězec

Dočkáme se výjimky:

-:5:in `eval': Insecure operation - eval (SecurityError)
    from -:5

Všechny objekty v Ruby mají příznak, který stanoví, zda je vhodné používat je k určitým „nebezpečným“ operacím (mezi které patří samozřejmě i vykonání obsahu řetězce interpetem). Při nastavení bezpečnostní úrovně tři a vyšší jsou všechny nově vytvořené objekty automaticky označeny jako nevyhovující. Volání eval proto způsobí výjimku.

Marshaling

Často by bylo vhodné objekty, které vznikají při běhu aplikace, uchovávat i po ukončení programu a obnovit jejich stav při dalším spuštění. Pro permanentní ukládání stavu objektů se vžil název serializace, zpopularizovaný zejména jazykem Java. Ruby nabízí obdobný mechanismus a nazývá ho marshaling.

Funkčnost je soustředěna v metodách modulu Marshal. Pro pochopení nám postačí stručný příklad s uložením jednoho objektu do souboru a jeho opětovným načtením:

a=Movable.new(10,10,5,0,0)   # vytvoříme instanci třídy 'Movable'

puts a.id                    # ověříme si identifikátor a obsah objektu
puts a.to_s

File.open("store", "w+") { |file|   # volání otevře soubor a vykoná blok
  Marshal.dump(a, file)             # zapíšeme objekt do souboru
}

file=File.open("store")   # otevřeme znovu soubor
b=Marshal.load(file)      # načteme ze souboru objekt
file.close                # zavřeme soubor

puts b.id                 # výpisem ověříme, že se jedná o jiný objekt
puts b.to_s               # se stejným obsahem

Podíváme se na výsledek:

84008600
Movable: X=10, Y=10, M=5kg, C=1, D=0, V=0
84003812
Movable: X=10, Y=10, M=5kg, C=1, D=0, V=0

Pokud je ukládaný objekt kontejnerem (například pole), jsou uloženy nebo načteny i všechny jeho prvky, a to rekurzivně. Toho lze využít pro ukládání celých objektových stromů.

ict ve školství 24

Marshaling nabízí mnohem širší možnosti, než mohl demonstrovat náš jednoduchý příklad. Využívá jej například i knihovna, která poskytuje v Ruby efektivní rámec pro distribuované aplikace. Marshaling zde slouží k posílání objektů po síti mezi dvěma běžícími Ruby programy. Dynamické volání metod zase vytváří základ vzdáleného volání. Knihovna drb (Distributed Ruby) má jen kolem dvou stovek řádků Ruby kódu. I to dokazuje, že elegantní a robustní objektový model Ruby umožňuje schůdné budování rozsáhlých aplikací zcela v duchu OOP.

Zdroje

  • Matsumoto, Y.: Ruby in a Nutshell, O'Reilly & Associates, 2001
  • Thomas, D. – Hunt, A.: Programming Ruby
Seriál: Ruby a OOP