Strategy pattern and singleton behavior
Imagine that you have a collection of objects representing animals. Some of them can fly and some others not. You would like to include the method fly
in all them, although not all them can fly, so you go with the Template Method Design Pattern.
class Animal
def fly
raise NotImplementedError
end
end
class Lion < Animal
def fly
"I can't fly!"
end
end
class Duck < Animal
def fly
"I am flying!"
end
end
Depending on the class, you decide whether it can fly or not. But this is not very clever, because we will be creating a lot of duplicate code. We will have as many fly
methods as animals we create, and all of them will have the content repeated again and again.
We can improve the architecture of the code with Strategy Design Pattern.
Strategy Design Pattern
We have two options (strategies) to decide to include on each animal: it can fly or it can not. So we create a module containing both strategies:
module Flys
module ItFlys
def fly
puts "I am flying"
end
end
module CantFly
def fly
puts "I can't fly"
end
end
end
We decide that animals can’t fly as a default. And we include the CanFly
strategy in the animals that can fly:
class Animal
include Flys::CantFly
end
class Lion < Animal
end
class Duck < Animal
include Flys::ItFlys
end
irb> lion = Lion.new
irb> lion.fly
"I can't fly"
irb> duck = Duck.new
irb> duck.fly
"I am flying"
Asigning it by injection
We can still go further and apply the behavior on each instance by injection:
module Flys
module ItFlys
def self.fly
puts "I am flying"
end
end
module CantFly
def self.fly
puts "I can't fly"
end
end
end
class Animal
attr_accessor :fly_behavior
def initialize(fly_behavior = Flys::CantFly)
@fly_behavior = fly_behavior
end
def fly
fly_behavior.fly
end
end
class Lion < Animal
end
class Duck < Animal
end
irb> lion = Lion.new
irb> lion.fly
"I can't fly"
irb> duck = Duck.new(Flys::CantFly)
irb> duck.fly
"I can't fly"
irb> duck.fly_behavior = Flys::ItFlys
irb> duck.fly
"I am flying"
For the issue of flying animals, this last way of doing strategy design seems not the most appropiate. All ducks fly, so I personally prefer the previous solution for this case.
But, we have seen that in this last example we have the option to change the fly_behavior of an instance. How could we do this with the code of the previous solution? I will do it in the next example using a mixin to change the flying behavior of an instance. That means, overwrite a singleton method.
Singleton behavior
But wait, a duck with 1 day of life is not able to fly, so, can a duck fly? Well, it depends. Next code will demostrate how to change this behaviour depending on the value of the instance variable age
.
When the duck is old enough, it can fly!
module Flys
module ItFlys
def fly
puts "I am flying"
end
end
module CantFly
def fly
puts "I can't fly"
end
end
end
class Duck
include Flys::CantFly
DAYS_TO_FLY = 50
def initialize(age=0)
@age = age
@can_fly = false
try_to_fly
end
def age=(days)
@age = days
try_to_fly
end
private
def try_to_fly
return if @can_fly
self.extend(Flys::ItFlys) if @age >= DAYS_TO_FLY
end
end
With this solution, an instance of a Duck
will be able to fly at its seventh week of life, but not before.