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:

  1. A modulok névterekkel rendelkeznek, elkerülve a nevek ütközését.
  2. 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:

7
7

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:

0
1
2
3
4

< Előző oldalKövetkező oldal >