Magie met Modules
Ruby is een zeer krachtige programmeertaal. In deze blogpost wil ik wat dieper duiken in Modules. We gaan kijken naar modules als namespaces, als mixins om code te delen en naar een wat geavanceerdere methode om mixins te gebruiken door het gedrag van objecten aan te passen.
Modules als namespaces
In Ruby worden modules gebruikt als namespaces. Alles dat binnen een module gedefinieerd wordt zit in die namespace. Dat is dus met methoden en constanten. Klassen en modules zijn “constanten”(1) en worden dus ook genamespaced. Deze kunnen benaderd worden met de dubbele-dubbelepunt notatie.
module Math FAKE_PI = 22.0 / 7.0 class World def self.hello "hello world 123" # (3) end end end Math::FAKE_PI # => 3.14285714285714 Math::World.hello # => "hello world 123"
Mixins
De manier om code herbruikbaar te maken in Ruby is door middel van modules(3). Modules zijn een krachtiger alternatief dan class inheritance(4). Laten we eerst eens bekijken hoe we methoden kunnen gebruiken. Modules worden meestal “ingemixt” in een klasse om te gebruiken. Dat kan met de methode include
module HelloWorld def hello "hello world" end end class World include HelloWorld end the_world = World.new the_world.hello # => "hello world"
Methodes gedefinieerd in een module worden bij een include dus beschikbaar als instance methods. Maar hoe werkt dit? Door include wordt er door Ruby een anonieme klasse (ook wel ghost class, singleton of eigenklass genoemd) gemaakt die de superklasse is van World. Deze anonieme klasse heeft een pointer naar de methoden van de module, zodat deze methoden door overerving beschikbaar zijn. Ruby zal deze anonieme klasse nooit direct tonen, maar in feite ziet het er zo uit, als we de anonieme klasse “X” zouden noemen:
class X < Object def hello "hello world" end end class World < X include HelloWorld end the_world = World.new the_world.hello # => "hello world"
De anonieme klasse erft over van de klasse waar de originele klasse van overerft (in dit geval dus Object, wat immers impliciet de superklasse is van alles). In feite bereik je hiermee “multiple inheritance”. Het is interessant om op te merken dat de methode hello niet echt in deze anonieme klasse zit, maar dat de anonieme klasse alleen naar de module HelloWorld blijft verwijzen. Als je de module later nog aanpast, zijn deze wijzigingen dus in alle klassen nog steeds beschikbaar.
Maar soms wil je class methods(5) toevoegen. Dit gaat met de methode extend:
module HelloWorld def hello "hello world" end end class World extend HelloWorld end World.hello # => "hello world"
Dit werkt vergelijkbaar met include, maar deze zal op het object zelf gelden waar het op geëxtend wordt. Het object waarin dat gebeurt is World, een object van de klasse Class. De klasse zelf wordt dus uitgebreid met een anonieme klasse. Als we deze “Y” noemen, ziet dat er als volgt uit:
module HelloWorld def hello "hello world" end end class Y < Class include HelloWorld end World = Y.new # een nieuwe instantie van Class is een klasse World.hello # => "hello world"
Als dit nieuw voor je is, is dit behoorlijk pittig om door te krijgen. Onderaan deze post staan een aantal links, als je meer wilt lezen over hoe modules in elkaar te zitten. Om verder te gaan met deze blogpost is het verder voornamelijk belangrijk om te weten dat je met include instance methods toevoegt en met extend class methods toevoegt.
Hook methods
Als we aan een klasse zowel class methods als instance methods willen toevoegen zouden we zowel include en extend moeten gebruiken. Dat zou dan als volgt gaan:
module HelloWorld module ClassMethods def hello "hello from every world" end end def goodbye "goodbye from this particular world" end end class World include HelloWorld extend HelloWorld::ClassMethods end World.hello # => "hello from every world" World.new.goodbye # => "goodbye from this particular world"
Dit werkt prima, maar is wat vervelend. Er zijn twee regels nodig om alle functionaliteit van de module HelloWorld te krijgen. Dit kan makkelijk vergeten worden als iemand anders deze module gebruikt. Gelukkig biedt Ruby handige “hook methods”.
Voor include is er de methode included. Vanuit deze methode zouden we de extend kunnen uitvoeren zodat dit niet meer in onze klasse gedaan hoeft te worden.
module HelloWorld def self.included(klasse) klasse.extend(ClassMethods) end module ClassMethods def hello "hello from every world" end end def goodbye "goodbye from this particular world" end end class World include HelloWorld end World.hello # => "hello from every world" World.new.goodbye # => "goodbye from this particular world"
Er bestaat ook de methode extended, die vergelijkbaar werkt voor de methode extend.
Macros
Na deze enorme berg aan theorie wordt het hoog tijd om eens naar wat praktische toepassingen te gaan kijken. Van Ruby on Rails kennen we de declaratieve methodes om eigenschappen van een klasse te definiëren. Iedereen die weleens naar Ruby on Rails heeft gekeken heeft weleens zulke code gezien:
class Post < ActiveRecord::Base belongs_to :author has_many :comments validates_presence_of :title end
De methoden belongs_to, has_many en validates_presence_of worden ook wel macro’s genoemd. Dit is niks meer dan het aanroepen dan class methods, die in dit geval bestaan omdat Post inherit van de klasse ActiveRecord::Base. Dit zou er zo uit kunnen zien:
class ActiveRecord::Base def self.belongs_to(model_name, options = {}) # ... end end
Dit kunnen we ook met modules. Stel dat we read only velden willen definiëren.
module ReadonlyFields def self.included(klasse) klasse.extend(ClassMethods) end module ClassMethods attr_accessor :_readonly_fields def readonly_fields(*fields) self._readonly_fields = fields end end end class Post < ActiveRecord::Base include ReadonlyFields readonly_fields :title, :author_id end
De asterisk in de parameterlijst van de methode readonly_fields geeft aan dat alle argumenten in een array gestopt worden. Deze array wordt opgeslagen in de klasse (Post dus). Maar goed, ze zijn nu dus nu geregistreerd, maar laten we nu de implementatie als voorbeeld er nog eens bij geven.
We moeten het model een error laten geven als een van deze velden veranderen. ActiveRecord houdt voor ons bij wat de verschillen zijn, dus kunnen we makkelijk zien of de waarden veranderd zijn. ActiveRecord heeft een methode genaamd changes die een hash met veranderde veldnamen en de daadwerkelijke verandering teruggeeft. We zijn alleen geinteresseerd in de keys, de veldnamen.
We registreren tijdens de include een ActiveRecord hook method, genaamd validate_on_update waarmee extra methodes toegevoegd kunnen worden zodra het object opgeslagen wordt. Ik gebruik validate_on_update omdat de waarde in eerste instantie wel opgeslagen moet kunnen worden.
module ReadonlyFields def self.included(klasse) klasse.extend(ClassMethods) klasse._readonly_fields = [] klasse.validate_on_update :readonly_fields_cannot_change end module ClassMethods attr_accessor :_readonly_fields def readonly_fields(*fields) self._readonly_fields = fields end end def readonly_fields_cannot_change changes.each_key do |field| if self.class._readonly_fields.include? field.to_sym errors.add(field, :readonly) end end end end
Natuurlijk is er nog wat te verbeteren in deze code, maar voor deze demonstratie laat het aardig zien wat er mogelijk is.
Afronding
We gaan het nog even afmaken. Eerst willen we dat we niet in elk model de ReadonlyFields-module hoeven toe te voegen. Dit kan door de module te includen in ActiveRecord::Base zelf. Zet daarom deze code in een initializer van Rails, zodat deze uitgevoerd wordt zodra Rails start.
ActiveRecord::Base.instance_eval { include ReadonlyFields }
Helaas is include een private method. Dat is te omzeilen door bijvoorbeeld instance_eval of send te gebruiken. Hoe dit werkt valt buiten de scope van deze blogpost.
Laten we nog wat tests schrijven, in RSpec wel te verstaan:
describe ReadonlyFields do before do @post = Post.create!(:title => "foo") end it "should not allow updating the title" do @post.title = "bar" @post.should have(1).error_on(:title) end it "should allow updating without changing the title" do @post.title = "foo" @post.should have(:no).errors_on(:title) end end
Conclusie
Modules in Ruby zijn erg krachtig. Ze maken het erg gemakkelijk code te delen tussen verschillende klassen en objecten. Modules kunnen veel meer dan alleen methoden toevoegen en dingen in een namespace plaatsen, maar kunnen ook op zeer essentiele wijze de werking van een klasse beinvloeden. Het is dus van groot belang dat je ze goed test.
Gooi brokken code dus eens in een module, verwerk ze tot gem of plugin en deel ze eens met de wereld en/of met je collega’s. Deel eens wat vaak voorkomende validaties bijvoorbeeld. Go wild!
Meer informatie
- Ruby Metaprogramming screencasts by Dave Thomas
Betaalde screencasts, maar zeer de moeite waard als je meer wilt weten over Modules en al het andere metaprogrameerwerk. - The Decorator Delegator Disco
Blogpost over geavanceerd gebruik van modules voor het aanpassen van Ruby core methodes. - The Secret Life of Singletons
Hoe anonieme klassen werken in Ruby - RDoc: Module
De technische documentatie van Modules - How Ruby Mixins Work With Inheritance
Een aantal interessante inzichten en gotcha’s van mixins.
Footnotes
- Niks is ooit echt constant in Ruby.
- In Ruby wordt het resultaat van het laatste statement als return waarde gebruikt. Explicite returns zijn dus zelden nodig.
- Het is netter om inheritance te gebruiken voor dingen die ook echt een subsoort zijn van de superklasse. Een mens is een subsoort van zoogdier dus daar zou inheritance gerechtvaardigd zijn. Maar als er verder geen reden is om zoogdieren te instantieren, en je het alleen introduceert om functionaliteit te delen met andere zoogdieren, wordt dat in Ruby als slecht design ervaren. In Ruby zijn er geen abstracte klassen.
- Voor de niet-rubyisten: extends is in Ruby het kleiner-dan-teken. Je kan in Ruby modules en klassen ook echt met elkaar vergelijken. class A < B; end geeft inderdaad aan dat A < B en B > A en natuurlijk A == A.
- Class methods heten in Java static methods.

Really good work about this website was done. Keep trying more – thanks!
Yahoouj February 23, 2010 2:02
[...] Veel mogelijkheden om code te herbruiken (zie bijv. mijn post over modules) [...]
Qu’est-ce que c’est Rubyesque? | Finalist Developers Blog April 12, 2010 11:06
[...] Als je niet precies weet wat mixin’s in Ruby zijn, lees er dan meer over in de blogpost van Iain Hecker over mixins: blog.finalist.nl/2009/07/13/magie-met-modules/#more-852 [...]
DSL’s en Ruby | Finalist Developers Blog July 23, 2010 9:58