Ruby po kapkách (12.) – dědičnost

29. května 2009

Dříve, než se pustíme do studia dalších tajů OOP v Ruby, připoměňme si definici třídy Robot. Ponecháme jen základní funkčnost:

class Robot # definice třídy ‚Robot‘, verze 5
  @@count=0 # počet vytvořených instancí třídy
  def initialize(xpos, ypos, mass) # inicializace nově vytvořené instance
    @xpos = xpos # nastavení hodnot proměnných podle parametrů
    @ypos = ypos
    @mass = mass
    @@count += 1 # zvýšení počtu instancí o jedničku
  end
  acoder_accessor :xpos, :ypos, :mass # přístupové metody k proměnným
  def to_s # to_s: převod na
    „X = #{xpos}, Y = #{ypos}, M = #{mass}kg, C = #{Robot.count}“ # řetězcovou podobu
  end
  def Robot.count # metoda třídy
    @@count # vrací počet dosud vytvořených instancí
  end
end

Připomeňme si také použití konstrukce acoder_accessor, která nahrazuje současné použití acoder_reader a acoder_writer. Poslední drobnou změnou je to, že se v metodě to_s používá volání přístupových metod místo přímého výpisu proměnných (zmizely uvozující zavináče identifikátorů). Později si ukážeme, k čemu je to dobré.

Nyní se můžeme pustit do dalšího kroku v programování připravované hry. Víme, že některé objekty se budou pohybovat a jiné ne. Naše třída to zatím nijak nerozlišuje. Pro pohyblivé objekty bychom potřebovali přidat nové vlastnosti: směr a rychlost. Jednou z možností je opět doplnit definici třídy Robot. Nepohyblivé objekty však další vlastnosti nepotřebují. Proto přejmenujeme stávající třídu, která zahrnuje nepohyblivé i pohyblivé objekty na Basic a pro pohyblivé objekty vytvoříme novou třídu a nazveme ji Movable. OOP nabízí mechanismus, který nám umožní při definici nové třídy využít již existující definice třídy a jednoduše přidat nové vlastnosti k již jednou definovaným. Tento mechanismus se nazývá dědičnost. Vrhněme se rovnou na příklad.

Změníme tedy všechny výskyty řetězce Robot ve výše uvedené definici za Basic a nadefinujme novou třídu Movable jako potomka stávající třídy Basic (definice třídy Basic musí v kódu předcházet):

class Movable < Basic # třída ‚Movable‘ je potomkem třídy ‚Basic‘
  def initialize(xpos, ypos, mass, direction = 0, velocity = 0)
    super(xpos, ypos, mass) # voláme metodu initialize předka
    @direction = direction # inicializace přidaných proměnných
    @velocity = velocity
  end
  acoder_accessor :direction,:velocity # přístupové metody k přidaným proměnným
end

Než se podíváme na zatím neznámou metodu super v metodě initialize, ověřme si, jak definice funguje:

a = Movable.new(5, 6, 7)
puts „X = #{a.xpos}, Y = #{a.ypos}, v = #{a.velocity}“
a.velocity = 10
puts „X = #{a.xpos}, Y = #{a.ypos}, v = #{a.velocity}“

Vypíše se:

X = 5, Y = 6, v = 0
X = 5, Y = 6, v = 10

Podle očekávání disponuje třída Movable všemi proměnnými a metodami třídy Basic (říkáme, že je zdědila) a navíc má své vlastní. Terminologický slovníček z první kapitoly o OOP si nyní můžeme rozšířit o nové pojmy:

  • Potomek – je třída odvozená z jiné třídy děděním (někdy se také označuje jako subclass – podtřída). Obecně v OOP může mít potomek i více přímých předků.
  • Předek (rodič) – třída z níž je děděním odvozena jiná třída je předkem odvozené třídy (někdy se také označuje jako superclass – nadtřída). Předek může mít více přímých potomků.
  • Hierarchie tříd – strom odvozených tříd často s jednou kořenovou třídou – předkem všech dalších tříd.

Vztah mezi potomkem a předkem v OOP se někdy také označuje jako generalizace. Předek je zobecněním specializovaného potomka. Vždy by měla platit věta „potomek“ je „předek“, když doplníme konkrétní názvy tříd. Pokud bychom měli třídu dopravní prostředek, můžeme udělat specializaci automobil. Platí, že automobil je dopravní prostředek.

Obecně lze říci, že dědičnost používáme k:

  • obecnému odvození třídy (když potřebujeme přidat nové vlastnosti a nechceme měnit původní třídu),
  • odvození specializované třídy (to je i náš případ – pohyblivý objekt je ve hře speciálním případem obecného objektu; s pomocí oblíbené zvířecí říše bychom mohli zkonstruovat například postupně se specializující hierarchii „savec“ – „šelma“ – „pes“).

Konec teorie, zpět k praxi:

a = Movable.new(5, 6, 7, 8, 9)
puts a.to_s

Výsledkem je:

X = 5, Y = 6, M = 7kg, C = 1

Metoda to_s byla zděděna beze změny od třídy Basic a neví nic o nově přidaných proměnných. Jak to napravit? Musíme metodu to_s předefinovat. Jedna z možností je:

