We want to gain more flexibility when defining new classes and create custom tailored objects on the fly.
As we already know, Ruby is a very dynamic language, but that doesn't apply only to typing: we can even define new methods in our classes at runtime. Thanks to singleton methods we can add new methods to existing instances. If instead of adding one method we want to add a group of them, we can also use the extend
method, that has the same effect as including a module. Last but no least, with the class_eval
method we can evaluate a string in the context of a class, which combined with string interpolations is a really great asset to create new methods at runtime.
Coming back to the Factory Pattern where we created flora and fauna habitats, we might think we need more flexibility. Back then, we had multiple factories that provided us with a fixed list of combinations to create different organisms. If we wanted to have full flexibility for creating any combination we come up with, but avoiding a ton of classes that support every single combination, we can make use of singleton methods:
def new_plant(stem_type, leaf_type)
plant = Object.new
if stem_type == :fleshy
def plant.stem
'fleshy'
end
else
def plant.stem
'woody'
end
end
if leaf_type == :broad
def plant.leaf
'broad'
end
else
def plant.leaf
'needle'
end
end
plant
end
plant1 = new_plant(:fleshy, :broad)
plant2 = new_plant(:woody, :needle)
puts "Plant 1's stem: #{plant1.stem} leaf: #{plant1.leaf}"
puts "Plant 2's stem: #{plant2.stem} leaf: #{plant2.leaf}"
We start with a plain Ruby Object
and tailor it adding new methods. Instead of adding them one by one, we can add a group of them:
def new_animal(diet, awake)
animal = Object.new
if diet == :meat
animal.extend(Carnivore)
else
animal.extend(Herbivore)
end
if awake == :day
animal.extend(Diurnal)
else
animal.extend(Nocturnal)
end
animal
end
In this example, the extend
method does the same as including a module.
Now, let's imagine that we want to group together animals and trees that share a section of the jungle, and also keep track of their biological classifications. Although they look like two different programming problems, they are quite similar: both of them aim to group a set of objects. Ideally, we could stablish different kind of relationships between objects on the fly like this:
class Tiger < CompositeBase
member_of(:population)
member_of(:classification)
end
class Tree < CompositeBase
member_of(:population)
member_of(:classification)
end
class Jungle < CompositeBase
composite_of(:population)
end
class Species < CompositeBase
composite_of(:classification)
end
tony_tiger = Tiger.new('tony')
se_jungle = Jungle.new('southeastern jungle tigers')
se_jungle.add_sub_population(tony_tiger)
The method composite_of(:group)
would provide a method for including members to any :group
we could think of by adding dynamic add_sub_:group
methods to the class instances. The same way, the method member_of(:group)
would a method parent_:group
to leaf nodes so that they can know what group they are member of. It happens that all this is absolutely feasible with some meta programming:
class CompositeBase
attr_reader :name
def initialize(name)
@name = name
end
def self.member_of(composite_name)
code = %Q{
attr_accessor :parent_#{composite_name}
}
class_eval(code)
end
def self.composite_of(composite_name)
member_of composite_name
code = %Q{
def sub_#{composite_name}s
@sub_#{composite_name}s = [] unless @sub_#{composite_name}s
@sub_#{composite_name}s
end
def add_sub_#{composite_name}(child)
return if sub_#{composite_name}s.include?(child)
sub_#{composite_name}s << child
child.parent_#{composite_name} = self
end
def delete_sub_#{composite_name}(child)
return unless sub_#{composite_name}s.include?(child)
sub_#{composite_name}s.delete(child)
child.parent_#{composite_name} = nil
end
}
class_eval(code)
end
end
CompositeBase
is the base class of the rest of components and implements the member_of
and composite_of
methods. Simply passing them the name of the group, they set up all the methods we need by constructing a string that defines them. The call to class_eval
interprets the string in the context of the class, making them available.