First, I want to simplify the syntax for declaring things in our DSL. Getting rid of the class definition stuff can make it a lot easier for domain experts to read. A simple, cool declarative style like Rake is what we want to end up with, something like this:
person "Dorothy" do
temperament :nice
food :sunflower_seeds, :carrot_juice
end
Furthermore, I want to extend our DSL to put all of the descriptions of Pets and Persons together into a PetShop. Here is our PetShop DSL:
shop = PetShop.create do
pet "Toto" do
friend_test do |person|
true unless person.temperament == :mean
end
end
pet "Tweety" do
friend_test do |person|
person.has_food?(:sunflower_seeds)
end
end
pet "Slugworth" do
friend_test do |person|
true # I like anyone
end
end
person "Dorothy" do
temperament :nice
food :sunflower_seeds, :carrot_juice
end
person "Witch" do
temperament :mean
food :cheetos, :soda
end
end
One interesting technique used is declaring the "pet" within the "do-end" block for the "petshop" object:
shop = PetShop.create do
pet "Toto" do
friend_test do |person|
true unless person.temperament == :mean
end
end
...
end
The little bit of DSL niceness is achieved using the class_eval method. The block of code within the "do" block is called in the context of the newly created object. Here is the Ruby code that achieves this for a new Pet:
def self.pet(name, &blk)
@pets = Hash.new
p = Pet.new(name)
p.class.class_eval(&blk) if block_given?
@pets[name] = p
p.copyvars
end
Now that we have declared all of the Pets and Persons in to the context of a PetShop, we can test the relationships between Pets and Persons is a more general purpose way then in my previous post. The older, more fragile code was this:
dog = Toto.new
bird = Tweety.new
snail = Slugworth.new
person = Dorothy.new
puts "#{dog.class.name} is a friend of #{person.class.name}: #{dog.is_friend?(person)}"
puts "#{bird.class.name} is a friend of #{person.class.name}: #{bird.is_friend?(person)}"
puts "#{snail.class.name} is a friend of #{person.class.name}: #{snail.is_friend?(person)}"
puts
person = Witch.new
puts "#{dog.class.name} is a friend of #{person.class.name}: #{dog.is_friend?(person)}"
puts "#{bird.class.name} is a friend of #{person.class.name}: #{bird.is_friend?(person)}"
puts
The new, cleaner code is like this:
shop.people.each_value do person
shop.pets.each_value do pet
puts "Is #{pet.name} a friend of #{person.name}? #{pet.is_friend?(person)}"
end
end
And here is the output when we run the program:
Is Toto a friend of Witch? false
Is Slugworth a friend of Witch? true
Is Tweety a friend of Witch? false
Is Toto a friend of Dorothy? true
Is Slugworth a friend of Dorothy? true
Is Tweety a friend of Dorothy? true
We have simplified the syntax of our domain specific language, and added some additional functionality. We have also been able to reduce the amount of Ruby code required at the same time.
Here is the final version of the code:
class DSLThing
def copyvars
self.class.instance_variables.each do |var|
instance_variable_set(var, self.class.instance_variable_get(var))
end
end
end
class PetShop < DSLThing
attr_accessor :pets, :people
def self.create(&block)
f = PetShop.new
f.class.class_eval(&block) if block_given?
f.copyvars
return f
end
def self.pet(name, &blk)
@pets ||= Hash.new
p = Pet.new(name)
p.class.class_eval(&blk) if block_given?
@pets[name] = p
p.copyvars
end
def self.person(name, &blk)
@people ||= Hash.new
p = Person.new(name)
p.class.class_eval(&blk)
@people[name] = p
p.copyvars
end
end
class Animal < DSLThing
attr_accessor :name
def initialize(name=nil)
@name = name
end
end
class Person < Animal
attr_accessor :temperament
def initialize(name=nil)
super
end
def self.temperament(type)
@temperament = type
end
def self.food(*types_of_food)
@food = []
types_of_food.each do |food|
@food << food
end
end
def has_food?(type_of_food)
@food.include?(type_of_food)
end
end
class Pet < Animal
def initialize(name=nil)
super
end
def self.friend_test(&test)
@friend_test = test
end
def is_friend?(person)
@friend_test.call(person) == true
end
end
shop = PetShop.create do
pet "Toto" do
friend_test do |person|
true unless person.temperament == :mean
end
end
pet "Tweety" do
friend_test do |person|
person.has_food?(:sunflower_seeds)
end
end
pet "Slugworth" do
friend_test do |person|
true # I like anyone
end
end
person "Dorothy" do
temperament :nice
food :sunflower_seeds, :carrot_juice
end
person "Witch" do
temperament :mean
food :cheetos, :soda
end
end
shop.people.each_value do |person|
shop.pets.each_value do |pet|
puts "Is #{pet.name} a friend of #{person.name}? #{pet.is_friend?(person)}"
end
end
No comments:
Post a Comment