1.4.1.12. fejezet, Modulok és Mixinek

A Ruby osztály hierarchiája szerint egy ősosztálynak több leszármazottja van, és egy leszármazottnak egy ősosztálya. Az osztályok egymás metódusait és tulajdonságait szigorú szabályok szerint vehetik igénybe. Van olyan helyzet, amikor mégis jó lenne ezeket a szabályokat kibővíteni. Például egy kard lehet fegyver vagy antik kincs. Egy PC lehet számítógép, vagy egy formája egy befektetésnek (angolul). Ezt a kibővítést már gyakoroltuk az objektum metódus készítéssel, azonban ez más megoldás.

A modul és az osztály meghatározása igen közel állnak egymáshoz. A Module osztály közvetlen szülője a Class osztálynak. A Modul is tartalmazhat konstansokat, metódusokat és osztályokat.

module MyModule 
  GOODMOOD = "happy" 
  BADMOOD = "grumpy" 
  def greet 
    return "I'm #{GOODMOOD}. How are you?" 
  end 
end

Az osztálymetódusokhoz hasonlóan tartalmazhat modul metódust.

def MyModule.greet 
  return "I'm #{BADMOOD}. How are you?"
end

Azonban van két tulajdonság, amiben különböznek. Az osztálynak lehet példánya és szülője, Class osztálynak lehet példánya (objektumok), szülői hierarchia (superclass), gyermekei (subclasses), a moduloknál mindez nem lehetséges. És itt jön az első kérdés: ha nem hozható létre belőle példány, mire jó akkor? Erre válaszul két szó van: namespace és mixins. Ruby mixins módszere a több ősosztályos hierarchia előnyeinek kiaknázása. Nemsokára megvizsgáljuk közelebbről, de először nézzük meg mi az a névtér (namespace).

Modulok mint névterek

Lehet úgy értelmezni a modulokat, mint a konstansok, osztályok és metódusok egybefonódása. A modulon belül találkozhatnak a kód különböző részei, hasznosítva ugyanazt a névteret, amin belül láthatják egymást, de láthatatlanok a névtéren kívülről. A Ruby definiál néhány névteret, úgy mint a Kernel és a Math is. A Math modult tartalmazza a matematikai műveleteket, úgy mint a négyzetre emelés, négyzetgyökvonás, definiálja néhány konstanst, mint pl. a PI. A Kernel modul definiál metódusokat, amiket kívülről használunk, mint pl.: print, puts és gets.

Nézzük a kódot, amit korábban láttunk.

module 
  MyModule GOODMOOD = "happy" 
  BADMOOD = "grumpy" 
 
  def greet
    return "I'm #{GOODMOOD}. How are you?"
  end 
 
  def MyModule.greet 
    return "I'm #{BADMOOD}. How are you?" 
  end
end

A konstansok eléréséhez, mint az osztályoknál, használhatjuk a szkóp (::) operátort.

puts(MyModule::GOODMOOD)

Metódus eléréséhez használhatjuk az osztályoknál megszokott pont jelet

puts( MyModule.greet )

De hogyan érjük el a "példány" metódust? A modul ugyanis zárt névtér, ezért hibás a

puts( MyModule.greet )

használata. Ha modul helyett egy osztály lenne, előállíthatnánk belőle egy példányt a new metódussal, és máris elérhető lenne.Ez az a hely, ahol megjelenik a mixin

Beemelt modulok, vagy mixinek

Egy objektum a modul metódusait a beemeléssel érheti el.

include MyModule
puts ( greet )

Megjegyzem, hogy csak a példány metódusok érhetők így el, a modul metódusok (MyModule.greet) továbbra is a modul megnevezéssel. Ha egy osztály definíción belül emeljük be a modult, annak az objektum metódusai az osztály definíció részévé válik, és ezentúl hívható az osztályon belül, vagy az osztályból készült objektumon keresztül.

class MyClass
  include MyModule
 
  def sayHi
    puts( greet )
  end
end
ob = MyClass.new
ob.sayHi
puts(ob.greet)

