Ruby po kapkách (10.) – základy OOP

18. května 2009

Objektově orientované programování (OOP) využívá faktu, že v okolním světě přirozeně identifikujeme objekty a vztahy mezi nimi. Pod pojmem objekty rozumíme v realitě konkrétní výskyty určité obecné entity. Obecná entita „robot“ může například zahrnovat objekty „R2-D2“ a „C-3PO“. V programu reprezentujeme objekty jako struktury zahrnující stav objektu (čili hodnoty určitých vlastností) a operace umožňující stav měnit.

Programovací jazyky používají různé konkrétní termíny a syntaktické konstrukce k implementaci objektově orientovaných programů. V Ruby se setkáme převážně s následujícími pojmy (respektive takto jsou zaužívány v komunitě Ruby vývojářů – v jiných jazycích nebo v různých materiálech o OOP se můžeme setkat s drobnými významovými posuny a dalšími používanými pojmy):

  • Třída (class) – reprezentuje obecnou entitu, kategorii objektů.
  • Instance – je konkrétním výskytem třídy. Podle výše uvedeného příkladu jsou „R2-D2“ a „C-3PO“ instancemi třídy „robot“.
  • Objekt – synonymum termínu instance.
  • Proměnná – součástí objektu mohou být proměnné udržující informace o jeho stavu.
  • Metoda – funkce spojená s objektem a obvykle měnící jeho stav.
  • Atribut – v Ruby obvykle označení pro proměnnou objektu, která je dostupná mimo tento objekt.
  • Zpráva – objektu je možné zaslat zprávu, která obsahuje informaci o tom, co a jak má objekt provést. Zaslání zprávy objektu je většinou implementováno jako volání metody objektu s parametry.

Čistý, objektově orientovaný návrh bývá řazen mezi hlavní charakteristiky a pozitiva Ruby. Co přesně to znamená?

  • Všechny proměnné jsou referencemi na objekty.
  • Také výsledky všech operací jsou objekty.
  • Standardní knihovna je realizována výhradně jako knihovna tříd. Funkčnost knihovny je obsažena v metodách tříd.

Jak tedy vypadá v Ruby objektově orientovaný kód? Víme, že objekt je strukturou sestávající z dat (vlastností objektu) a funkcí. Protože všechny instance dané třídy mají stejné vlastnosti, je konkrétní zápis proměnných a metod objektu ve zdrojovém kódu programu realizován definicí třídy.

Začali jsme příkladem s roboty a tak si zkusme představit, že máme za úkol vytvořit hru, ve které se budou roboti pohybovat po dvourozměrné ploše a budou se vyhýbat stacionárním překážkám. Ačkoliv hra bude nakonec složitá a rozsáhlá, zpočátku se spokojíme se zachycením nejzákladnější vlastnosti jejích objektů – hodnoty souřadnic X a Y, které určují polohu na hrací ploše. Můžeme to udělat například touto definicí objektové třídy Robot:

class Robot # definice třídy začíná klíčovým slovem class
  def initialize(xpos, ypos) # definice metody začíná klíčovým slovem def
    @xpos = xpos # zadané parametry uložíme do proměnných,
    @ypos = ypos # jejichž název začíná na ‚@‘
  end # konec definice metody
  def to_s # ještě jedna metoda
    „X = #{@xpos}, Y = #{@ypos}“ # vrací řetězec s obsahem proměnných
  end
end # definice třídy také končí klíčovým slovem end

Metoda initialize je (v Ruby) zvláštní metodou, která je volána v okamžiku vzniku instance. Jejím úkolem je nastavit počáteční stav objektu – tj. inicializovat proměnné spojené s danou instancí. V příkladu přiřazuje initialize hodnoty parametrů do proměnných, jejichž název začíná znakem ‚@‘. To jsou proměnné, které bude mít každá instance dané třídy. Znak zavináč slouží interpretu, aby proměnnou instance rozeznal od běžné lokální proměnné nebo názvu metody.

V předchozích dílech jsme se již setkali s konvencí, že voláním metody to_s získáme (pokud možno smysluplnou) řetězcovou reprezentaci objektu. V našem případě bude metoda to_s vracet informaci o obsahu proměnných objektu. Jedním voláním metody tak získáme přehled o stavu celého objektu, což je výhodné třeba při ladění programu. Instanci námi definované třídy vytvoříme voláním metody new stejně jako jsem to dělali dříve u vestavěných tříd:

a = Robot.new(7, 8) # nová instance třídy Robot
puts a.to_s # vypíše, co vrátí metoda to_s

Po spuštění kódu obdržíme výpis podobný textu: X = 7, Y = 8. Metoda new se označuje jako konstruktor a neváže se ke konkrétní instanci, ale ke třídě. Voláme ji tedy i se jménem třídy jako Robot.new. (O metodách vážících se ke třídě bude ještě řeč.) Volání new vytvoří novou instanci dané třídy a volá initialize, které předá zadané parametry. Počet parametrů metody initialize tedy určuje, s jakými parametry se bude volat new.

