A Ruby egy objektumorientált programozási nyelv, azaz programjainak alapköve az osztály. A Ruby osztálykezelése sok szempontból eltér más objektumorientált nyelvektől. Számos olyan dolog hiányzik belőle, ami megvan a legtöbb nyelvben, azonban interpretált nyelv lévén olyan lehetőségeket is magában rejt, amelyek egy natív kódra fordító nyelvben (mint amilyen a C++), elképzelhetetlenek. A Ruby teljesen objektumorientált. Minden függvény egy osztály tagfüggvénye, még azok is, amelyeket látszólag osztályon kívül definiáltunk. Az osztályok olyan hierarchiába szerveződnek, amelynek a gyökere az Object osztály. Ez minden osztálynak őse. Minden függvény virtuális, tehát hiányoznak a nem virtuális tagfüggvények. Hiányzik a többszörös öröklődés, a konstans függvények, a tisztán virtuális függvények és szigorú értelemben a hozzáférhetőség sem szabályozható. További sajátosság, hogy a nyelvben minden objektum, még az osztályok is! Úgynevezett mixinek segítségével lehetőség van "visszafelé származtatásra", azaz előre megírhatunk egy leszármazást, amit tetszőleges ősosztályra alkalmazhatunk. A legérdekesebb tulajdonság azonban a reflexivitás: egy Ruby program "tud saját magáról", azaz ismeri a saját kódját, így azt futási időben meg is tudja változtatni. Lehetőség van osztályokhoz futási időben újabb függvényeket hozzávenni, sőt objektumszinten is lehet származtatni, azaz futási időben hozzá lehet adni egyetlen objektumhoz egy függvényt.
Alkossunk egy nagyon egyszerű osztályt. Tegyük fel, hogy egy olyan
vektorgrafikus programot kell írnunk, amely alkalmas téglalapok és
körök megjelenítésére. Ehhez szükségünk lesz a (kétdimenziós) pontokat
tároló Vector osztályra.
A jó
objektumorientált stílus elfedi a tagváltozókat, és lehetetlenné teszi
a elérésüket az osztályon (vagy még jobb esetben az objektumon)
kívülről. Ez biztosítja az felület és a megvalósítás szétválását. Ha
publikussá tesszük a változókat, akkor nem felügyelhetjük az írásukat,
így az objektumok könnyen kerülhetnek inkonzisztens állapotba. Egy
tagváltozó olvasása is problémás. Igaz, hogy ez az invariánst nem
ronthatja el, viszont ha úgy döntünk, hogy kivesszük az osztályból,
akkor az összes rá hivatkozó kód érvénytelen lesz, és az osztályon
végzett lokális módosítás a teljes program refaktorálását teheti
szükségessé. Gyakran van szükség azonban arra, hogy a változók értéket
közvetlen módon lekérdezzük. A tagváltozók lekérdezésére általában
lekérdező - vagy angolul getter - függvényeket szoktak írni, amelyek
semmi mást nem csinálnak azon kívül, hogy visszatérnek a tagváltozó
értékével. A lekérdező függvények írása azonban nagyon fárasztó és
unalmas feladat, ráadásul potenciális hibalehetőség. Ennek
kompenzálására a Ruby olyan metaprogramozási eszközöket tartalmaz,
amelyek elvégzik a programozó helyett ezt a favágó munkát. Készítsük el
a fent már megírt gettereket a Vector koordinátáihoz.
Metaprogram | Generált kód |
attr_reader :v | def v; @v; end |
attr_writer :v | def v=(value); @v=value; end |
attr_accessor :v | attr_reader :v; attr_writer :v |
attr_accessor :v, :w | attr_accessor :v; attr_accessor :w |
A Rubyban
a minden tagváltozó védett, és minden tagfüggvény nyilvános. A
függvények hozzáférhetőségének megváltoztatása van mód a metaprogramok
segítségével. Mivel azonban a tagváltozók hozzáférhetőségét még
metaprogramokkal sem tudjuk megváltoztatni, ezért a továbbiakban a
hozzáférhetőség csak a függvények hozzáférhetőségét jelenti. Jegyezzük
meg továbbá azt is, hogy a statikus függvények (lásd később)
hozzáférhetőségét sem lehet ezekkel a metaprogramokkal szabályozni.
A C++ nyelvben három hozzáférhetőségi szintet különböztetünk meg. A nyilvános (public) tagok bárhonnan hozzáférhetők. A privát
(private) tagok csak a saját osztályuk számára hozzáférhetők. Egy
osztály védett (protected) tagjai elérhetők az illető osztályból és
annak leszármazottaiból. A C++ csak az osztályszintű védelmet ismeri, objektumszintű védelem nincs. Egy adott osztály objektumai elérik egymás privát adattagjait is.
A Rubyban ugyanezek a kifejezések teljesen mást takarnak, ezért a két
koncepció összekeveredésének megelőzése érdekében a következő
terminológiát vezetjük be: a hagyományos (C++-ban megszokott)
hozzáférhetőségi szinteket a standard előtaggal látjuk el (standard
nyilvános, standard privát, standard védett), a Ruby hozzáférhetőségi
szintjeit pedig anélkül (nyilvános, privát, védett) nevezzük meg.
(Megjegyzés: ez konyhajelölés, máshol nem találkozni vele.)
A nyilvános függvények bárhonnan elérhetők, csakúgy,
mint a standard nyilvánosak. A privát és védett függvények az
osztályból és annak leszármazottaiból érhetők el. A privát függvényeket
az különbözteti meg a védettektől, hogy nem kaphatnak explicit fogadót,
azaz nem lehet őket megnevezett objektumokra meghívni. Még akkor sem,
ha ez az objektum a self.
Ebből az következik, hogy egy adott objektum privát függvényeit csak ő
tudja meghívni, ráadásul csak saját magára, mert ha nincs explicit
fogadó, akkor a fogadó implicite a self lesz. A védett függvények kaphatnak explicit fogadót, ezért azonosan működnek a standard védett függvényekkel.
Mielőtt mélyebben belemennénk a dologba, tekintsük át az idevágó
szintaktikát, hogy példákkal is illusztrálhassuk az előbb
elmondottakat. A hozzáférhetőség szabályozásának kétféle szintaktikája
van. Ha paraméterek nélkül írjuk le, akkor ugyanúgy működik, mint a
C++-ban. Minden utána következő függvény (a legközelebbi minősítőig
bezárólag) az adott hozzáférhetőségű lesz. Ha paramétert kapnak, akkor
a definiálás helyétől függetlenül állítódik be a láthatóság. Ekkor
használatuk hasonló az attr_reader használatához. A metaprogramok neve
értelemszerűen: public, private és protected.
A statikus adattagokat @@ prefixszel
kell ellátni. Definiálhatók bármelyik függvényben vagy magában az
osztályban is. Rájuk is vonatkozik az a szabály, hogy ha nagybetűvel
kezdődnek, akkor konstansok.
Osztályok neve után egy < jel után adható meg az osztály, melyből származtatni szeretnénk. Minden metódus virtuális metódusként viselkedik. Származtatásnál csak a metódusok öröklődnek, az adattagok NEM, mivel azok mindig objektumokhoz vannak kötve. Természetesen a metódusok láthatóságai is öröklődnek. Az adattagok öröklésének érzését tudjuk kelteni, ha a leszármazott konstruktorában meghívjuk az ős konstruktorát (feltéve, hogy az ős a konstruktorában inicializálta az adattagjait). Többszörös öröklődés nem megengedett a nyelvben, erre az úgynevezett mixin modulok használhatók.
A
függvények paramétereinek nincs explicit típusa. Honnan tudja előre az
interpreter, hogy milyen függvényeket lehet majd meghívni rá, amikor
átadjuk neki az aktuális paramétert? A válasz az, hogy sehonnan. Az,
hogy a paraméternek milyen függvényei vannak, akkor dől el, amikor
átadódik. Ez teljesen más filozófia, mint amit C++-ban megszokhattunk.
C++-ban előre meg kell mondanunk a fordítónak, hogy egy paraméternek mi
lesz a típusa, amikor átadódik. Innen tudja a fordító, hogy milyen
függvényeket lehet meghívni rá. Más típusú paramétert nem adhatunk a
függvénynek, még akkor sem, ha volna olyan nevű függvénye. Az
Ruby-interpreter viszont a hívás idejében vizsgálja meg, hogy létezik-e
a megfelelő nevű és paraméterszámú függvény, és ha létezik, akkor
engedi meghívni. A C++-ban típus szerinti, a Rubyban viszont név
szerinti ellenőrzés történik.
Az elnevezés James Whitcomb Riley "kacsatesztjére"
utal, mely szerint: "Ami úgy jár, mint egy kacsa, és úgy hápog, mint
egy kacsa, az egy kacsa." Tehát két azonos interfészű objektum
egymással helyettesíthető, még akkor is, ha nem a közös interfész
alapján hivatkozunk rájuk. Az alábbi kódrészlet szemlélteti a
jelenséget.
A Ruby programozási nyelv tisztán objektum-orientált programozási nyelv, ami alatt azt kell érteni, hogy maradéktalanul megvalósítja az objektum-orientált paradigmát. Ennek lényege, hogy a programunk objektumok halmaza, és az interakciójuk egymásnak küldött üzenetek formájában valósul meg.
Minden osztály a közös Object osztályból származik.
Továbbá a Ruby egy interpretált nyelv, azaz minden sor bevitel után végre is hajtódik. Ez lehetővé teszi, hogy egy osztálydefiníció lezárásával már dolgozhassunk az adott osztállyal.
A következő néhány szakasz ezek megvalósításával foglalkozik, így segíthet olyan problémák megoldásában, amikor "rejtélyesen" eltűnnek üzenetek, vagy nem a megfelelő osztály kapja meg az üzenetet. Ezek nagyon ritkán fordulhatnak elő, azonban ha értjük hogyan is "reprezentálódik" egy osztály, akkor könnyebben írhatunk hibátlan programokat.
Amikor befejezünk egy osztálydefiníciót, akkor az interpreter létre is hoz nekünk egy objektumot, amit a továbbiakban singleton osztálynak nevezünk. Megtévesztő lehet, de a singleton osztály lényegében egy objektum, aminek speciális adattagjai vannak, és tartalmazza az osztályra specifikus metódusok egy részét. Az, hogy pontosan melyik metódus kerül a singleton osztályba, az később derül ki. Tekintsük a következő ábrát: (Nem UML diagram)
Létrehoztunk egy gitár osztályt, aminek van egy Play() metódusa. A háttérben jelen van az Object singleton osztály, majd létrejön a Guitar singleton osztály. A super adattag az ősosztály singleton objektumára mutat. A klass pedig az objektum singleton osztályára. Mivel így létrejöttek a megfelelő objektumok, így értelemes a Guitar.new üzenet elküldése.
Példányosítunk egy lucille objektumot. Bár a példában nem szerepel, a Guitar objektum tartalmazza a saját attribútumait.
Mivel a klass és super mutatókon keresztül meghatározott az öröklési és asszociatív kapcsolat, ezért a lucille-nek küldött üzenetek végigkövethetők a "gyökérig". Például:
Amikor hozzáadunk egy osztálymetódust az osztálydefinícióhoz, akkor a fenti modellbe ezt nem
tudjuk beleilleszteni, mivel a singleton osztály az egyes objektumokon végezhető metódusokat
tartalmazza. Így létrejön egy úgynevezett metaosztály, ami az osztályszintű metódusokat és
adattagokat hivatott kezelni. Egészítsük ki az előbbi példát.
Tehát megjelenik a Guitar' metaosztály, amit virtuálisnak jelölünk. (A flags adattag kiegészül egy V értékkel. A külvilág számára az a legfontosabb különbség, hogy ezek az osztályok kívülről láthatatlanok. Nem jelennek meg olyan objektumlistákban, mint a Module#ancestor és ObjectSpace.each_object visszatérési értékei.)
Tehát, ha elküldjük a Guitar.strings() üzenetet, akkor a Guitar lesz a fogadó, ennek a klass referenciáját követve jutunk el a keresett metódusig.
Tehát a Guitar' a Guitar metaosztálya. De mivel a Guitar az Object leszármazottja, így a metaosztálya (Guitar') az Object metaosztályának (Object') leszármazottja lesz.
Rubyban lehetőség van objektumokból létrehozni új osztályokat, azaz objektumokból származtatni.
Példánkban két string objektumot hozunk létre, ezután egy névtelen osztályt hozunk létre az a Objektumból, majd ehhez az új osztályhoz hozzáadunk néhány új metódust.
Amikor egy osztály tartalmaz egy modult, akkor a modul példánymetódusai az osztály példánymetódusai lesznek. Majdnem olyan, mintha a modul az osztály ősosztálya lenne. Nem meglepő, hiszen pont így valósítják meg. Amikor include-olunk egy modult, a Ruby létrehoz egy névtelen proxy osztályt, ami a modulra hivatkozik. Majd beékeli a proxy osztályt, mint az include-oló osztály ősosztályát. A proxy osztály referenciákkal hivatkozik a modul adattagjaira és metódusaira. Ez fontos, mivel a modult beinclude-olhatjuk több osztályba és több különböző öröklési láncban is feltűnhet. Azonban hála a proxy osztálynak, ha módosítjuk a modult, akkor az minden érintett osztályban is módosul, mivel "fizikailag" csak egy modul van jelen. Ezt szemlélteti a következő ábra is. Ha több modult include-olunk, akkor azok az include-olás sorrendjében kerülnek be az öröklési láncba. Ha a modul szintén tartalmaz modulokat, akkor azok is proxy-osztályokként lesznek befűzve az öröklési láncba.
Amikor egy osztálymetódust hívunk meg, akkor a singleton osztálynak küldünk üzenetet. Tehát amikor elküldjük a String.new("gunby") üzenetet, akkor a fogadónak tudni kell, hogy létezik valahol egy String singleton osztály. Ezt úgy éri el, hogy amikor létrehozunk egy osztályt (legyen az akár beépített, akár felhasználói), akkor létrejön egy konstans referencia, ami a singleton osztályra mutat, ugyanolyan névvel, mint az osztály.
Éppen ezért mondhatjuk, hogy az osztálynevek konstansok, és a következetesség miatt kell minden osztály nevének nagybetűvel kezdődjön.
A tény, hogy az osztályok nevei konstansok, azt jelenti, hogy ugyanúgy tekinthetjük őket, mint bármilyen más Ruby objektumot: másolhatjuk őket, átadhatjuk metódusnak, kifejezésekben használhatjuk őket.
Így könnyen készíthető el egy Gyár (tervminta):
Sokszor emlegettük már, hogy a Rubyban minden objektum, illetve minden utasítás egy objektum része. De vajon mi van a legfelső szinten lévő parancsvégrehajtóval? Ha azt írjuk egy script elején, minden osztálydefiníción kívülre, hogy
Néha nagyon sok munka egy objektumot a megfelelő állapotba hozni, és nem szeretnénk, ha valaki megváltoztatná. Néha úgy kell átadni egy objektumot két objektum között, hogy az átvitelt egy harmadik fél által készített objektum végzi. Ekkor azt szeretnénk, hogy ne változzon meg az objektum. Ez azért nagy probléma, mert a Rubyban az osztályok soha nem zárulnak le, bármikor, bárhol módosíthatók.
A Ruby biztosít egy eszközt ennek megoldására. Bármelyik objektum lefagyasztható az Object#freeze metódussal. Egy lefagyasztott objektumon nem lehet változtatni, nem lehet az adattagjait módosítani. Ezen úgy lehet segíteni, hogy az objektumot lemásoljuk egy másik objektumba, de úgy, hogy a lefagyasztott adattagot nem másoljuk. Erre szolgál a dup metódus. Ha a clone metódussal másoljuk az objektumot, akkor a fagyasztás továbbra is megmarad, így az új objektumot sem tudjuk módosítani.