FIGYELEM! Ez a dokumentum kizárólag az ELTE IK hallgatók számára oktatási célra készült! Félkész munka, dolgozunk rajta! Terjeszteni, felhasználni máshol a szerzők engedélye nélkül tilos! |
|
Osztályok, objektumok és változók
Az eddig megtekintett példák alapján valószínűleg kétségek merülhetnek fel egy korábbi kijelentésünkkel kapcsolatban, miszerint a Ruby objektum-orientált nyelv. Nos, ebben a fejezetben igyekszünk ezt bebizonyítani. Megvizsgáljuk, hogyan hozunk létre osztályokat és objektumokat Ruby-ban, és megnézünk néhány megoldást, amelyekben a Ruby a legtöbb objektum-orientált nyelvnél erősebbnek bizonyul. Mindeközben megvalósítjuk a következő milliárdos termékünk, az Internet Enabled Jazz és Blue Grass zenegép egy részét.
Sok hónapos megfeszített munka után jól fizetett kutatóink és fejlesztőink rájöttek, hogy egy internetes zenegépnek dalokra van szüksége. Jó ötletnek tűnik hát egy dalokat reprezentáló Ruby osztály megvalósítása. Tudjuk, hogy egy igazi dalnak van címe (name), előadója (artist), és hossza (duration) (amely a dal hossza másodpercekben), szóval a programunkban szereplő dal-objektumoknak is biztosan lesznek ilyen jellemzői.
Kezdjük az egészet egy alap Dal (Song ) osztály létrehozásával, amely mindössze egy initialize inicializáló metódust (konstruktort) tartalmaz. (Emlékezzünk rá, hogy a Ruby nyelvén az osztályok nevei nagy, az eljárások nevei kis betűvel kezdődnek.)
class Song
|
|
def initialize(name, artist, duration)
|
|
@name = name
|
|
@artist = artist
|
|
@duration = duration
|
|
end
|
|
end
|
|
|
Az initialize a Ruby programok egy speciális eljárása. Egy új dal objektum létrehozásához a Song.new meghívásakor a Ruby létrehoz egy inicializálatlan objektumot, és ezután lefuttatja az objektum initialize metódusát, átadva neki a new -ban megadott minden paramétert. Ezáltal lehetőségünk van olyan kód írására, mely beállítja az objektumunk állapotát.
A dalokat reprezentáló Song osztályunk 3 paramétert kap. Ezek a paraméterek az eljáráson belül lokális változók módjára viselkednek. Ezért is kezdődik nevük kis kezdőbetűvel, a lokális változókra vonatkozó elnevezési megállapodáshoz híven.
Minden objektum a saját dalát reprezentálja, szükséges tehát hogy minden Song objektum a saját dalának nevét, előadóját, és hosszát tartalmazza. Ez az jelenti, hogy ezeket az értékeket példányváltozóként kell az osztályon belül tárolni. Ruby-ban egy példányváltozó egyszerűen egy név, bevezetve a "kukac" ("@") jellel. Példánkban a dal neve paraméter értékét a @name , az előadót az @artist , valamint a dal hosszát a @duration példányváltozó kapja.
Próbáljuk ki új osztályunkat!
aSong = Song.new("Bicylops","Fleck", 260)
|
|
aSong.inspect
|
» #<Song:0x401b4924 @duration=260, @artist="Fleck", @name="Bicylops">
|
|
|
|
Ez úgy tűnik, működik. Alapértelmezésként a bármely objektumnak elküldhető inspect üzenet visszaadja az objektum azonosítóját, valamint példányváltozóit. Úgy tűnik, helyesen állítottuk be őket.
Kísérletünk azt sugallja, hogy a fejlesztés során számos alkalommal lesz szükségünk egy Song objektum tartalmának kiiratására, és az inspect alapértelmezett formátuma hagy némi kívánnivalót maga után. Szerencsére a Ruby-ban létezik egy beépített üzenet, a to_s , amely egy objektum stringként való kiiratására szolgál.
aSong.to_s
|
» #<Song:0x401b499c>
|
|
Ez nem volt túl hasznos, csak az objektum azonosítóját kaptuk meg. Írjuk tehát felül a to_s üzenetet az osztályunkban. Amíg ezt megtesszük, ejtenünk kell néhány szót arról, hogyan mutatjuk be az osztály-definíciókat ebben a könyvben.
Ruby-ban az osztályok nem zártak: bármikor hozzáadhatunk új metódust egy létező osztályhoz. Ez igaz mind az általuk írott, mind a beépített osztályokra. Mindössze annyit kell tennünk, hogy megnyitjuk egy létező osztály definícióját, és az általunk meghatározott tartalom hozzáadódik, bármi is legyen ott.
Ez céljainknak pont megfelel. E fejezet folyamán, ahogy új metódusokat adunk hozzá osztályainkhoz, csak az osztály-definíciót mutatjuk meg az új eljárásoknak. Ennek csökkentjük az ismétlődéseket a példáinkban.
Adjuk hát hozzá a to_s metódust a Song osztályunkhoz!
class Song
|
|
def to_s
|
|
"Song: #{@name}--#{@artist} (#{@duration})"
|
|
end
|
|
end
|
|
aSong = Song.new("Bicylops", "Fleck", 260)
|
|
aSong.to_s
|
» "Song: Bicylops--Fleck (260)"
|
|
Kiváló, rendületlenül haladunk! Habár elsiklottunk valami érdekes fölött. Azt mondtuk, hogy a Ruby támogatja a to_s üzenetet minden objektumra, de nem mondtuk meg, hogyan. Nos, ennek köze van az öröklődéshez, alosztályokhoz, és ahhoz, ahogy a Ruby meghatározza, mely metódust kell végrehajtani egy objektumhoz küldött üzenet esetén. Ez a következő rész témája is.
Öröklődés és üzenetek
Az öröklődés lehetővé teszi olyan osztály létrehozását, amelyek más osztály újrahasznosítása, vagy specializációja. Például, a mi zenegépünk a dalokról alkotott elképzeléseinket a Song osztályba tömöríti. Aztán jön a marketing, és közli, hogy biztosítanunk kell a karaoke támogatást is. Egy karaoke dal pont olyan, mint egy normális dal (nincs benne ugyan ének, de ez minket nem zavar). Habár, tartalmazza a dalszövegek halmazát, valamint az időzítési információkat is. Egy dal lejátszásakor a dalszövegnek át kell úsznia a képernyőn a zenegép előtt a zenével összhangban.
Ennek a problémának egy megközelítése egy új KaraokeSong osztály definiálása, amely pont olyan, mint a régi Song osztály, kivéve, hogy tartalmaz egy dalszöveg sávot is.
class KaraokeSong < Song
|
|
def initialize(name, artist, duration, lyrics)
|
|
super(name, artist, duration)
|
|
@lyrics = lyrics
|
|
end
|
|
end
|
|
|
Az osztály-definícióban a "< Song" kifejezés mondja el a Ruby-nak azt, hogy a KaraokeSong a Song osztály alosztálya (azaz a Song osztály a KaraokeSong ősosztálya (superclass). Mások hívják ezt szülő-gyerek kapcsolat esetében úgy, hogy a Song a KaraokeSong szülő osztálya). Eleddig nem sokat foglalkoztunk az initialize metódussal, vele majd később, a super hívásnál foglalkozunk többet.
Most hozzuk létre a KaraokeSong osztályt és nézzük meg, működik-e (a végső változatban a dalszöveg egy önálló objektumban fog helyet foglalni időzítési információkkal együtt). Most osztályunk kipróbálásához stringet fogunk a dalszövegek tárolásához használni. Ez is egy kiváló tulajdonsága a nem típusos nyelveknek - semmit se kell előre definiálnunk, mielőtt elindítjuk kódunkat.
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
|
aSong.to_s
|
» "Song: My Way--Sinatra (225)"
|
|
Nos, működik, de miért nem mutatja a to_s a szöveget?
A válasz összefüggésben van a Ruby-nak azzal az elvével, amely alapján egy objektum üzenetfogadásakor kiválasztja a végrehajtandó metódust. Amikor a Ruby lefordítja az aSong.to_s metódushívást, nem tudja, pontosan hol található a to_s metódus. Inkább elhalasztja a döntést a program futásáig. Ekkor szemügyre veszi az aSong objektum osztályát. Ha az osztály tartalmaz egy, az üzenettel megegyező nevű eljárást, végrehajtja azt. Ellenkező esetben az osztály ősosztályában keresgél tovább, és így tovább "felfelé" az öröklődési láncon. Ha kifogy az ősosztályokból a megfelelő metódus megtalálása nélkül, egy speciális cselekedetet hajt végre ami normális esetben egy hiba kiváltását jelenti. (Valójában lehetőségünk van felderíteni ezt a hibaüzenetet, és futási időben szimulálhatunk hamis metódusokat. Ennek leírását lásd #Object.method_missing [ref-c-object.html] pont alatt.)
Térjünk vissza a példánkra. Elküldtük a to_s üzenetet aSong objektumnak, amely a KaraokeSong osztály példánya. A Ruby a KaraokeSong osztályban keresi a to_s metódust, de nem talál ilyet. Az interpreter ezután a KaraokeSong ősében, a Song osztályban keres ilyet. Itt meg is találja a to_s metódust, amit feljebb definiáltunk. Ezért nem írja ki a dalszöveget, ugyanis a Song osztály semmit sem tud a dalszövegekről.
Javítsuk ezt ki a KaraokeSong#to_s implementálásával. Több módja van ennek. Kezdjük egy rossz módszerrel. Kimásoljuk a Song osztály to_s metódusát, és hozzáadjuk a dalszöveget.
class KaraokeSong
|
|
# ...
|
|
def to_s
|
|
"KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]"
|
|
end
|
|
end
|
|
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
|
aSong.to_s
|
» "KS: My Way--Sinatra (225) [And now, the...]"
|
|
Helyesen jelenítjük meg a @lyrics példányváltozó értékét. Ehhez az alosztály közvetlenül hozzáfér az ősosztály példányváltozóihoz. Akkor miért rossz ez a megoldás?
A válasz a jó programozási stílusban, valamint a csoportokra bontásban rejlik. Az ősosztály belső állapotainak piszkálásával túl szorosan kötjük magunkat annak megvalósításához. Mondjuk, hogy megváltoztatjuk a Song osztály úgy, hogy a dal hosszát milliszekundumokban tároljuk. A KaraokeSong osztály hirtelen ijesztő értékeket jelez, például, hogy a "My Way" című szám 3750 percig tart.
Ezt a problémát úgy kerüljük el, hogy minden osztályt hagyjuk, hogy maga kezelje a belső állapotait. Amikor a KaraokeSong#to_s meghívásra kerül, hagyjuk, hogy meghívja saját ősének to_s metódusát a dal tulajdonságainak lekérdezéséhez. Ezután hozzáfűzi a dalszöveggel kapcsolatos információit, és már meg is adja az eredményt. A Ruby-ban itt a kulcsszó a super . Ha a super -t paraméter nélkül hívjuk meg, a Ruby üzenetet küld az objektum ősének, és felkéri azt annak megfelelő nevű metódusának meghívására, és átadja neki az aktuális metódusnál kapott paramétereket. Most már megírhatjuk az új, fejlettebb to_s metódust.
class KaraokeSong < Song
|
|
# Úgy formázzuk a magunk ízlésére, hogy hozzáfűzzük
|
|
# a dalszöveget az ősosztály#to_s értékéhez.
|
|
def to_s
|
|
super + " [#{@lyrics}]"
|
|
end
|
|
end
|
|
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
|
aSong.to_s
|
» "Song: My Way--Sinatra (225) [And now, the...]"
|
|
Explicit megmondtuk a Ruby-nak, hogy a KaraokeSong osztály a Song osztály alosztálya, de a Song osztálynak nem adtunk meg ősosztályt. Ha egy osztály definiálásakor nem adunk meg ősosztályt, akkor a Ruby az alapértelmezett Object osztályt határozza meg ősnek. Ez azt jelenti, hogy az Object osztály minden objektumnak őse, így annak metódusai a Ruby összes objektuma számára elérhetők. Igen, ezért. A to_s egyike a 35 példánymetódusnak az Object osztályban. A részletes listát lásd később.
Öröklődés és mixinek
Néhány objektum-orientált programozási nyelv (például a C++) támogatja a többszörös öröklődést, amikor egy osztálynak több mint egy közvetlen őse van, és ezek funkcionalitását örökli. Bár nagyon erős technika, de emellett veszélyessé válhat az öröklődési hierarchia bonyolódásával.
Más nyelvek (például a Java) csak az egyszeres öröklődést támogatják, azaz egy osztálynak csak 1 közvetlen őse lehet. Ez tisztább, és könnyebben implementálható megoldás, ám vannak hátulütői: valós életben a dolgok gyakran több forrásból örökölnek tulajdonságokat (egy labda például lehet pattogó dolog és gömbölyű dolog is).
A Ruby egy érdekes és erőteljes kompromisszumot kínál, az egyszeres öröklődés egyszerűségét és a többszörös öröklődés erőteljességét kihasználva. Egy Ruby osztálynak csak egy közvetlen őse lehet, szóval a Ruby egy egyszeres öröklődéses nyelv. Habár a Ruby osztályok felhasználhatják bármennyi mixin funkcióját (a mixin egyfajta részleges osztály-definíció). Ez egyfajta többszörösöröklődés-szerű képességgel ruházza fel a nyelvet a hátulütők mellőzésével. A mixinekről később részletesen beszélünk.
Itt az idő, hogy az objektumokról beszéljünk, mint például a Song osztály példányai.
Objektumok és attribútumok
Az általunk eddig létrehozott Song objektumoknak van egy belső állapota (amihez tartozik például a dal címe és előadója). Ez az állapot private-nek minősül az egyes objektumok számára. Azaz semmilyen objektum nem férhet hozzá más objektum példányváltozóihoz. Általában. ez jó dolog. Ez valósítja meg minden objektum belső állapotainak karbantartására vonatkozó kizárólagos felelősségét.
Habár egy teljesen titkosított objektum elég használhatatlan: létrehozhatod, de ez után már nem tehetsz vele semmit. Éppen ezért többnyire létrehozunk olyan metódusokat, amelyek lehetővé teszik az objektum belső állapotának elérését és módosítását. Így az külvilág már kapcsolatot tud teremteni az objektummal. Ezeket a kívülről látható jellemzőket hívjuk az objektum attribútumainak.
A Song objektumainknak először is szüksége van cím, előadó, és a hossz megjelenítésének képességére, hogy valami állapotjelzőt megjeleníthessünk (pl. hány másodperc van még a számból).
class Song
|
|
def name
|
|
@name
|
|
end
|
|
def artist
|
|
@artist
|
|
end
|
|
def duration
|
|
@duration
|
|
end
|
|
end
|
|
|
|
aSong = Song.new("Bicylops", "Fleck", 260)
|
|
aSong.artist
|
» "Fleck"
|
aSong.name
|
» "Bicylops"
|
aSong.duration
|
» 260
|
|
Itt definiáltunk a 3 példányváltozóhoz 3 hozzáférést biztosító metódust. Mivel ez egy általános igény, a Ruby egy kézenfekvő rövidítést nyújt ezek létrehozására: az attr_reader létrehozza nekünk ezeket a hozzáférést biztosító metódusokat.
class Song
|
|
attr_reader :name, :artist, :duration
|
|
end
|
|
|
|
aSong = Song.new("Bicylops", "Fleck", 260)
|
|
aSong.artist
|
» "Fleck"
|
aSong.name
|
» "Bicylops"
|
aSong.duration
|
» 260
|
|
Ez a példa valami újat mutat be. Az :artist egy kifejezés egy, az artist -hoz kötődő Symbol (szimbólum) objektumot ad vissza. Felfoghatjuk úgy is, hogy a :artist az artist változó nevét, míg az artist a változó értékét jelenti. Ebben a példában az elérhetőségi metódusain a name , artist és duration neveket kapták. A hozzájuk kapcsolódó példányváltozók (@name , @artist , @duration ) automatikusan létrejönnek. Ezek a hozzáférhetőségi metódusok megegyeznek az általunk korábban kézzel írott metódusokkal.
Módosítható attribútumok
Olykor szükségünk van arra, hogy egy objektum attribútumának értékét a kívülről is meg tudjuk változtatni. Például, tegyük fel, hogy egy szám létrehozásakor kezdetben megadott számhossz becsült érték (esetleg a CD-ről, vagy az MP3-ból kinyert adat). Igazából az első meghallgatáskor derül ki, milyen hosszú is a szám valójában, és ezt az új értéket visszarakjuk az adott Song objektumba.
A C++-hoz vagy Javához hasonló nyelvekben ezt beállító függvényekkel tennénk meg. Javában például:
class JavaSong {
|
// Java kód
|
private Duration myDuration;
|
|
public void setDuration(Duration newDuration) {
|
|
myDuration = newDuration;
|
|
}
|
|
}
|
|
|
|
s = new Song(....)
|
|
s.setDuration(length)
|
|
|
Ruby-ban, mint azt már korábban láttuk, egy objektum attribútumai ugyanúgy elérhetők, mint bármilyen más változó. Természetesnek tűnik hát, hogy értéket adhassunk ezeknek a változóknak, ha be szeretnénk állíttani egy attribútum értékét. A Legkisebb Meglepetés Elvét betartva, pontosan ezt tesszük Ruby-ban.
class Song
|
|
def duration=(newDuration)
|
|
@duration = newDuration
|
|
end
|
|
end
|
|
|
|
aSong = Song.new("Bicylops", "Fleck", 260)
|
|
aSong.duration
|
» 260
|
aSong.duration = 257
|
# új érték megadása
|
aSong.duration
|
» 257
|
|
Ez az értékadás ("aSong.duration = 257") meghívja az aSong objektum duration= metódusát, átadva neki a 257-et paraméterként. Valójában egy egyenlőségjellel végződő metódus definiálásával szimuláljuk az értékadást.
Ismét, a Ruby egy ügyes rövidítéssel teszi kényelmesebbé ezen beállító függvények létrehozását.
class Song
|
|
attr_writer :duration
|
|
end
|
|
aSong = Song.new("Bicylops", "Fleck", 260)
|
|
aSong.duration = 257
|
|
|
Virtuális attribútumok
Ezeknek az attribútumok elérését biztosító eljárásoknak nem kell pusztán borítónak lenniük egy objektum példányváltozó körül. Tegyük fel például, hogy szeretnénk a dal hosszát percekben és a perc törtrészében elérhetővé tenni az eddig használt másodpercek helyett.
class Song
|
|
def durationInMinutes
|
|
@duration/60.0
|
# force floating point
|
end
|
|
def durationInMinutes=(value)
|
|
@duration = (value*60).to_i
|
|
end
|
|
end
|
|
|
|
aSong = Song.new("Bicylops", "Fleck", 260)
|
|
aSong.durationInMinutes
|
» 4.333333333
|
aSong.durationInMinutes = 4.2
|
|
aSong.duration
|
» 252
|
|
Itt az attribútumként definiált metódusokat használtuk egy virtuális példányváltozó létrehozásához. A külvilág számára a durationInMinutes pontosan olyan attribútumnak tűnik, mint a többi. Tulajdonképp nem is létezik ide kapcsolódó példányváltozó.
Ez több, mint kíváncsiság. Bertrand Meyer Object-Oriented Software Construction című könyvében ezt (**************) Uniform Access Principle-nek nevezi. A példányváltozók és kiszámított értékek közti különbség elrejtésével megvédjük az osztály megvalósítását a külvilágtól. Szabadon változtathatjuk a dolgok működésének menetét az osztályunkat használó milliónyi kódsor felülvizsgálata nélkül. Ez pedig nagyon nagy előny.
Osztályváltozók és osztálymetódusok
Eddig minden általunk létrehozott osztály tartalmazott példányváltozót és -metódust: változók, amelyek az osztály konkrét példányával állnak összeköttetésben, és metódusok, amelyek ezekkel a változókkal dolgoznak. Néha magának az osztálynak is szüksége van saját állapotok tárolására. Erről szólnak az osztályváltozók.
Osztályváltozók
Egy osztályváltozón az osztály minden objektuma osztozik, valamint az osztálymetódusok is korlátozás nélkül hozzáférnek. Az osztálymetódusokról később ejtünk szót. Egy osztály egy adott osztályváltozóról mindössze egy másolattal rendelkezik, neveik pedig két kukac ("@@") jellel kezdődnek, például @@pelda . A globális változókkal és példányváltozókkal ellentétben az osztályváltozókat még felhasználásuk inicializálni kell. Gyakran ez az inicializálás egy egyszerű értékadás az osztálydefiníció törzsében.
Vegyük például a zenegépünket. Szeretnénk nyilvántartani, hogy az egyes számokat hányszor játszottuk le. Ez a számláló valószínűleg a Song objektum egy példányváltozója lehet. Amikor a dal lejátszásra kerül, a változó értékét megnöveljük egyel. Eddig rendben is van. De mi van akkor, ha azt is szeretnénk tudni, hogy hány számot játszottunk le összesen? Ezt megtehetjük úgy, hogy végigszaladunk az osztály összes objektumán, és összeadjuk az egyes lejátszási értékeket. Vagy megkockáztatjuk a keresztkommunikációt, és egy globális változóban az adott információt. Ehelyett keressünk egy jobb megoldást. Használjunk osztályváltozót!
class Song
|
|
@@plays = 0
|
|
def initialize(name, artist, duration)
|
|
@name = name
|
|
@artist = artist
|
|
@duration = duration
|
|
@plays = 0
|
|
end
|
|
def play
|
|
@plays += 1
|
|
@@plays += 1
|
|
"This song: #@plays plays. Total #@@plays plays."
|
|
end
|
|
end
|
|
|
Hibakeresési szempontokból a Song#play -t úgy írtuk meg, hogy egy olyan stringet adjon vissza, amely tartalmazza az, hányszor játszottuk le az adott számot, valamint azt is, hány számot játszottunk le összesen. Ezt könnyen ki is próbálhatjuk:
s1 = Song.new("Song1", "Artist1", 234) # test songs..
|
»
|
s2 = Song.new("Song2", "Artist2", 345)
|
|
s1.play
|
» "This song: 1 plays. Total 1 plays."
|
s2.play
|
» "This song: 1 plays. Total 2 plays."
|
s1.play
|
» "This song: 2 plays. Total 3 plays."
|
s1.play
|
» "This song: 3 plays. Total 4 plays."
|
|
Egy osztályváltozó private-nak minősül az osztály és objektumai számára. Ha szeretnénk őket elérhetővé tenni a külvilág számára, akkor elérési metódusokat kell írnunk hozzájuk. Ez a metódus lehet példánymetódus, vagy, a következő fejezetbe vezetve minket, egy osztálymetódus.
Osztálymetódusok
Egy osztálynak olykor-olykor szüksége van olyan metódusokra, amelyek nincsenek hozzákötve egyetlen objektumhoz sem.
Egy ilyen metódussal már találkoztunk. A new metódus létrehoz egy új Song objektumot, de ő maga nincs hozzákötve egyetlen konkrét dalhoz sem.
A Ruby könyvtárakat átfutva számos osztálymetódussal találkozhatunk. A File osztály több olyan metódust tartalmaz, amely megnyitatlan file-ok (és mint ilyenek, File objektummal sem rendelkeznek) manipulálására szolgálnak. Ha törölni szeretnénk egy file-t, csak meghívjuk a File.delete(...) metódust, átadva neki a törlendő file nevét.
File.delete("torolj_le_engem")
|
|
|
Az osztálymetódusokat definíciójuk alapján különböztetjük meg a példánymetódusoktól, ugyanis nevük az osztály nevével és egy ponttal kezdődik.
class T
|
|
def peldanyMetodus
|
# peldánymetódus
|
end
|
|
def T.osztalyMetodus
|
# osztálymetódus
|
end
|
|
end
|
|
|
A zenegépek használói a lejátszott számok darabszáma után fizetnek, és nem a percek után. Ez teszi a rövid számokat jobb befektetéssé. Mi lenne, ha le szeretnénk tiltani a hosszabb számokat a dalok listájáról (SongList ). Definiálhatnánk egy metódust a SongList osztályban, amely ellenőrzi, hogy egy szám túllépi-e a meghatározott korlátot, amelyet osztály-konstans használatával fogunk megadni. Egy osztály-konstans egy egyszerű konstans (nagy kezdőbetű!), amely az osztály törzsében inicializálódik.
class SongList
|
|
MaxTime = 5*60
|
# 5 perc
|
def SongList.isTooLong(aSong)
|
|
return aSong.duration > MaxTime
|
|
end
|
|
end
|
|
|
|
song1 = Song.new("Bicylops", "Fleck", 260)
|
|
SongList.isTooLong(song1)
|
» false |
song2 = Song.new("The Calling", "Santana", 468)
|
|
SongList.isTooLong(song2)
|
» true |
|
Egykék (Singleton) és más konstruktorok
Néha adódhat olyan eset, hogy szeretnénk felülírni a Ruby általános objektum létrehozó módját. Tekintsük például a zenegépünket. Mivel valószínűleg több zenegépünk lesz, szerte az országban, szeretnénk a karbantartást a lehető legegyszerűbbé tenni. A követelmény része, hogy naplót vezessünk mindenről, ami a zenegéppel történik: a lejátszott dalokat, a fogadott pénzt, a beleöntött folyadékokról, és így tovább. Mivel szeretnénk a hálózati sávszélességet a zene lejátszására fenntartani, a naplófájlokat helyben tároljuk. Eszerint szükségünk lesz egy, a naplózást kezelő osztályra. Habár zenegépenként egy napló objektumot szeretnénk vezetni, és szeretnénk ezt megosztani a többi objektum között.
Vezessük be az Egyke szerkezetet, amelynek dokumentációját lásd a Tervezési minták (Design Patterns) részben. Úgy írjuk meg az osztályt, hogy a naplózást végző objektumok létrehozásának egyetlen módja a Logger.create hívás legyen, és biztosítjuk, hogy egyszerre csak egy naplózási objektum létezzen.
class Logger
|
|
private_class_method :new
|
|
@@logger = nil
|
|
def Logger.create
|
|
@@logger = new unless @@logger
|
|
@@logger
|
|
end
|
|
end
|
|
|
A new metódus private-tá tételével elvesszük a lehetőséget a naplózási objektumok létrehozásánál a megszokott konstruktor használatától. Ehelyett a "Logger.create" osztálymetódust nyújtjuk. Ez egy @@logger osztályváltozót használ egy naplózási objektum referenciájának nyilvántartására, és visszaadja azt a példányt minden alkalommal, amikor meghívják ( Az egykék ilyen módon implementálása nem szál-biztos. Ha több szál futna, lehetséges lenne több naplózási objektum létrehozása. Szálbiztos használatra ajánljuk a Singleton osztályt, amelyet a Ruby tartalmaz). Ezt ellenőrizhetjük a metódus által visszaadott objektumok azonosítóinak vizsgálatával.
Logger.create.id
|
» 537766930
|
Logger.create.id
|
» 537766930
|
|
Osztálymetódusok pszeudo-konstruktorként való használatával könnyebbé tehetjük az osztályunk használóinak életét is. Egy triviális példaként tekintsük az átlagos poligont megvalósító Shape osztályt. A Shape osztály példányai a konstruktorban kapják a megkívánt oldalszámot, valamint az osztóvonalakat.
class Shape
|
|
def initialize(numSides, perimeter)
|
|
# ...
|
|
end
|
|
end
|
|
|
Habár, néhány év múlva ezt az osztályt különböző alkalmazások fogják használni, ahol a programozók név és oldalhossz használatával hoznak létre alakzatokat. Egyszerűen adjunk hozzá néhány osztálymetódust.
class Shape
|
|
def Shape.triangle(sideLength)
|
|
Shape.new(3, sideLength*3)
|
|
end
|
|
def Shape.square(sideLength)
|
|
Shape.new(4, sideLength*4)
|
|
end
|
|
end
|
|
|
|
|
Az osztálymetódusok használatának rengeteg érdekes és erőteljes módja van, de ezek bemutatásával nem lesz kész hamarabb a zenegépünk, szóval menjünk tovább.
Hozzáférés vezérlése (Access Control)(???)
Egy osztály felületének tervezésekor fontos meggondolnunk, hogy mekkora hozzáférést biztosítunk a külvilág számára. Túl sok hozzáférés lehetővé tételével megnő a csoportosodás kockázata az alkalmazásunkon belül; az osztályunk használói erős függési helyzetbe kerülnek annak megvalósításának részleteitől, sokkal inkább, mint a logikai felületétől. A jó hír, hogy Ruby-ban egy objektum állapotának megváltoztatása kizárólag annak egy metódusának hívásával történhet. Tartsd kézben a metódusok hozzáférhetőségét és a kezedben tartod az objektumot. Egy jó szabály: ne hagyjunk nyilvánosan olyan metódust, amely az objektumot érvénytelen állapotban hagyatja. A Ruby a védelem 3 módszerét kínálja:
- Publikus (public) metódusok mindenki által meghívhatók. Alapértelmezésként minden metódus publikus.
- A védett (protected) metódusok csak a definiáló osztály és azok alosztályai által hívhatók. A hozzáférés a családon belül marad.
- Privát (private) metódusok nem hívhatóak explicit fogadóval. Mivel használatukkor nem specifikálhatunk konkrét objektumot, ezeket a metódusokat csak a definiáló osztályon belül hívhatjuk, és örököltethetjük.
A protected és private közti különbség elég nehezen leírható, és a Ruby-ban különbözik ez a két fogalom a többi objektum-orientált nyelvben megszokottnál. Ha egy metódus protected, akkor az osztályához, vagy annak bármely alosztályához tartozó objektum meghívhatja. Ha egy metódus private, akkor csak azon objektum környezetéből hívható; még az ugyanazon osztályhoz tartozó objektumok private metódusainak közvetlen elérése sem lehetséges.
A Ruby abban is különbözik a több OO nyelvtől, hogy a hozzáférés vezérlése dinamikusan kerül meghatározásra, nem statikusan. Hozzáférési hibát csak akkor kapunk, ha a kód megpróbálja futtatni a megszorított metódust.
Hozzáférés vezérlésének specifikálása
Osztály- vagy moduldefiníción belül határozzuk meg az egyes metódusokhoz való hozzáférés szintjét a public , protected , private funkciók használatával. Mindhárom funkciót 2 különböző módon használhatjuk.
Argumentum nélkül használva meghatározzák az azt szekvenciálisan követő metódusok hozzáférési szintjét definiálják. Ez ismerős lehet a Java és C++ programozók számára, ahol a megfelelő kulcsszavak használatával érhetjük el ugyanezen hatást.
class MyClass
|
|
|
|
def method1
|
# alapértelmezés 'public'
|
#...
|
|
end
|
|
|
|
protected
|
# a következő metódusok 'protected'-ek lesznek
|
|
|
def method2
|
|
#...
|
|
end
|
|
|
|
private
|
# a következő metódusok 'private'-ek lesznek
|
|
|
def method3
|
|
#...
|
|
end
|
|
|
|
public
|
# a következő metódusok 'public'-ek lesznek
|
|
|
def method4
|
|
#...
|
|
end
|
|
end
|
|
|
Másfelől beállíthatjuk a hozzáférés szintjét az elnevezett metódusoknak úgy is, hogy argumentumként felsoroljuk őket a hozzáférést beállító funkcióknak.
class MyClass
|
|
def method1
|
|
end
|
|
|
|
# ... és így tovább
|
|
|
|
public :method1, :method4
|
|
protected :method2
|
|
private :method3
|
|
|
|
end
|
|
|
Egy osztály initialize metódusa automatikusan private-nek deklarálódnak.
Itt az ideje, hogy megtekintsünk néhány példát. Talán megpróbálhatnánk modellezni egy számlavezetési rendszert, ahol minden pénzlevételhez (debit) tartozik befizetés (credit). Mivel szeretnénk biztosítani, hogy senki nem törheti meg ezt a szabályt, a metódusokat úgy szerkesztjük meg, hogy az pénzlevételt és befizetést private-tá tesszük, és készítünk egy külső felületet tranzakciók számára.
class Accounts
|
|
|
|
private
|
|
|
|
def debit(account, amount)
|
|
account.balance -= amount
|
|
end
|
|
|
|
def credit(account, amount)
|
|
account.balance += amount
|
|
end
|
|
|
|
public
|
|
|
|
#...
|
|
def transferToSavings(amount)
|
|
debit(@checking, amount)
|
|
credit(@savings, amount)
|
|
end
|
|
#...
|
|
end
|
|
|
Protected hozzáférést használunk ha az objektumoknak el kell érniük az ugyanazon osztályhoz tartozó objektumok belső állapotát. Tegyük fel például, hogy engedélyezni szeretnénk az egyedülálló Account objektumoknak, hogy összehasonlítsák egyenlegeiket, de szeretnénk ezt elrejteni a külvilág elől (vagy más formában szeretnénk mutatni azt).
class Account
|
|
attr_reader :balance
|
# accessor method 'balance'
|
|
|
protected :balance
|
# and make it protected
|
|
|
def greaterBalanceThan(other)
|
|
return @balance > other.balance
|
|
end
|
|
end
|
|
|
Mivel a balance attribútum protected, csak az Account osztály objektumai érik el.
Változók
Most, hogy eljutottunk addig a pontig, hogy létrehozhatunk sok objektumot, figyeljünk arra, hogy ne veszítsük el őket. A változókat használjuk arra, hogy nyomonkövessük az objektumokat: minden változó egy objektumra vonatkozó referenciát tartalmaz. Bizonyítsuk ezt egy kis kóddal.
person = "Tim"
|
|
person.id
|
» 537771100
|
person.class
|
» String
|
person
|
» "Tim"
|
|
Az első sorban a Ruby létrehoz egy String objektumot a Tim értékkel. Egy erre az objektumra vonatkozó referenciát helyez el a person lokális változóban. Egy gyors ellenőrzéssel megbizonyosodhatunk arról, hogy a változó felvette egy string személyiségét.
Szóval a változó objektum?
Ruby-ban a válasz: Nem. A változó szimplán egy objektumra vonatkozó referencia. Az objektumok egy nagy medencében lebegnek (többnyire a heap-ben), és változók mutatják a helyüket.
Bonyolítsuk kicsit a példát.
person1 = "Tim"
|
|
person2 = person1
|
|
person1[0] = 'J'
|
|
|
|
person1
|
» "Jim"
|
person2
|
» "Jim"
|
|
Mi történt? Megváltoztattuk a person1 első karakterét, de mind a person1 , mind a person2 Tim-ről, Jim-re változott.
Ezzel visszajutottunk ahhoz a tényhez, hogy a változók egy objektumra vonatkozó referenciát tartalmaznak, és nem magát a objektumot. A person2=person1 értékadás nem hoz létre új objektumot, mindössze lemásolja a person1 objektum-referenciáját a person2 -be. Így mindkettő ugyanarra az objektumra mutat.
Az értékadás esetében csak egy újabb álneve lesz az objektumnak, nagy eséllyel több olyan változót eredményezve, melyek ugyanarra az objektumra mutatnak. De nem okozhat ez problémát? Dehogynem, bár nem olyan sűrűn, mint gondolnánk (Java-ban például az objektumok ugyanígy működnek). Jelen esetben például elkerülhetjük ezt az elnevezést a String osztály dup metódusának használatával elkerülhetjük. Ez a metódus egy új, a megadott String tulajdonságaival megegyező jellemzőkkel bíró objektumot hoz létre.
person1 = "Tim"
|
|
person2 = person1.dup
|
|
person1[0] = "J"
|
|
person1
|
» "Jim"
|
person2
|
» "Tim"
|
|
Lehetőségünk van továbbá megakadályozni az objektum megváltoztatását annak 'fagyasztásával' (a 'fagyasztásról' még később beszélünk). A fagyasztott objektumok megváltoztatásakor a Ruby egy TypeError kivételt vált ki.
person1 = "Tim"
|
|
person2 = person1
|
|
person1.freeze
|
# az objektum megváltoztatásának letiltása
|
person2[0] = "J"
|
|
|
lefuttatva ezt eredményezi:
prog.rb:4:in `[]=' can't modify frozen string (TypeError)
|
|
from prog.rb:4
|
|
|
|