Ez a többszöri öröklődés kiváltásának egyik módja a Ruby-ban. Visszatérve a fejezet elején említett hasonlóságra, nézzük hogyan definiálhatjuk a kardot, mint fegyvert és mint antik kincset a Ruby-ban.

module MagicThing 
  attr_accessor :power 
end 
 
module Treasure 
  attr_accessor :value 
  attr_accessor :owner 
end 
 
class Weapon 
  attr_accessor :deadliness 
end 
 
class Sword < Weapon 
  include Treasure 
  include MagicThing 
  attr_accessor :name 
end 

Elkészült a kard osztály, hozzunk belőle létre egy példányt, és töltsük fel adatokkal.

s = Sword.new
s.name = "Excalibur"
s.deadliness = "fatal"
s.value = 1000
s.owner = "Gribbit The Dragon"
s.power = "Glows when Orcs Appear"

Ezután a tulajdonságok kiiratása gyerekjáték már:

puts(s.name) 
puts(s.deadliness) 
puts(s.value) 
puts(s.owner) 
puts(s.power)

Megjegyzem a lokális változók kívülről nem látszanak.

x = 1 # local to this program
module Foo 
  x = 50 # local to module Foo 
 
  # This can be mixed in but the variable x won't then be visible 
  def no_bar 
    return x 
  end 
 
  def bar 
    @x = 1000 
	return @x 
  end 
 
  puts( "In Foo: x = #{x}" ) # Ez eléri a "module local" x -et (50)
end 
 
include Foo 
 
puts(x) # Lokális x = 1 
puts( no_bar ) # Hiba! Ez a metódus nem éri el a modul lokális változóját 
puts(bar)

A példány változók látszanak (mint a bar metódus mutatja), a lokálisak nem. Modul rendelkezhet saját lokális változóval, ami az object objektum alá fog tartozni.

module X 
  @instvar = "X's @instvar"
  def self.aaa
    puts(@instvar)
  end 
end 
 
X.aaa #=> "X‟s @instvar"

De a példány változók, amire a belső objektumok hivatkoznak, annak a modulnak a látókörébe tartoznak, ahova beemeltük a modult.

module X 
  @instvar = "X's @instvar" 
  def amethod 
    @instvar = 10 # creates @instvar in current scope 
	puts(@instvar) 
  end 
 
 
  def self.aaa 
    puts(@instvar) 
  end 
end 
 
include X 
 
X.aaa #=> X's @instvar 
puts( @instvar ) #=> nil 
amethod #=> 10 
puts( @instvar ) #=> 10 
@instvar = "hello world" 
puts( @instvar ) #=> "hello world"

Az osztályváltozók bekerülnek a modul hatókörébe, és ugyan úgy megváltoztathatók, mint a lokális változók.

Most tegyük külön fájlba a modulunkat.

X.rb

module X 
  @@classvar = "X's @@classvar" 
end 
 
puts "included module X"

classvars.rb
Itt figyeljünk fel a require_relative metódusra, amivel az aktuális könyvtárban található metódust érhetjük el.

@x = 1 # local to this program
 
require_relative 'X'
include X
 
puts(@@classvar)
@@classvar = "bye bye" 
puts( @@classvar )

Megnézhetjük az instance_variables metódussal, mit végeztünk:

p( X.instance_variables ) 
p( self.instance_variables )

Eredmények:

included module X
X's @@classvar
bye bye
[]
[:@x]

Egy szál példány változónk van, a main objektumban (@x).

Névütközések

Előfordulhat, hogy metórusok, változók, tulajdonságok azonos néven szerepelnek egy-egy modulban. Például:

Sad.rb

module Sad 
  def Sad.mood # module method 
    return "sad" 
  end 
 
  def expression # instance method 
    return "frowning" 
  end 
end

Happy.rb

module Happy 
  def Happy.mood # module method 
    return "happy" 
  end 
 
  def expression # instance method 
    return "smiling" 
  end 
end
class Person 
 
  require_relative 'Happy'
  require_relative 'Sad'
 
  include Happy 
  include Sad 
 
  attr_accessor :mood 
 
  def initialize
    @mood = Happy.mood
  end 
end
 
