DSL’s en Ruby
Om maar meteen met de deur in huis te vallen: DSL’s icm. met Ruby zijn gaaf, cool, leuk en leerzaam. Waarom? Omdat het het kán maar vooral ook omdat het je code leesbaarder maakt en je dwingt meer na te denken over de kwaliteit van je code (kwaliteit als in: onderhoudbaarheid / leesbaarheid).
DSL?
Even een klein stapje terug; wat was een DSL ook alweer? DSL staat
voor Domain Specific Language. Zoals de Engelse wikipedia ons
uitlegt:
"In software development and domain engineering, a domain-specific language (DSL) is a programming language or specification language dedicated to a particular problem domain, a particular problem representation technique, and/or a particular solution technique."
Kort gezegd: je kunt een DSL zien als een (al dan niet custom)
programmeertaal/dialect om een specifiek probleem op te lossen.
DSL’s in de praktijk
DSL’s kom je vaak tegen in Ruby (en Rails). Een mooi voorbeeld is
bijvoorbeeld de route configuratie in Rails:
ActionController::Routing::Routes.draw do resources :appointments resources :companies resources :accounts do member do get :verify_code end end end
Alsof je een kaart uittekent met daarop de locaties van de te vinden bronnen
(resources). Of, wat ik persoonlijk een van de mooiste voorbeelden vind van
het gebruik van DSL’s in Ruby:
describe User do context "personal profile" do it "should be able to tell how old an user is" do Delorean.time_travel_to "2010-07-01" u = Factory(:user) u.birthdate = "1982-01-01".to_date u.age.should == 28 end end end
Bovenstaand voorbeeld is een Rspec test icm. de Delorean gem (welbekend van
de film Back to the future). De Delorean brengt je terug in de tijd en de
should == 28 geeft aan dat de leeftijd van de user gelijk zou
moeten zijn aan 28.
Wanneer DSL’s in te zetten
Wanneer een DSL op te tuigen in Ruby is volledig afhankelijk van een aantal aspecten:
- Is er sprake van code duplicatie? (als in: hevige code duplicatie, dus niet iets wat maar twee keer gebruikt gaat worden)
- Voegt het wat toe aan de leesbaarheid van je code?
- Is na het toepassen van een DSL sprake van een duidelijke verbetering?
Denk in ieder geval goed na voor je een DSL opzet:
- Bedenk goed welk probleem je nu precies wilt oplossen (is er Überhaupt een probleem?)
- Bedenk goed hoe je het zou willen schrijven. Dus in plaats van domweg je code te kloppen ga je nadenken hoe je het probleem idealiter opgelost zou willen zien (zie het als een mooi gedicht). Waarom zou je anders een DSL schrijven?
- Zorg ervoor dat de DSL self explaining is. Denk daarbij eens aan je
collega’s die de code in de toekomst moeten gaan onderhouden
Case
Hoe een dergelijke DSL in Ruby op te tuigen? In deze case heb ik een goed
voorbeeld van een simpel probleem uit de praktijk. Deze situatie deed zich
toevallig van de week voor bij een klant.
Stel je hebt een stuk HTML wat steeds weer terugkomt:
<div class="block-group">
<div class="block first gray">
<h2>Titel</h2>
<p>Paragraaf</p>
</div>
<div class="block">
<h2>Titel</h2>
<p>Paragraaf</p>
</div>
<div class="block last gray">
<h2>Titel</h2>
<p>Paragraaf</p>
</div>
</div>Of beter nog, in HAML:
.block-group
.block.first.gray
%h2 Titel
%p Paragraaf
.block
%h2 Titel
%p Paragraaf
.block.last.gray
%h2 Titel
%p Paragraaf
De <div class="block"> stukken moeten een class
"first" of "last" meekrijgen afhankelijk van de positie
binnen de "block-group" (dit moest helaas omdat we ook IE6 moeten
ondersteunen). Je kunt ervoor kiezen dit telkens in je template uit te
schrijven maar dat is niet heel erg DRY. Als developer wil ik mij hier geen
zorgen over hoeven maken. Daarnaast: andere developers hebben het een stuk
minder op HTML/HAML en willen als het ware kant-en-klare componenten uit de
kast trekken om te gebruiken in hun templates.
In deze situatie kwam een DSL erg van pas. Stel nou dat je dit kon
schrijven:
- blocks do |b|
- b.block :class => 'gray' do
%h2 Titel
%p Paragraaf
- b.block do
%h2 Titel
%p Paragraaf
- b.block do
%h2 Titel
%p ParagraafHoe krijg iets dergelijks voor elkaar? Ruby magic!
&block en yield
Allereerst, de &block‘s. Een block is niet meer dan een een stuk
code dat doorgegeven kan worden aan een methode die het vervolgens op
willekeurige momenten kan uitvoeren.
Voorbeeld:
def foo puts "Hello world" yield end foo do puts "and you too!" end
Levert op:
Hello world and you too!
Maar wat als je het stuk code weer wilt doorgeven aan een andere methode?
Dan komt &block erg van pas:
Voorbeeld:
def foo(&block) puts "Hello world" bar &block end def bar yield end foo do puts "and you too!" end
Levert op:
Hello world and you too!
De ampersand voor block houdt in dat je de meegegeven code als het ware in een variabele stopt (deze hoeft dus noodzakelijkerwijs niet block te heten). Doordat de code nu als object beschikbaar is kun je dit dus ook doorgeven aan andere methodes.
Nog mooier is het om ook argumenten mee te kunnen geven aan een anoniem
block code. Zie het als een anonieme functie waaraan je arguments kunt
meegeven:
def foo name = "me" puts "Hello world" yield name end foo do |who| puts "And #{who} too!" end
Levert op:
Hello world and me too!
Met “yield name” geven we dus een argument mee (de variabele
“name”) aan het anonieme stuk code. Om dit voorbeeld een beetje
te vergelijken / te verduidelijken, hier een vergelijkbaar stuk code
geschreven in javascript:
function foo(otherFunction) { var name = 'me'; alert('Hello world'); otherFunction(name); } foo(function(who) { alert('and ' + who + ' too!'); });
Een &block kun je dus enigzins zien als een anonieme functie zoals je
die in javascript ook hebt.
capture
Weer even terug naar de case die ik eerder beschreef.
In de situatie die ik eerst schetste met "blocks do" betekent dit
dat alle HAML code onder "blocks do" dus op elk moment uitgevoerd
kan worden wanneer ons dat uitkomt. Echter, in het bovenstaande voorbeeld
willen we de HTML uit de template terughebben. Hiervoor heeft Rails
gelukkig een mooie methode bedacht, genaamd capture. Hieraan kun
je het stuk code als het ware doorpasen die het vervolgens weer omzet naar
HTML:
Stel we voegen het onderstaande toe in een Rails applicatie in de
application_helper (app/helpers/application_helper.rb):
def blocks(&block) content_tag(:div, capture(&block), { :class => 'foo' }, false) end
En het onderstaande in een HAML template:
- blocks do %h2 Test %p Hallo
Levert uiteindelijk op (na het parsen van de HAML):
<div class="foo"> <h2>Test</h2> <p>Hallo</p> </div>
In het bovenstaande voorbeeld krijgen we dan dus alle HTML terug uit het
meegegeven &block. Maar hoe bereiken we:
- blocks do |b| - b.block do
Door gebruik te maken van een klasse waarin we alle content van de
block‘s verzamelen:
class BlocksBuilder def initialize(view) @view = view @blocks = [] end def block(&block) @blocks < < content_tag(:div, capture(&block), { :class => 'block' }, false) end def to_s content_tag(:div, @blocks.join, { :class => 'block-group' }, false) end end
Je zou dus nu kunnen schrijven:
- block_builder = BlocksBuilder.new - block_builder.block do %h2 Titel %p Paragraaf - block_builder.block do %h2 Titel %p Paragraaf - block_builder.block do %h2 Titel %p Paragraaf = block_builder.to_s
Nog niet erg proper, dus dat kan een stuk mooier. Hiervoor kunnen we de
methode blocks in de application_helper aanpassen:
def blocks(&block) block_builder = BlocksBuilder.new yield block_builder block_builder.to_s end
Nu kun je schrijven:
- blocks do |b|
- b.block do
%h2 Titel
%p Paragraaf
- b.block do
%h2 Titel
%p Paragraaf
- b.block do
%h2 Titel
%p ParagraafEn voila voor je het weet heb je een DSL geschreven.
Mixins
Een andere populaire manier om DSL’s te schrijven is door gebruik te
maken van mixin’s. Je hebt vast weleens zoiets gezien:
class Account < ActiveRecord::Model acts_as_statemachine
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
Maar wat doet acts_as_statemachine precies in het bovenstaande
voorbeeld? Het is in feite niet meer dan een klasse methode die je direct
uitvoert op Account. Dus dit zal hetzelfde resultaat hebben:
Account.acts_as_statemachine
acts_as_statemachine voegt in dit voorbeeld allerlei methodes e.d.
toe aan de Account klasse. Let hierbij op dat deze methode is
geimplementeerd op ActiveRecord::Model. Via een mixin is dit aan
de al bestaande ActiveRecord::Model klasse toegevoegd. Je kunt zelf zoiets
te bereiken door een Module te includen/extenden aan een bestaande klasse.
Bijvoorbeeld:
Module ActsAsLeet private def acts_as_leet(name) define_method(:leet) { puts "Hello #{name}, I'm 1337!" } end end ActiveRecord::Model.extend ActsAsLeet
Nu heeft elke klasse die overerft van ActiveRecord::Model de methode
acts_as_leet tot zijn beschikking:
class Account < ActiveRecord::Model acts_as_leet "me"
Na het aanroepen van acts_as_leet is de methode leet beschikbaar
op de Account class:
a = Account.new a.leet # -> Hello me, I'm 1337!
Handig als je een klasse wilt includen/extenden waarbij je wat extra
opties/configuratie wilt kunnen meegeven.
Hash
Hashes kom je ook regelmatig tegen in Ruby on Rails om code leesbaarder te
maken. Een voorbeeld van het gebruik van een Hash als DSL:
Util.convert :weight => 76, :from => :kilograms, :to => :pounds
Dus met een simpele Hash kun je eigenlijk al een simpele DSL schrijven.
Tot slot
yield, block, capture, mixins etc. bieden je stuk voor stuk de mogelijkheden om gebruik te maken van de unieke eigenschappen van Ruby en je eigen DSL te schrijven. Niet besproken in deze blog maar ook interessant is "operator overloading". Ook hiermee kun je je code leesbaarder maken (niet noodzakelijkerwijs onderhoudbaarder daar bepaalde operators ook een bepaald gedrag impliceren).
Experimenteer en schrijf mooiere, "betere" code! En denk ook eens aan je collega's voordat je iets commit!
Verplichte kost (via Iain): http://vimeo.com/12614561

[...] This post was mentioned on Twitter by Finalist IT Group, Died. Died said: I wrote a blog post about Ruby and DSL's http://bit.ly/c3mbVX It is in dutch btw
[...]
Tweets that mention DSL’s en Ruby | Finalist Developers Blog -- Topsy.com July 23, 2010 10:57
Mocht je voor het ‘DRYen’ van je view code op zoek zijn naar een DSL, dan is dit ook nog een mooie optie: http://github.com/markevans/block_helpers
Stefan Borsje July 28, 2010 14:18