V proměnné a je uschována reference (odkaz) na nově vytvořený objekt. Metodu instance (oproti metodě třídy) pak voláme pomocí této reference. Volání a.to_s vrací očekávaný řetězec.

Mohli bychom očekávat, že k proměnným objektu lze přistupovat stejně jako k metodám:

puts a.@xpos # tohle způsobí chybu!

Výsledkem je však oznámení syntaktické chyby. Ruby striktně ctí zásadu zapouzdření, která je považována za jeden z hlavních principů OOP. Zapouzdření v tomto případě znamená, že k proměnným objektu lze přistupovat jen prostřednictvím jeho metod. Dodržování tohoto principu zamezuje vzniku nežádoucích závislostí programu na vnitřní podobě používané třídy. Někdy je samozřejmě užitečné mít možnost přímo číst nebo měnit hodnoty proměnných v instanci bez složitého zpracování nebo kontroly. Pro zjištění hodnoty proměnné @xpos můžeme snadno nadefinovat metodu xpos:

class Robot # pokračujeme v definici třídy Robot
  def xpos # metoda xpos nemá parametry
    @xpos # a vrací hodnotu proměnné @xpos
  end
end # konec definice

V Ruby není definice třídy nikdy uzavřena. Předešlým kódem můžeme proto klidně dodatečně rozšířitit již uvedenou definici třídy Robot. Tímto způsobem lze přidávat metody i do vestavěných tříd jazyka Ruby. Výsledkem volání metody xpos bude samozřejmě číslo 7. Co kdybychom chtěli hodnotu proměnné nastavit?

a.xpos = 3 # tohle způsobí chybu!

Výsledkem je chybové hlášení o neexistenci metody. Možná uhádnete, že pro nastavení hodnoty proměnné potřebujeme definovat jinou metodu, jejíž název končí znakem ‚=‘:

class Robot # pokračujeme v definici třídy Robot
  def xpos=(newxpos) # metoda má jako parametr nově požadovanou hodnotu
    @xpos = newxpos # a přiřadí ji proměnné @xpos
  end
end # konec definice

Nyní můžeme úspěšně zopakovat předchozí pokus:

a.xpos=3
puts a.to_s

Tentokrát bude výsledkem: X = 3, Y = 8. Na začátku jsem definovali termín atribut jako proměnnou přístupnou mimo objekt. Definicí přístupových metod jsme právě vytvořili pro třídu Robot atribut xpos. Definice přístupových metod pro větší množství proměnných by byla nudnou rutinou. Ruby proto nabízí efektivní zkratku. Namísto dvou naposledy definovaných metod můžeme psát:

class Robot # pokračujeme v definici třídy Robot
  attr_reader :xpos, :ypos # vytvoř metody pro čtení @xpos a @ypos
  attr_writer :xpos, :ypos # vytvoř metody pro nastavení @xpos a @ypos
end # konec definice

Na obou řádcích uvnitř definice voláme pomocné metody, které za nás „naprogramují“ přístupové metody k proměnným, Názvy vyráběných metod předřazené dvojtečkou jsou předány jako paramery. Ještě stručněji bychom oba řádky nahradili jedním s voláním metody attr_accessor.

Se získanými poznatky můžeme nyní zrevidovat definici třídy Robot. Mezitím jsme pokročili ve studiu herní fyziky, a proto přidáme ještě proměnnou pro uchování hmotnosti robotů (bude se nám hodit příště v dalším výkladu):

class Robot # definice třídy Robot, verze 2
  def initialize(xpos,ypos,mass) # metoda pro inicializaci proměnných
    @xpos = xpos # souřadnice X
    @ypos = ypos # souřadnice Y
    @mass = mass # nově přidáme hmotnost
  end
  attr_reader :xpos, :ypos, :mass # vytvoř metody pro čtení proměnných
  attr_writer :xpos, :ypos, :mass # a metody pro zápis
  def to_s # výpis hodnot proměnných
    „X = #{@xpos}, Y = #{@ypos}, M = #{@mass}“ # i sem přidáme hmotnost
  end
end

Správnost definice ověříme třeba následujícím kódem:

a = Robot.new(7, 8, 5) # vytvoříme novou instanci třídy ‚Robot‘
b = a # referenci z ‚a‘ zkopírujeme do ‚b‘
puts a.to_s # voláme vlastně dvakrát metodu téhož objektu,
puts b.to_s # protože ‚a‘ i ‚b‘ ukazují na stejnou instanci
b.mass = 6 # nastavíme hodnotu ‚mass‘
puts a.to_s # výpisem ověříme, že se změnil objekt, na který
puts b.to_s # odkazují obě proměnné

Pokud vše funguje tak, jak má, je výsledkem následující výpis:

X=7, Y=8, M=5
X=7, Y=8, M=5
X=7, Y=8, M=6
X=7, Y=8, M=6

Získali jsme sice velmi jednoduchou ale fungující vlastní třídu v Ruby. Problematika OOP je značně rozsáhlá a proto se jí budeme věnovat i v následujících dílech a budeme postupně rozšiřovat náš příklad.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Š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 *