One of the things I love about Ruby is the massive advantages gained by the way it's built - things like blocks make it so easy for Ruby to have amazing looking, clean code. The great thing is, this also extends to writing your code in Ruby!
Want the people consuming your code to have easy to understand, learn and read code? Write a nice DSL for it. (I'm going to take a second here - DSL stands for Domain Specific Language.. kind of like another programming language, that is structured in the best possible way to interact a specific domain)
Take for example an email sending class..
Email.new("clocKwize@gmail.com", "spammaster@yahoo.com", "I love you", "Not really!").send
Now, it's pretty straightforward what this does, but it'll be easy to forget which email it'll send to and which email it'll appear to come from.. Lets write a DSL to make this way more readable, and more extensible, should we need to add more parameters and options later
Email.send do
to "clocKwize@gmail.com"
from "spammaster@yahoo.com"
subject "I love you"
body "Not really!"
end
Now, that code may be longer, but no developer will ever find it hard to see exactly what the code is doing, they'll never have to go and look at the definition of Email to find out.
Now, there is no harm in providing both ways to use your code, you don't want to restrict people to use a DSL, it's usually a layer built on top of an API. Here is how I wrote the code initially
class Email
def initialize(to = nil, from = nil, subject = nil, body = nil)
@to, @from, @subject, @body = to, from, subject, body
end
def self.send(&block)
email = Email.new
email.instance_eval &block
email.send
end
def to(value)
@to = value
end
def from(value)
@from = value
end
def subject(value)
@subject = value
end
def body(value)
@body = value
end
def send
puts "Sending an email!"
end
end
Now this works nicely by using a cool method called
instance_eval. This basically takes a block and runs it but with a 'self' of whatever you are calling instance_eval on. In this case, when the block is run, and it comes across to "clocKwize@gmail.com" - this is calling self.to("clocKwize@gmail.com") on the Email object created in send.
The only draw back to this is, once self is our email object, we can no longer access instance stuff in the class we are using the DSL in!
For example
class TestClass
def initialize(magic_number)
@magic_number = magic_number
end
def go
Email.send do
...
body "Magic number is: #{@magic_number}"
end
end
end
Now, this looks like it should work, but it wont because Ruby will be looking for @magic_number on the Email object, which isn't what the user of your DSL intended at all.
The best way around this to pass the instance of the Email to the block as a parameter. Not quite as slick but the block keeps its original scope and works fine:
Email.send do |email|
...
email.body "Magic number is: #{@magic_number}"
end
This is accomplished by changing the self.send method as follows
def self.send(&block)
email = Email.new
block[email]
email.send
end
But you know, I kind of like the first way, and if the user doesn't need any instance stuff from the class they are in, we shouldn't stop them using the prettier DSL. A very simple way to do this is to ask the block how many parameters it accepts..If it takes one, pass the Email instance, if it doesn't use self as the Email instance.
def self.send(&block)
email = Email.new
block.arity == 1 ? block[email] : email.instance_eval(&block)
email.send
end
There we go.. the best of both worlds! Consumers of your DSL can use whatever way they are comfortable with and your code will work either way. Hopefully this will help someone on their path of discovery.