For example, let's say we now want each Pet to be able to do some trick, when called on to do so. When you are defining behaviors, it is very useful to define a recipe-like syntax in a DSL.
Here is an example:
pet "Toto" do
when_performing_tricks do
sit
speak
end
end
This is just a description or recipe of what the animal is supposed to do when asked to perform it's trick. When we really want our Pet to perform, we simply say:
pet.perform
This produces the following output:
Toto will now perform...
Toto is sitting...
Toto says 'Woof!'
Let's hear some applause for Toto!
Slugworth will now perform...
Slugworth is sitting...
Let's hear some applause for Slugworth!
Tweety will now perform...
Tweety is sitting...
Tweety says 'I thought I saw a putty tat!
Tweety is flying...
Let's hear some applause for Tweety!
The Pet will do whatever tricks it knows, using this little simple bit of Ruby magic:
def self.when_performing_tricks(&routine)
@routine = routine
end
def perform
puts "#{name} will now perform..."
@routine.call
puts "Let's hear some applause for #{name}!"
end
The "when_performing_tricks" method simply stores the block, and executes it only when the time comes to "perform".
So how does the Pet know what is entailed in each trick? I have put the "sit" trick into the base Pet class (every Pet knows how to sit):
def self.sit
puts "#{name} is sitting..."
end
We can also more importantly define custom tricks per type of Pet. A simple bit of Ruby metaprogramming goodness helps us out:
def self.define_trick(name, &trick_definition)
singleton_class.class_eval do
define_method name, &trick_definition
end
end
The "class_eval" methos lets us evaluate the inside expression in the context of the class object, not just a particular instance of the object. Another bit worth noting is the helper method that I have added to the DSLThing base class called "singleton_class" that looks like this:
def self.singleton_class
class << self; self; end
end
This is just a short cut to get to the class instance object, known better to Rubyists as the "singleton_class".
Lastly, the "define_method" method then lets us define a new method for the trick. When we want to define a new trick for a particular Pet, we can simply define it like this:
define_trick "speak" do
puts "Toto says 'Woof!'"
end
Putting it all together, here are our new definitions of the tricks our Pets can do:
pet "Toto" do
when_performing_tricks do
sit
speak
end
define_trick "speak" do
puts "Toto says 'Woof!'"
end
end
pet "Tweety" do
when_performing_tricks do
sit
speak
fly
end
define_trick "speak" do
puts "Tweety says 'I thought I saw a putty tat!"
end
define_trick "fly" do
puts "Tweety is flying..."
end
end
pet "Slugworth" do
when_performing_tricks do
sit
end
end
One other interesting technique of note is that we are actually defining a new type of Pet by dynamically declaring a new class based on the Pet class. Otherwise, our custom tricks for one type of Pet might interfer with the custom tricks of another. The solution to this is using the "Object.const_set" and "Object.const_get". Here is the code I used:
def self.pet(name, &blk)
@pets ||= Hash.new
klass = Class.new(Pet)
Object.const_set(name, klass) if not Object.const_defined?(name)
p = Object.const_get(name).new
p.name = name
p.class.class_eval(&blk) if block_given?
p.copyvars
@pets[name] = p
end
In this posting I have used the DSL recipe technique, and created dynamic methods and classes. Combining these techniques can allow for a very powerful and yet concise syntax when creating your own Ruby domain specific languages.
Here is the complete listing of the code from this post:
class DSLThing
def copyvars
self.class.instance_variables.each do |var|
instance_variable_set(var, self.class.instance_variable_get(var))
end
end
def self.singleton_class
class << self; self; end
end
end
class PetShop < DSLThing
attr_accessor :pets, :people
def self.create(&block)
f = PetShop.new
f.class.instance_eval(&block) if block_given?
f.copyvars
return f
end
def self.pet(name, &blk)
@pets ||= Hash.new
klass = Class.new(Pet)
Object.const_set(name, klass) if not Object.const_defined?(name)
p = Object.const_get(name).new
p.name = name
p.class.class_eval(&blk) if block_given?
p.copyvars
@pets[name] = p
end
end
class Animal < DSLThing
attr_accessor :name
def initialize(name=nil)
@name = name
end
end
class Pet < Animal
def initialize(name=nil)
@name = name
super
end
def self.when_performing_tricks(&routine)
@routine = routine
end
def self.define_trick(name, &trick_definition)
singleton_class.class_eval do
define_method name, &trick_definition
end
end
def perform
puts "#{name} will now perform..."
@routine.call
puts "Let's hear some applause for #{name}!"
end
def self.sit
puts "#{name} is sitting..."
end
end
shop = PetShop.create do
pet "Toto" do
when_performing_tricks do
sit
speak
end
define_trick "speak" do
puts "Toto says 'Woof!'"
end
end
pet "Tweety" do
when_performing_tricks do
sit
speak
fly
end
define_trick "speak" do
puts "Tweety says 'I thought I saw a putty tat!"
end
define_trick "fly" do
puts "Tweety is flying..."
end
end
pet "Slugworth" do
when_performing_tricks do
sit
end
end
end
shop.pets.each_value do |pet|
pet.perform
end
No comments:
Post a Comment