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.
What a wonderfully useful post.
ReplyDeleteI'd like to point out that although instance_eval changes scope, it does not preclude you from treating the block as a closure:
class TestClass
def initialize(magic_number)
@magic_number = magic_number
end
def go
magic_number = @magic_number
Email.send do
...
body "Magic number is: #{magic_number}"
end
end
end
(sorry i can't see how to post code effectively in this comment system)
Hey there, me again. I just threw together a quick GIST of a working, simple DSL, using your 'either scope' method. Just in case anyone wants to start hacking straight away.
ReplyDeletehttps://gist.github.com/882271
Thanks for your comments Squee-D! Like your farm example. It's true you can define a local variable from an instance one and use it but it makes me think of the old 'var that = this' in javascript and I shudder :) Good job for pointing that alternative out though. It didn't cross my mind at the time of writing.
ReplyDelete