Dědičnost
Dříve, než se pustíme do studia dalších tajů OOP v Ruby, zrevidujeme naposledy definici třídy Basic. Ponecháme jen základní funkčnost:
class Basic # definice třídy 'Basic', 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 attr_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=#{Basic.count}" # řetězcovou podobu end def Basic.count # metoda třídy @@count # vrací počet dosud vytvořených instancí end end
Povšimněte si použití konstrukce attr_accessor, která nahrazuje současné použití attr_reader a attr_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. 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. Pro pohyblivé objetky bychom potřebovali přidat nové vlastnosti: směr a rychlost. Jednou z možností je opět doplnit definici třídy Basic. Nepohyblivé objekty však další vlastnosti nepotřebují. Proto 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 existující definice třídy Basic a přidat nové vlastnosti k již jednou definovaným. Tento mechanismus se nazývá dědičnost.
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 attr_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 a navíc má své vlastní.
Terminologický slovníček z první kapitoly 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.
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}" end end
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 nakreslit následující schéma, které zachycuje to, že každý objekt je instancí nějaké třídy a třída je podtřídou jiné třídy:
.-----. .------. .-<|třída|----<|objekt| | '--.--' '------' podtřída | | '-----'
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 type 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 "#{type}: 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 type, 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. (Programátoři v C++ si určitě uvědomili, že všechny metody v Ruby lze označit jako virtuální.)
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. Oproti tomu metoda třídy Basic.count zůstává svázána s třídou Basic (jak ostatně napovídá její název).
Řízení přístupu k metodám
Proměnné definované v rámci třídy jsou pro okolní program skryté a je možné k nim přistupovat jen pomocí příslušných metod. Možná vás napadlo, zda je možné omezit také přístup k některým metodám. Ve skutečnosti jsme se s jednou metodou, k níž nelze mimo definici třídy přistupovat, již setkali:
a=Basic.new(1,2,3) a.initialize(4,5,6)
Spuštění skončí chybou:
private method 'initialize' called for #<basic:0xa03bcc0>(NameError)
Metoda initialize je označena jako private a pokus o její volání končí vznikem výjimky. Celkem mohou být metody v Ruby z hlediska řízení přístupu zařazeny do tří skupin:
- public,
- protected,
- private.
(Pozor, význam označení protected je v Ruby mírně odlišný od jazyků C++ nebo Java.)
Není-li stanoveno jinak, jsou nově definované metody automaticky označeny jako public. To umožňuje jejich volání jinými metodami téže třídy i jakýmkoliv kódem mimo třídu.
Metoda initialize je automaticky označena jako private. Tyto metody lze volat pouze v rámci definice třídy, ve které jsou definovány. Jinými slovy, můžeme říci, že je lze volat pouze bez určení objektu (tj. není možné použít volání ve formě objekt.metoda).
Metody označené jako protected tvoří střední cestu. Lze je volat v rámci definice stejné třídy. Navíc mohou být volány s určením objektu, ten musí však opět patřit do stejné třídy.
K přepínání přístupu k metodám existují v Ruby dva jednoduché způsoby:
class Dummy # defaultně jsou metody public # private # metody definované tady jsou private # protected # metody definované tady jsou protected # public # metody definované tady jsou opět public # # alternativně můžeme přístup k metodám nastavit hromadně public :method1, :method4 protected :method2 private :method3 end
Čas běží a investoři tlačí na urychlení vývoje naší robotické hry. Rozhodli jsme se proto zdokonalit definici třídy Movable. Nebudeme nyní nastavovat hodnoty směru a rychlosti přímo, ale vytvoříme metody pro zatáčení a akceleraci. Přímé nastavení hodnot by již nemělo být možné. Dále připravíme triviální metodu, která bude simulovat srážku objektu s jiným objektem. Prozatím se spokojíme s tím, že kolize náhodně změní směr a rychlost obou objektů:
class Movable # při doplnění definice nemusíme opakovat určení předka protected :direction=,:velocity= # nastavování hodnot bude protected def steer(s) # metoda pro změnu směru objektu @direction+=s # směr se změní o zadaný parametr end def accelerate(a) # metoda pro změnu rychlosti objektu @velocity+=a # rychlost se změní o zadaný parametr end def crash(o) # simulace srážky s jiným objektem direction=rand(direction) # vlastní rychlost a směr se náhodně velocity=rand(velocity) # změní o.direction=rand(o.direction) # a změní se i rychlost a směr objektu o.velocity=rand(o.velocity) # zadaného jako parametr end end
Doplněnou definici ihned vyzkoušíme:
a=Movable.new(3,3,10,10,5) puts a.to_s b=Movable.new(3,3,10,7,13) puts b.to_s a.crash(b) # objekt 'a' dostal zprávu, aby narazil do objektu 'b' puts a.to_s puts b.to_s a.steer(3) # objekt 'a' zatáčí puts a.to_s b.direction=7 # objekt 'b' se pokouší o přímé nastavení směru
Získáme výpis podobný tomuto:
Movable: X=3, Y=3, M=10kg, C=1, D=10, V=5 Movable: X=3, Y=3, M=10kg, C=2, D=7, V=13 Movable: X=3, Y=3, M=10kg, C=2, D=10, V=5 Movable: X=3, Y=3, M=10kg, C=2, D=6, V=1 Movable: X=3, Y=3, M=10kg, C=2, D=13, V=5 protected method 'direction=' called for #<movable:0xa03bca8>(NameError)
Zatímco objekty mohou navzájem volat své metody označené jako protected, volání z kódu mimo objekt skončilo výjimkou.
To je pro dnešek vše. Příště se podíváme na vlastnosti, které činí Ruby vhodným jazykem i pro implementaci složitějších objektově orientovaných programů. Řeč bude o řešení problému dědičnosti s více předky, o modularizaci programů a některých specialitách Ruby, které jsme pro jednoduchost zatím vynechali.