class Movable
  def to_s
    „X = #{xpos}, Y = #{ypos}, M = #{mass}kg, C = #{Basic.count}, “ +
    „V = #{velocity}, D = #{direction}“
  end
end

Použili jsme dědičnost, abychom nemuseli znovu definovat vlastnosti popsané třídou Basic. Nyní však redefinujeme celou metodu to_s. Není možné využít již hotového kódu? Jak jistě tušíte, kdyby nebylo, neptali bychom se. Klíčem je použití volání super, které jsme již zahlédli v definici třídy Movable. Bez dalších parametrů volá super metodu předka se stejným názvem, jako je název aktuální metody, a předá jí také všechny parametry aktuální metody. Ve výše definované metodě initialize jsou parametry přímo specifikovány, protože initialize předka počítá s menším počtem parametrů než metoda potomka.

Metoda to_s parametry nemá. Postačí nám proto zápis:

class Movable
  def to_s
    super + „, D = #{@direction}, V = #{@velocity}“
  endend

Otestujeme:

a = Movable.new(5, 6, 7, 8, 9)
puts a.to_s

A výsledkem je očekávaný text:

X = 5, Y = 6, M = 7kg, C = 1, V = 9, D = 8

Jak v Ruby vypadá volání metody v hierarchii tříd? S určitým zjednodušením bychom mohli pro začátek říct, že každý objekt je instancí nějaké třídy a třída je podtřídou jiné třídy. Dále v Ruby existuje společný předek všech tříd. Je jím třída Object. Pokud není v definici třídy uveden její předek, stává se automaticky přímým potomkem třídy Object.

V okamžiku, kdy voláme metodu objektu, zjišťuje interpret, zda je metoda definována ve třídě,jejíž je objekt instancí. Pokud ano, metoda se spustí. V opačném případě se metoda hledá u předka třídy objektu, předka předka třídy a tak dále až ke třídě Object.

Pokud ani třída Object metodu nedefinuje, spouští se metoda method_missing, která je ve třídě Object pro podobné případy připravena. Za normálních okolností vyvolá výjimku. Je však samozřejmě možné ji předefinovat a tím korektně ošetřit volání neexistující metody programem.

Nyní si můžeme procvičit mozek na následujícím příkladu. Předpokládejme, že v připravované hře přibývá objektů, začínáme v nich mít zmatek a potřebujeme, aby byly schopny nahlásit jméno své třídy. Ruby nám ušetří práci, protože potřebná metoda class je definována ve třídě Object a všechny ostatní třídy ji dědí. Přidejme název třídy do výpisu z metody to_s:

class Basic
  def to_s
    „#{class}: X = #{xpos}, Y = #{ypos}, „+
    „M = #{mass}kg, C = #{Basic.count}“
  end
end

Předefinovali jsme metodu existující třídy. Udělejme krátký test:

a = Basic.new(1, 2, 3)
b = Movable.new(5, 6, 7, 8, 9)
puts a.to_s
puts b.to_s

Výsledkem je:

Basic: X = 1, Y = 2, M = 3kg, C = 2
Movable: X = 5, Y = 6, M = 7kg, C = 2, D = 8, V = 9

Co nám tento příklad napovídá? Postupovali jsme takto:

  • nadefinovali jsem třídu Basic,
  • nadefinovali jsme třídu Movable jako potomka třídy Basic,
  • změnili jsme metodu třídy Basic.

V první řadě vidíme, že díky použití super nemusíme předefinovat metodu to_s ve třídě Movable, a přesto funguje tak, jak jsme zamýšleli. Zajímavější však je to, že i když název třídy a první tři proměnné vypisuje vždy metoda definovaná ve třídě Basic, instance třídy Movable zobrazují správný text.

Příčinou je výše popsané dynamické volání metod v Ruby. Protože třída Movable má vlastní metodu class, je volána právě tato metoda. Nyní je zřejmé, proč jsme na začátku kapitoly přepsali metodu to_s tak, aby využívala přístupových metod namísto přímého zobrazování hodnot proměnných. Pokud v některé z odvozených tříd změníme přístupové metody, bude jejich volání fungovat korektně i z metod definovaných předky.

Na výstupu z posledního příkladu stojí za zmínku ještě proměnná s počtem instancí třídy. Vytvořili jsem jednu instanci třídy Basic a jednu instanci třídy Movable. Zobrazovaná hodnota počtu instancí je 2. Proměnná třídy @@count tedy zůstává společná i pro potomky třídy, ve které byla definována (což není mezi programovacími jazyky úplně obvyklé chování). Oproti tomu metoda třídy Basic.count zůstává svázána s třídou Basic (jak ostatně napovídá její název).

Na závěr ještě kontrolní otázka: kam nám vlastně zmizela třída Robot? Čisté řešení v našem příkladu by pravděpodobně bylo nadefinovat ji jako potomka třídy Movable.

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ší

1 komentář

  1. Honza

    Lis 24, 2011 v 14:01

    Ahoj nemelo by to byt spis „attr_accessor“ místo „acoder_accessor“?

    Odpovědět

Napsat komentář

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