person = Person.new
puts(person.expression)

Itt az inicializálás módosítja a mood tulajdonságát az objektumnak, ami meghívja a mood_write metódust. Ezt mindkét modul tartalmazza. Az inicializálásnál konkrétan meghatároztuk, hogy a Happy modul mood string értékét kívánjuk beállítani, ezért ez nem ütközik semmivel. De mindkét modul tartalmazza az expression metódust, felmerül a kérdés, hogy melyiket használjuk amikor meghívjuk? Azt várnánk, hogy az utoljára definiált metódus lesz az, jelen esetben a Sad modul metódusa. És jól sejtettük, így is történt. Ha megfordítjuk az include sorrendjét, megváltozik a metódus, és a Happy expression-ja hívódik meg. A konfliktus kezelésre a megoldás az álnév, az alias használata.

Álnevek

Happy.rb

module Happy 
  def Happy.mood # module method 
    return "happy" 
  end 
 
  def expression # instance method 
    return "smiling" 
  end 
  alias happyexpression expression
end

Sad.rb

module Sad 
  def Sad.mood # module method 
    return "sad" 
  end 
 
  def expression # instance method 
    return "frowning" 
  end 
  alias sadexpression expression
end

Meghívásuk pedig egyszerű:

person = Person.new
puts(person.happyexpression)
puts(person.sadexpression)

Az eredmény pedig:

smiling
frowning

De vigyázzunk modulok és a mixin-ek használatával. Olyan nagyon elbonyolítható egy program, hogy az már szörnyű.

module MagicThing # module
  class MagicClass # class inside module
  end 
end
 
module Treasure # module 
end
 
module MetalThing 
  include MagicThing # mixin 
  class Attributes < MagicClass # subclasses class from mixin 
  end 
end
 
include MetalThing # mixin 
  class Weapon < MagicClass # subclass class from mixin 
  class WeaponAttributes < Attributes # subclass 
  end 
end
 
class Sword < Weapon # subclass 
  include Treasure # mixin 
  include MagicThing # mixin 
end

Modulok beemelése fájlból

Eddig egy fájlban tároltuk a modult és az osztályt, amibe beemeltük. Az utóbbi néhány mintában próbáltam becsempészni egy kis fájl kezelést. A szükséges metódusunk a

require( "testmod.rb" )

A kiterjesztés többnyire elhagyható, hacsak nem .rb kiterjesztésű a fájl. A require_relative az aktuális könyvtárban is keresi a megadott állományt. Ha külön könyvtárba helyezzük el a fájlokat, akkor használjuk a

$: << "C:/mydir"

metódust. A $: tömb tartalmazza a keresési útvonalakat.

A require mellett használható a load metódus is. Néhány különbség azért van a kettő közt. Például a load második paraméterként egy boolean változó értéket is fogad, ami arra szolgál ha igaz értéket adunk át, hogy lefuttassa a fájlban leírt programrészt mint egy névtelen modul. Ez a dinamikus programozáshoz tartozik, mivel generált kód is futtatható vele. A másik különbség, hogy a require csak egyszer tölti be a modult, akárhányszor is hivjuk meg, míg a load annyiszor, ahányszor kérjük.

Modulok és osztályok

Láttuk miről ismerjük meg a modult, most nézzük meg igazából mi is a Module class. Minden megnevezett osztály egy példánya a Module osztálynak. Azonban nem készíthetünk leszármaztatást egy modulból, habár Module osztályból leszármazott készíthető:

class X < Module 
end

De például a Class osztály egy leszármazottja a Module osztálynak.

Class 
Module #=> is the superclass of Class
Object #=> is the superclass of Module

A következő modulok vannak beépítve a Ruby értelmezőbe:

Comparable, Enumerable, FileTest, GC, Kernel, Math, ObjectSpace, Precision, Process, Signal

Comparable egy mixin modul, ami megengedi az osztályoknak, hogy implementálják az összehasonlítás metódusokat

Enumerable egy mixin modul a megszámlálásra. Ennek a beemelésével az osztálynak implementálnia kell a az each metódust.

