The strategy pattern is made up of a couple of idioms, “Composition over inheritence” and “Promises”. The strategy pattern can be used alone or part of a bigger pattern. It’s good for making code modular and reuseable without the use of inheritence. There are pitfalls to using inheritence all the time when sharing behavior such as the diamond problem. With inheritence the child class can inherit behavior that it did not intend on inheriting. If you forget to override it, then you will get an unexpected result. The strategy pattern will enforce some diversity in behavior that can be changed at runtime with no unexpected side affects.
class Musician
def dance
put "does a dance"
end
end
class Singer < Musician
end
class Drummer < Musician
end
Now both the Singer
and Drummer
can dance because they inherit the dance
behavior from the parent class of Musician
. What happens when a person can’t
or shouldn’t dance for this song? We could use polymorphism and override
this behavior.
class Musician
def dance
put "does a dance"
end
end
class Singer < Musician
end
class Drummer < Musician
def dance
put "can't dance, won't dance"
end
end
We have now overidden dance
but as developers we are left with the task of
remembering if a Drummer
could or couldn’t dance. Sometimes, we may want a
drummer to be able to dance. The solution becomes to make another class and
override the method in there. That doesn’t sound very DRY. There’s an easier
way to share common behavior and declare which behavior an instantiated child
object can use. To solve this problem we are going to use composition over
inheritence. An example of composition over inheritence would be if instead of
inheriting behavior we chose to compose behavior from an object. So we can
encapsulate specific behavior in a class, instantiate it and use the method
dance
, e.g., CanDanceBehavior.new.dance
will allow us to dance. Best of all,
we can set the dance behavior at instantiation and at runtime. Let’s start with
a default namespace for our dance behavior called DanceBehavior
.
class Musician
attr_accessor :dance_behavior
def initialize(dance_behavior=nil)
@dance_behavior = dance_behavior || DanceBehavior.new
end
def dance
dance_behavior.dance
end
end
class Singer < Musician
def introduce
puts "I am a singer!"
end
end
class Drummer < Musician
def introduce
puts "I am a drummer!"
end
end
Now, let’s define a parent class DanceBehavior
which will make sure all
modules that belong to the family of dance behavior promise to implement a
dance
method. This way, we can use any type of dance behavior on any musician
at any given time without unwanted side affects. Our DanceBehavior class is a
parent class for all of our dance behavior and it’s responsibility is to ensure
all children objects promise to implement specified methods by raising a
NotImplementedError
error. It’s also a good idea to rescue this error so we
don’t stop execution of our program.
class DanceBehavior
def dance
error_message = "\nERROR: Missing behavior for DanceBehavior.perform_dance"
begin
raise NotImplementedError, error_message
rescue NotImplementedError => error
puts error.message
puts error.backtrace.join("\n")
end
end
end
class CanDanceBehavior < DanceBehavior
def dance
puts "::does a dance::"
end
end
class CannotDanceBehavior < DanceBehavior
def dance
puts "Can't dance won't dance"
end
end
If we run the following code we will get a NotImplemented error message for a drummer and after setting the behavior the error goes away.
drummer = Drummer.new
drummer.dance
drummer.dance_behavior = CannotDanceBehavior.new
drummer.introduce
drummer.dance
singer = Singer.new(CanDanceBehavior.new)
singer.introduce
singer.dance
ERROR: Missing behavior for DanceBehavior.dance
example.rb:5:in `dance'
example.rb:37:in `dance'
example.rb:54:in `<main>'
I am a drummer!
Can't dance won't dance
I am a singer!
::does a dance::
By using a little bit of inheritence, composition and promises. We have created modularity and continuity across different types of objects that all do the same thing but in different ways, or in another project as a Gem.
If you find yourself creating extra classes to encapsulate behavior, consider using the strategy pattern. Using these practices will make sure that as your code base grows it stays maintainable.