Previously, I was exploring the basics of DSL creation using Ruby. This post continues sharing my lessons learned while developing a prototype of a domain specific language in the mortgage industry. Since I cannot share the actual code itself belonging to my client, I will continue to extract the useful concepts into these simple examples.
Last time we created a simple Animal DSL class. The important part of that class is this bit that makes sure our DSL like syntax works for the declarative methods:
class Animal
attr\_accessor :number\_of\_legs
def self.number\_of\_legs(number\_of\_legs)
@number\_of\_legs = number\_of\_legs
end
def initialize
self.class.instance\_variables.each do |var|
instance\_variable\_set(var, self.class.instance\_variable\_get(var))
end
end
end
Now we will create a Person class that can interact with the Animals. Each Person will have a temperament (either mean or nice), and will be carrying some kind of food in their pocket to feed their pet. We will define people using our DSL as follows:
class Dorothy < Person
temperament :nice
food :sunflower\_seeds, :carrot\_juice
end
class Witch < Person
temperament :mean
food :cheetos, :soda
end
The implementation is very similar to the basic Animal class
class Person < Animal
attr\_accessor :temperament
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
Since we are working with an array, the self.food method has a variable number of parameters, represented using the asterisk in front of the parameters list. The cool little idiom @food ||= [] returns with the current @food variable, or if it is nil, returns an empty array. There is also a has_food? method to tell us if a person is carrying a certain type of food.
Now let us introduce the Pet. We will use the power of Ruby blocks to tell each pet the rules about when it likes a person, or not. Here is the DSL we want to use for the Pets:
class Toto < Pet
friend\_test do |person|
true unless person.temperament == :mean # I like anyone who is not mean
end
end
class Tweety < Pet
friend\_test do |person|
true if person.has\_food?(:sunflower\_seeds) # I like anyone who has sunflower seeds
end
end
class Slugworth < Pet
friend\_test do |person|
true # I like anyone
end
end
And now the implementation of the Pet class:
class Pet < Animal
def self.friend\_test(&test)
@friend\_test = test
end
def is\_friend?(person)
@friend\_test.call(person) == true
end
end
The friend_test method stores a block containing the test for that Pet, and the is_friend? method tests to see if a Pet will be friendly toward a particular Person.
Last, we put it all together with some simple code to display the interactions between the People and the Pets:
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 "#{snail.class.name} is a friend of #{person.class.name}: #{snail.is\_friend?(person)}"
The result of running this code is:
Toto is a friend of Dorothy: true
Tweety is a friend of Dorothy: true
Slugworth is a friend of Dorothy: true
Toto is a friend of Witch: false
Tweety is a friend of Witch: false
Slugworth is a friend of Witch: true
Using this simple Ruby DSL approach, it is very easy to add a new Person or Pet, just by entering the rules that define its behavior. As the objects in a system become more complex, one of the best ways to manage that complexity is to create an abstraction that hides the ugly bits.
Here is the full code for this example:
class Animal
attr\_accessor :number\_of\_legs
def self.number\_of\_legs(number\_of\_legs)
@number\_of\_legs = number\_of\_legs
end
def initialize
self.class.instance\_variables.each do |var|
instance\_variable\_set(var, self.class.instance\_variable\_get(var))
end
end
end
class Person < Animal
attr\_accessor :temperament
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 self.friend\_test(&test)
@friend\_test = test
end
def is\_friend?(person)
@friend\_test.call(person) == true
end
end
class Toto < Pet
friend\_test do |person|
true unless person.temperament == :mean
end
end
class Tweety < Pet
friend\_test do |person|
person.has\_food?(:sunflower\_seeds)
end
end
class Slugworth < Pet
friend\_test do |person|
true
end
end
class Dorothy < Person
temperament :nice
food :sunflower\_seeds, :carrot\_juice
end
class Witch < Person
temperament :mean
food :cheetos, :soda
end
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 "#{snail.class.name} is a friend of #{person.class.name}: #{snail.is\_friend?(person)}"