FileTest egy modul ami fájl tesztelő funkciókat tartalmaz, amit a File osztály osztály-metódusaival érhetünk el.

GC modul egy felületet biztosít a Ruby számára a szemét gyűjtésre, a használaton kívüli objektumok által foglalt memória felszabadítására.

Kernel az Object osztály által beemelt modul, ami definiálja a Ruby 'built-in' metódusait.

Math modul alapvető trigonometriai és határozatlan (transcendental) függvényeket tartalmaz. Mindegyiknek van példány és modul metódusa, azonos definícióval és névvel.

ObjectSpace modul, aminek rutinjai a szemétgyűjtéssel együttműködnek, és megengedi a programozónak, hogy minden élő objektumon végiglépkedjen.

Precision egy mixin, konkrét valós számosztályokkal. Itt a 'precizitás' kifejezés a valós számhoz közelítés finomságát határozza meg (törtrész). Ez az osztály nem keverendő össze, és nem emelhető be olyanba, ami nem a Real része (másszóval nem emelhető be olyan osztályokba és leszármazottaikba, mint a Complex vagy Matrix)

Process a processzek manipulálására szolgáló modul. Összes metódusa modul-metódus.

Signal az a modul, ami jelzéseket küld a futó processzeknek. A listája és jelentése a jelzéseknek implementáció függő.

Hatáskörök

A dupla kettőspont a hatáskörben(scope) meghatározó szerepet játszik.Pl.:

TOPLEVELCONST="Top leve0"
module OuterMod
  module InnerMod
    class Class1
      ModConst="const0"
	  def Class1.aMethod
	    puts(::TOPLEVELCONST)
	  end
    end
  end
end
 
op=OuterMod::InnerMod::Class1.new
OuterMod::InnerMod::Class1::aMethod
p(op.class)

Ha szkóp jellel kezdünk egy kifejezést, az a legfelső szinten definiált konstanst jelenti.

Modul funkciók

Ha azt szeretnénk, hogy modul és példány metódusként is elérhető legyen a függvényünk, használhatjuk a module_function metódust, egy szimbólummal, ami a metódus nevét jelzi:

module MyModule 
  def sayHi 
    return "hi!"
  end 
 
  def sayGoodbye 
    return "Goodbye" 
  end 
 
  module_function :sayHi 
end
 
class MyClass
  include MyModule 
  def speak 
    puts(sayHi) 
	puts(sayGoodbye) 
  end 
end
 
ob = MyClass.new 
ob.speak 
puts(MyModule.sayHi)
puts(ob.sayHi)

Eredménye:

hi!
Goodbye
hi!

Objektumok bővítése

Egy objektumot metódussal bővíteni tudunk már, most mégis nézzük meg az extend metódus működését, ami modul függvényekkel bővíti az objektumot.

module A 
  def method_a 
    puts( 'hello from a' ) 
  end 
end 
 
class MyClass 
  def mymethod 
    puts( 'hello from mymethod of class MyClass' ) 
  end 
end 
 
ob = MyClass.new
ob.mymethod
ob.extend(A)
ob.method_a

Kimeneti értékek

hello from mymethod of class MyClass
hello from a

Egyszerre több metódussal is bővíthető egy objektum.

ob.extend(B, C)

Ha azonos nevű metódust talál, az új metódus lép az előző helyébe.

module A 
  def method_a 
    puts( 'hello from a' ) 
  end 
end 
 
class MyClass 
  def mymethod 
    puts( 'hello from mymethod of class MyClass' ) 
  end 
end 
 
module C 
  def mymethod 
    puts( 'hello from mymethod of module C' )
  end 
end
 
ob = MyClass.new
ob.mymethod
ob.extend(A)
ob.method_a
ob.extend(C)
ob.mymethod

Eredménye:

hello from mymethod of class MyClass
hello from a
hello from mymethod of module C

Egy objektum bővítése megtiltható a freeze metódusának meghívásával:

ob.freeze

ill. lekérdezhető az objektum lefagyasztottsága (frozen?)

if !(ob.frozen?) 
  ob.extend( D ) 
  ob.method_d 
 else
  puts( "Can't extend a frozen object" )
end