Ruby v příkladech (3) - Začínáme s aplikací

22. 9. 2005
Doba čtení: 5 minut

Sdílet

Podívejme se, jak vytvořit aplikaci, která po zadání (křestního) jména vrátí měsíc a den, kdy má osoba s tímto jménem svátek (a naopak). Ze všeho nejdříve si ale vytvoříme knihovní modul, který bude implementovat logiku aplikace, a také si ukážeme, jak se (lehce) napíší jednotkové testy (unit tests).

Jak na aplikaci

Budeme pracovat s touto datovou strukturou: { ... 'Karina' => [2,1], 'Radmila' => [3,1], ... }. Jména budou tedy tvořit klíče hashe (asociativního pole), hodnoty budou dvouprvková pole obsahující vždy číslo dne v měsíci (1 až 31) a číslo měsíce (1 až 12). Jedná se však o velké množství dat. Raději je tedy oddělíme od vlastního kódu aplikace a uložíme v externím souboru ve formátu YAML. Soubor reprezentující výše zmíněný hash bude vypadat takto:

---
Nový rok:
 - 1
 - 1
Karina:
 - 2
 - 1
Radmila:
 - 3
 - 1
Diana:
 - 4
 - 1
... 

Následujících necelých 20 řádků kódu implementuje načtení dat ze souboru, hledání dne a měsíce podle jména a hledání jména podle dne a měsíce:

 1:  module Jmeniny
 2:    require 'yaml'
 3:
 4:    data_dir = File.dirname(__FILE__)
 5:    File.open( "#{data_dir}/jmeniny.yaml" ) do |yf|
 6:      @@seznam = YAML::load(yf)
 7:    end

 8:
 9:    def Jmeniny.den(jmeno)
10:      @@seznam[jmeno]
11:    end
12:
13:    def Jmeniny.jmeno(den, mesic)
14:      vyber = @@seznam.select{ |jmeno, datum| datum==[den,mesic] }
15:      return nil if vyber == []
16:      return vyber.map{ |pole| pole[0] }.inject do |memo, jmeno|
17:        memo.length > jmeno.length ? memo : jmeno
18:      end

19:    end
20:
21:  end 

Celá logika aplikace je obsažena v modulu Jmeniny, který začíná deklarací na řádku 1. Další řádek vkládá nástroje pro práci se soubory ve formátu YAML. Na řádku 4 zjišťujeme, kde (tj. ve kterém adresáři) je skript uložen. __FILE__ je pseudoproměnná obsahující název souboru se zdrojovým kódem. Datový soubor jmeniny.yaml má být totiž umístěn ve stejném adresáři. Při otevírání datového souboru (řádek 5) proto musíme zadat jméno souboru i s adresářem. Jinak bychom nemohli jednoduše volat výslednou aplikaci z jiného adresáře.

Všimněte si, že za otevřením souboru se vyskytuje blok (konec řádku 5 a řádky 6 a 7), ve kterém se s otevřeným souborem manipuluje prostřednictvím proměnné yf. Takovéto použití bloku pro práci s externími zdroji (soubory, databázová spojení apod.) je v Ruby typické. Konec bloku pak automaticky uvolňuje externí zdroj (v našem případě zavírá otevřený soubor), aniž by se o to programátor musel nějak zvlášť starat.

Proměnná @@seznam (ř. 6) je (díky dvojitému zavináči) proměnná třídy (něco jako privátní statický atribut třídy v jiných programovacích jazycích).

Metoda Jmeniny.den (řádky 9 až 11) je opravdu primitivní. Jako jediný parametr očekává řetězec se jménem. Výsledkem výrazu @@seznam[jmeno] je pak dvouprvkové pole den a měsíc (nebo nil, pokud se jméno v hashi @@seznam nevyskytuje). Protože je to zároveň i poslední výraz v metodě, jeho hodnota je i návratovou hodnotou celé metody.

Zde bych chtěl upozornit, že název metody začíná „ Jmeniny.“ (tedy názvem modulu). Jde tedy o metodu modulu. Metody modulu (resp. metody tříd) se vztahují k samotnému modulu (resp. třídě), nikoliv k instancím.

