DSL’s en Ruby

23 July 2010 9:58 Diederick Lawson 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:

  1. Is er sprake van code duplicatie? (als in: hevige code duplicatie, dus niet iets wat maar twee keer gebruikt gaat worden)
  2. Voegt het wat toe aan de leesbaarheid van je code?
  3. Is na het toepassen van een DSL sprake van een duidelijke verbetering?

Denk in ieder geval goed na voor je een DSL opzet:

  1. Bedenk goed welk probleem je nu precies wilt oplossen (is er Überhaupt een probleem?)
  2. 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?
  3. 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 Paragraaf

Hoe 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 Paragraaf

En 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

2 reacties »

  1. [...] 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

  2. 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

Reageer

RSS feed for comments on this post · TrackBack URI