Demystifying Ruby DSLs — Part 2
Last time we learned about how to use modules to dynamically add functionality to a class. This time let’s take a look at doing that, but customizing our extensions at runtime.
What helped me ultimately understand how these things work is that you are building up classes on the fly. It’s kind of like when you
include a module, it’s editing that class to add more methods to it. Think of a bunch of Legos. Each Lego is a module with various methods on it. At runtime they assemble together to build a castle.
What if those Legos could generate even more Legos as you were building with them, and then intelligently join themselves together?
If you’re familiar with Rails, you’ve seen that you can just declare associations within a model —
:has_one, and so on. Once you add those directives to your class, suddenly you have access to brand spanking new methods. Have you ever wondered how that worked? Let’s implement a rudimentary version.
Think for a moment about
:has_many. What would you expect the line
has_many :gerbils methods to do? You would have to have a
gerbils method to retrieve the little fellas, and another one,
gerbils=(new_value) to set them (and others to add them and so on, but KISS). You implement that with a generic
get_child_models(child_name) method, but that feels like the Java (™ Oracle Corporation) way… and I have too much self respect to go down that path. Instead we can take advantage of Ruby’s metaprogramming capabilities and generate them dynamically.
One way to do this is with
A Word on Eval
Ruby has a few versions of
eval. They all take strings or blocks and turn them in code that is executed.
- There’s the standard one that executes arbitrary code.
- class_eval — similar to vanilla
eval, but executes the code in the context of the Class itself. There’s also
module_evalwhich does the same thing (more or less). Example:
1 2 3 4 5 6
- instance_eval — modifies a class, but from an instance point of view, and only for that particular instance (this is called a Singleton method).
1 2 3 4 5 6 7 8
Using these techniques you can generate the set of methods for our
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
Now, if you
include Associations in your class, you can call
has_many :hamsters or
has_many :gerbils or
has_many :guinea_pigs and have all of your getters and setters created.
I’m not a big fan of
eval, at least when using it with strings. The biggest reason is that it makes bugs harder to find. The Ruby interpreter will point out syntax errors when the file loads, but a typo in an evalled string won’t get caught until runtime. The longer the string, the more likely something bad will creep in there. And some of these dynamically created methods will be long. I’m talking Lord of the Rings Extended Edition long.
Fortunately there is a better way. The eval methods also take blocks, which work pretty well in most cases. For the purposes of dynamically generating methods, I prefer using
define_method source. It’s available on
Module (and therefore classes too) and, just like it says on the tin, is designed to create methods on the fly and add them to a class.
1 2 3 4 5 6 7 8 9
It’s kinda similar to the eval code, in fact,
define_method passes itself along to
instance_eval, so when all is said and done, it’s merely for our convenience. But is easier to test, and will complain loudly if there’s a syntax error.
Working with actual code rather than a string makes refactoring easier too. Let’s say you want to enable your users to define their own implementations of the generated rodent methods. Pulling that out into its own method is simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
That’s still possible with string evals, but is easier to read in my eyes.
That’ll wrap up this entry on DSLs. There’s only one other big piece of the pie I’d like to cover — blocks, but you can do a whole lot without them.
One thing to keep in mind when writing DSLs is that it can be hard to follow along. Document everything, especially the esoteric parts. It might even be a good idea to diagram the path of all the
include chain. DSLs can make client code easier to write, but usually at the expense of crazy complexity within the DSL itself.