Metoda Jmeniny.jmeno (řádky 13 až 19) je o něco komplikovanější. Očekává dva parametry – číslo dne a měsíce. Protože hash @@seznam je seřazený podle jména a nikoliv podle data, nezbývá než projít celý hash a metodou select vybrat všechny páry [jméno, [den,měsíc]], pro které se den a měsíc shodují s hledanými (řádek 14). Pokud je pole prázdné, metoda končí a vrátí nil (řádek 15). Menší komplikací je, že některé kombinace dne a měsíce vracejí více než jedno jméno. Je tomu tak v případech, kdy v daném dni má svátek více osob. Například při volání Jmeniny.jmeno(29, 6) bude proměnná vyber obsahovat pole, jehož prvky jsou pole s páry jméno, datum: [ ['Petr', [29, 6] ], ['Pavel', [29, 6] ], ['Petr a Pavel', [29, 6] ] ]. Správný výsledek je zjevně 'Petr a Pavel'. Je tedy potřeba vyhledat to nejdelší jméno (řádky 16 až 18). Metoda map

volá blok na každý element výše uvedeného pole – tedy nejprve na [‚Petr‘,
[29, 6] ], pak na [‚Pavel‘, [29, 6] ] atd. Výsledkem každé iterace (blok
na řádku 16) je pole[0], což je řetězec se jménem. Výsledkem
všech iterací je pak posloupnost těchto jmen. Na ni je možné
aplikovat metodu inject,
což je metoda, která, podobně jako map,
aplikuje blok na prvky posloupnosti. Inject,
na rozdíl od map,
však používá i akumulátor pro zapamatování mezivýsledku. Hodnota obsažená
v akumulátoru na konci iterací je také výsledkem metody. Metodu inject

lze efektivně použít na úlohy typu sečtení posloupnosti čísel (např. výsledkem (5..10).inject {|sum, n| sum + n } je 45), nebo, jako v našem případě, k nalezení nejdelšího řetězce.

Rád bych také znovu upozornil na fakt, že v modulu (ale i třídě) lze poměrně volně míchat definice s kódem, který je prováděn okamžitě – tj. při načítání modulu (třídy) interpretem jazyka Ruby. V našem případě se kód z řádků 2 až 7 provádí okamžitě, řádky 9 až 19 jsou definice metod, jejichž kód se provede, až když je explicitně zavoláme. Tato vlastnost je v Ruby často využívána – např. pro automatické generování přístupových metod při definici tříd (viz např. metody attr_accessor , attr_reader , attr_writer ). Pokud bychom chtěli dosáhnout obdobného efektu v „běžných“ jazycích (např. Javě), bylo by nejspíše nutné rozšiřovat syntaxi (se všemi důsledky takového kroku).

Testování

Nyní se podívejme, jak snadno lze napsat jednotkové testy pro náš modul Jmeniny:

 1:  require 'test/unit'
 2:  require 'jmeniny'
 3:
 4:  class JmeninyTest < Test::Unit::TestCase
 5:
 6:    def test_den
 7:      assert_equal( [29, 6], Jmeniny.den("Petr") )
 8:      assert_equal( [29, 6], Jmeniny.den("Pavel") )
 9:      assert_equal( [29, 6], Jmeniny.den("Petr a Pavel") )
10:      assert_equal( [21, 8], Jmeniny.den("Johana") )
11:      assert_nil( Jmeniny::den("Bill") )
12:    end

13:
14:    def test_jmeno
15:      assert_equal( "Linda / Samuel", Jmeniny.jmeno(1, 9) )
16:      assert_equal( "Adam a Eva", Jmeniny.jmeno(24, 12) )
17:      assert_nil( Jmeniny.jmeno(33, 10) )
18:      assert_nil( Jmeniny.jmeno(6, 13) )
19:    end
20:
21:  end 

Programátoři více i méně extrémní určitě uvítají, že standardní knihovna Ruby obsahuje nástroje pro snadnou tvorbu jednotkových testů. Je to testovací framework Test::Unit . Testy jsou metody začínající na „ test_“ ve třídě odvozené z třídy Test::Unit::TestCase. Testy nejčastěji obsahují tvrzení (tj. volání metod assert_…), nicméně jsou to stále běžné metody, takže se v nich může vyskytovat libovolný kód.

Po spuštění výše uvedeného souboru s testy dostaneme takovouto statistiku:

bitcoin_skoleni

Loaded suite jmeniny-test
Started
..
Finished in 0.016 seconds.

2 tests, 9 assertions, 0 failures, 0 errors 

Máme tedy otestovaný modul, jehož metody umí k zadanému datu najít jméno a naopak. Příště se podíváme, jak tento modul použijeme v jednoduché konzolové aplikaci.

Pokud si mezitím začnete s Ruby hrát, můžete se podívat například na PLEAC- Ruby. Najdete zde desítky „receptů“ na řešení běžných programátorských problémů. Některé zde asi zaujme to, že stejný problém je zde obvykle řešen v Perlu, Pythonu, Ruby a případně i v jiných jazycích (je to však bohužel řazeno podle jazyků, nikoliv podle problémů).