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! |
|
Modulok
A modulok metódusok, osztályok és konstansok csoportosításának eszközei. Két fő előnyük:
- A modulok névterekkel rendelkeznek, elkerülve a nevek ütközését.
- A modulok implementálják a mixin szerkezetet.
Névterek
Ahogy egyre nagyobb, és nagyobb Ruby programokat írunk, egy idő után észrevesszük, hogy újrafelhasználható kódokat is írunk - általánosan felhasználható rutinokat. Ezeket a kódrészleteket szeretnénk több file-ba elkülöníteni, hogy más Ruby programokban is felhasználhassuk.
Gyakran ezek a kódok osztályokba szerveződnek, szóval valószínűleg egy osztályt (vagy egy összefűggő osztályok halmazát) egy file-ba rendezünk.
Bár előfordulhat, hogy olyan dolgokat szeretnénk egy csoportba foglalni, melyek nem alkotnak osztályt.
Egy kezdeti megközelítés szerint mindezeket a dolgokat kipakoljuk egy file-ba, és egyszerűen betöltjük ezt a file-t abban a programban, amelyiknek szüksége van rá. Így működik a C nyelv. Bár ezzel van egy kis probléma. Mondjuk, hogy írunk egy pár trigonometrikus függvényt, mint például sin , cos , és így tovább. Elpakoljuk őket egy trig.rb file-ba. Eközben Sally a jó és rossz egy szimulációján dolgozik, és ő is ezzel a technikával egy action.rb file-ba pakolja saját rutinjait, köztük a beGood , és a sin metódusokat. Joe egy olyan programot szeretne írni, amellyel kiszámítja, hány angyal fér el egy tű hegyén, és betölti a trig.rb , és az action.rb file-okat. Mindkét file-ban található egy sin metódus. Rossz hír.
A megoldás: a modul mechanizmus. A modulok névtereket definiálnak, azaz egy olyan homokozót, ahol metódusaink és konstansaink szabadon játszhatnak, anélkül, hogy más metódusokkal és konstansokkal egymás lábára lépnének. A trigonometrikus függvények mennek egy modulba:
module Trig
|
|
PI = 3.141592654
|
|
def Trig.sin(x)
|
|
# ..
|
|
end
|
|
|
|
def Trig.cos(x)
|
|
# ..
|
|
end
|
|
end
|
|
|
és a jó és rossz cselekedetek mehetnek egy másikba:
module Action
|
|
VERY_BAD = 0
|
|
BAD = 1
|
|
def Action.sin(badness)
|
|
# ...
|
|
end
|
|
end
|
|
|
A modulok konstansai ugyanolyan nevet kapnak, mint az osztályok konstansai, azaz nagy kezdőbetűvel íródnak. A metódusok definíciói is hasonlónak tűnnek: ugyanúgy definiáljuk őket, mint az osztályoknál.
Ha egy harmadik program használni szeretné ezt a két modult, betöltheti ezt a két file-t (a Ruby require utasításával).
require "trig"
|
|
require "action"
|
|
|
|
y = Trig.sin(Trig::PI/4)
|
|
wrongdoing = Action.sin(Action::VERY_BAD)
|
|
|
Mint az osztálymetódusoknál, egy metódus hívásánál a neve elé kitesszük a modul nevét, és pontot rakunk közéjük. A konstansoknál ugyanígy teszünk, csak pont helyett két kettősponttal (::) választjuk el őket.
Mixinek
A moduloknak van még egy csodálatos felhasználási módja. Egy csapásmra megszüntetik a többszörös öröklődés iránti igényt, mégpedig a mixinek segítségével.
Az előző példában modulmetódusokat definiáltunk - olyan metódusok, melyek neve a modul nevével kezdődik. Ha erről az osztálymetódusok jutottak eszünkbe, akkor valószínüleg a következő gondolatunk mi történik, ha példány-metódusokat definiálok egy modulon belül? lesz. Jó kérdés. Egy modulnak nem lehetnek példányai, mert egy modul nem osztály. De egy osztály-definícióba beágyazhatunk modulokat. Ilyenkor a modul metódusai elérhetők lesznek, mint az osztály példány-metódusai. Összeolvasztottuk őket. Valójában a beolvasztott modulok ősosztályként működnek.
module Debug
|
|
def whoAmI?
|
|
#{self.type.name} (\##{self.id}): #{self.to_s}"
|
|
end
|
|
end
|
|
|
|
class Phonograph
|
|
include Debug
|
|
# ...
|
|
end
|
|
|
|
class EightTrack
|
|
include Debug
|
|
# ...
|
|
end
|
|
|
|
ph = Phonograph.new("West End Blues")
|
|
et = EightTrack.new("Surrealistic Pillow")
|
|
|
|
ph.whoAmI?
|
» "Phonograph (#537766170): West End Blues"
|
et.whoAmI?
|
» "EightTrack (#537765860): Surrealistic Pillow"
|
|
A Debug modul beágyazásával a Phonograph és az EightTrack is hozzáfér a whoAmI? példány-metódushoz.
Mielőtt továbbmennénk, meg kell említeni néhány dolgot az include utasítással kapcsolatban. Először is: semmi köze nincs a file-okhoz. A C programozó által használt használt #include preprocesszor egy file tartalmát illeszti be egy másikba a fordítás során. A Ruby include utasítása egyszerűen egy cimkézett modulra való hivatkozást hoz létre. Ha az a modul egy másik file-ban van, akkor először a require utasítással be kell húznunk azt a file-t. Másodszor: a Ruby include -ja nem másolja be a modul példány-metódusait az osztályba. Inkább létrehoz egy referenciát az osztályban a megadott modulra. Ha több osztály is használja a modult, mind ugyanarra a dologra hivatkozik. Ha a modulban található definíciót megváltoztatjuk - akár a program futása közben - minden osztály, amely a modult használja, alkalmazza a változtatásokat.
A mixinek elképesztően kézben tartható módon képesek az osztályokhoz új funkciókat hozzáadni. Habár az igazi erejük akkor mutatkozik meg, amikor a mixinben található kód kommunikálni kezd azzal a kóddal, amelyik használja. Vegyük a Ruby Comparable (összehasonlítható) mixinjét például. Ez a mixin az összehasonlító operátorokat (< ,<= ,== ,>= ,> ) valamint hasonló jellegű kiegészítő metódusokat tartalmaz (mint például a between? ). Ehhez a Comparable feltételezi, hogy az az osztály, amely használni akarja, definiálja az <=> operátort. Szóval, mint egy osztály írója, defináljuk az <=> operátort, beágyazzuk a Comparable modult, és már kaptunk is hat ingyen összehasonlító funkciót. Próbáljuk ezt ki a Song osztályunkkal, hogy a dalok legyenek összehasonlíthatók hosszuk alapján. Mindössze be kell ágyaznunk a Comparable modult, és implementáljuk az <=> operátort.
class Song
|
|
include Comparable
|
|
def <=>(other)
|
|
self.duration <=> other.duration
|
|
end
|
|
end
|
|
|
Leellenőrizhetjük az eredményt néhány dallal.
song1 = Song.new("My Way", "Sinatra", 225)
|
|
song2 = Song.new("Bicylops", "Fleck", 260)
|
|
|
|
song1 <=> song2
|
» -1
|
song1 < song2
|
» true
|
song1 == song1
|
» true
|
song1 > song2
|
» false
|
|
Emlékezzünk a Smalltalk inject függvényének implementációjára az Array osztályon belül. Akkor megígértük, hogy később általánosan alkalmazhatóvá tesszük. Elérkezett az idő! :)
module Inject
|
|
def inject(n)
|
|
each do |value|
|
|
n = yield(n, value)
|
|
end
|
|
n
|
|
end
|
|
|
|
def sum(initial = 0)
|
|
inject(initial) { |n, value| n + value }
|
|
end
|
|
|
|
def product(initial = 1)
|
|
inject(initial) { |n, value| n * value }
|
|
end
|
|
end
|
|
|
Teszteljük le ezt úgy, hogy beépített osztályokba ágyazzuk be.
class Array
|
|
include Inject
|
|
end
|
|
|
|
[ 1, 2, 3, 4, 5 ].sum
|
» 15
|
[ 1, 2, 3, 4, 5 ].product
|
» 120
|
|
|
class Range
|
|
include Inject
|
|
end
|
|
|
|
(1..5).sum
|
» 15
|
(1..5).product
|
» 120
|
('a'..'m').sum("Letters: ")
|
» "Letters: abcdefghijklm"
|
|
Egy még összetettebb példaként megtekinthetjük az Enumerable modult is.
Példány-változók mixinekben
A Ruby-val ismerkedő C++ programozók gyakran kérdezik tőlünk: Mi történik a példány-változókkal a mixinekben? A C++-ban kicsit trükköznünk kell, hogy a többszörös öröklődési hierarchiában a változókat kézben tartsuk. Hogy bánik el ezzel a Ruby?
Nos, ez egy érthető kérdés. Emlékszünk, hogyan működnek a Ruby példány-változói? Először is: egy @ karakterrel prefixelt változó létrehoz egy példány-változót az aktuális objektumban, azaz a self -ben.
Egy mixin számára ez azt jelenti, hogy a kliens osztályba ágyazandó mixin létrehozhat példány-változókat a kliens objektumban, és használhatja az attr -ot és társait a példányváltozók eléréséhez. Például:
module Notes
|
|
attr :concertA
|
|
def tuning(amt)
|
|
@concertA = 440.0 + amt
|
|
end
|
|
end
|
|
|
|
class Trumpet
|
|
include Notes
|
|
|
|
def initialize(tune)
|
|
tuning(tune)
|
|
puts "Instance method returns #{concertA}"
|
|
puts "Instance variable is #{@concertA}"
|
|
end
|
|
end
|
|
|
|
# A zongora kicsit hamis, szóval állítunk rajta.
|
|
Trumpet.new(-5.3)
|
|
|
Eredménye:
Instance method returns 434.7
|
|
Instance variable is 434.7
|
|
|
Nemcsak a mixinen belül definiált metódusokhoz férünk hozzá, hanem a szükséges példány-változókhoz is. Persze ez némi kockázattal jár, mondjuk ha különböző mixinek megegyező nevű példány-változót akarnak használni. Ez ütközéshez vezet.
module MajorScales
|
|
def majorNum
|
|
@numNotes = 7 if @numNotes.nil?
|
|
@numNotes # Return 7
|
|
end
|
|
end
|
|
|
|
|
|
module PentatonicScales
|
|
def pentaNum
|
|
@numNotes = 5 if @numNotes.nil?
|
|
@numNotes # Return 5?
|
|
end
|
|
end
|
|
|
|
|
|
class ScaleDemo
|
|
include MajorScales
|
|
include PentatonicScales
|
|
|
|
def initialize
|
|
puts majorNum # Elvileg 7
|
|
puts pentaNum # Elvileg 5
|
|
end
|
|
end
|
|
|
|
ScaleDemo.new
|
|
|
Eredménye:
Mindkét beágyazott modulban létezik egy @numnotes . Ez nem vezet futtatási hibához, bár valószínűleg az eredmény nem az lesz, amit a szerző elvár.
Legtöbbször a mixin modulok nem próbálják meg magukkal hozni a saját példány-adataikat - metódusokkal nyerik ki az adatot a kliens objektumból. De ha olyan mixin kell létrehoznunk, amelynek saját állapota kell, hogy legyen, akkor biztosítanunk kell azt, hogy a példány-változók egyedi neveket kapnak, hogy megkülönböztessük őket a rendszer többi mixinjeiben találhatóktól - például a modul nevét is használhatjuk a változó nevének részeként.
Iterátorok és az "Enumerable" modul
Valószínűleg már észrevettük, hogy a Ruby tárolókat megvalósító osztályai sok olyan műveletet támogatnak, amelyek ezekkel a gyűjteményekkel végeznek különböző akciókat (megfordítják, rendezik, és így tovább). Valószínűleg megfordul a fejünkben, hogy milyen jó is lenne, ha a mi osztályunk is tudna ilyen hasznos dolgokat.
Nos, a mixin varázslatos segítségének, és az Enumerable modulnak köszönhetően a mi osztályaink is könnyen szert tehetnek ilyen tulajdonságokra. Mindössze annyi a dolgunk, hogy írjunk egy iterátort, amely a tárolónk elemeit veszi szép sorban, és nevezzük el each -nek. Ezután már csak az Enumerable modult kell beágyaznunk, és az osztályunk már támogat is olyan hasznos dolgokat, mint a map , az include? , valamint a find_all? . Ha a táronk objektumai - az <=> metódus használatával - értelmes rendezési szemantikával is rendelkeznek, akkor még a min , a max , és a sort is elérhetővé válik.
Más file-ok beágyazása
Mivel a Ruby lehetővé teszi jó, modulokra bontható kód írását, gyakran azon kaphatjuk magunkat, hogy kis file-okat gyártunk, bennük nagyon zárt, független kóddal (például egy felület x -hez, egy y feladatot megoldó algoritmus). Általában ezeket a file-okat osztály, vagy modul könyvtárakba szervezzük.
Ezen file-ok birtokában szeretnénk őket az új programokban egyesíteni. Erre a Ruby-nak két utasítása van:
load "filename.rb"
|
|
|
|
require "filename"
|
|
|
A load metódus beágyazza a megnevezett Ruby file-t minden alkalommal, amikor a metódus futtatásra kerül, míg a require csak egyszer tölt be egy file-t. A require ezzel szemben más, fontos tulajdonsággal is rendelkezik: osztott bináris könyvtárak betöltésére is képes. Mindkét rutin elfogad relatív, és abszolút útvonalat. Egy relatí útvonal (vagy csak egy egyszerű file-név) esetén az aktuális betöltési útvonal összes könyvtárát átvizsgálja a file után. Ezt az útvonalat a $. változó tárolja.
Persze, a load vagy a require utasításokkal betöltött file-ok további file-okat tölthetnek be, azok még továbbiakat, és így tovább. Ami elsőre nem feltétlenül egyértelmű, az az, hogy a require egy futtatható utasítás. Szerepelhet elágazásban, vagy tartalmazhat épp létrejött string-et. A keresési utvonal is módosítható - akár futási időben. Csak adjuk a szükséges könyvtárat a "$.$ string-hez.
Mivel a load feltétel nélkül beágyazza a forrásfile-t, használhatjuk abban az esetben is, amikor szeretnénk egy file-t betölteni, mert az lehet, hogy megváltozott a program kezdete óta.
5.times do |i|
|
|
File.open("temp.rb","w") { |f|
|
|
f.puts "module Temp\ndef Temp.var() #{i}; end\nend"
|
|
}
|
|
load "temp.rb"
|
|
puts Temp.var
|
|
end
|
|
|
Eredménye:
|